Swift函数派发优化

通过减少动态调度来提高性能

与许多其他语言一样,Swift允许类重写其父类中声明的方法和属性。
这意味着程序必须在运行时确定要引用哪个方法或属性,然后执行间接调用或间接访问。
这种称为动态调度的技术以每次间接使用时恒定的运行时开销为代价提高了语言的表达能力。
在对性能敏感的代码中,我们不希望花费这些开销。
这篇博客文章展示了通过消除这种动态性来提高性能的三种方法:final,private和Whole Module Optimization。

看一下以下示例:

class ParticleModel {
    var point = ( 0.0, 0.0 )
    var velocity = 100.0

    func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newPoint: newP, newVelocity: newV)
    }
}

var p = ParticleModel()
for i in stride(from: 0.0, through: 360, by: 1.0) {
    p.update(newP: (i * sin(i), i), newV:i*1000)
}

按照编写的方式,编译器将发出一个动态调度指令:

  1. 变量p调用update函数.
  2. 变量p的update函数又调用了updatePoint函数.
  3. 获取p的point元组属性.
  4. 获取p的velocity属性.

这可能不是你所希望看到的。但是动态调用是必需的,因为ParticleModel的子类可能会重写属性point和velocity,或者使用新的实现覆盖updatePoint或update函数。

在Swift中,动态调度调用是通过从函数表中查找函数然后执行间接调用来实现的(函数表派发),这比直接派发要慢。
此外,间接调用还会阻止许多编译器优化,从而使间接调用的成本更高。
在对性能有要求的代码中,有一些技巧可以用来限制不需要提高性能的动态行为。

当不需要重写这个函数时,请使用final

final关键字可以限制类,方法,或者属性被子类重写,这使编译器可以安全地消除动态调度间接寻址,例如,在下面的point和velocity属性将通过对象的存储属性中直接访问,而updatePoint将被直接派发。另一方面,update仍将通过动态调度来调用,从而允许子类可以重写update方法:

class ParticleModel {
    final var point = ( x: 0.0, y: 0.0 )
    final var velocity = 100.0

    final func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newPoint: newP, newVelocity: newV)
    }
}

通过将属性附加到类本身,可以将整个类标记为final。这禁止对类进行子类化,这意味着该类的所有功能和属性也都是final。

final class ParticleModel {
	var point = ( x: 0.0, y: 0.0 )
	var velocity = 100.0
	// ...
}

通过应用private关键字推断在一个文件中引用的final声明

将private关键字应用于声明会限制该声明对当前文件的可见性。这使编译器可以找到所有可能覆盖的声明。缺少任何此类覆盖声明,可使编译器自动推断final关键字,并删除对方法和属性访问的间接调用。

假如没有什么类在当前文件中重写ParticleModel,编译器可以将所有的动态分派调用与直接调用用私有声明代替。

class ParticleModel {
    private var point = ( x: 0.0, y: 0.0 )
    private var velocity = 100.0

    private func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
        updatePoint(newPoint: newP, newVelocity: newV)
    }
}

与前面的示例一样,子类不能直接访问point和velocity,并不能直接调用updatePoint。因为update不是私有方法,因此可以再次被子类重写和调用。

就像final一样,可以将private属性应用于类声明本身,从而导致该类是私有的,因此也可以让该类的所有属性和方法都变为私有。

private class ParticleModel {
	var point = ( x: 0.0, y: 0.0 )
	var velocity = 100.0
	// ...
}

使用整个模块的优化来推断最终的内部声明

具有内部访问权限的声明(如果未声明,则为默认值)仅在声明它们的模块中可见。
由于Swift通常单独编译组成模块的文件,所以编译器无法确定是否在其他文件中重写了内部声明。
但是,如果启用了整个模块优化,则所有模块将同时编译在一起。
这允许编译器一起对整个模块进行推断,并在没有可见重写的情况下对内部声明进行final推断。
让我们回到原始代码段,这一次向ParticleModel添加了一些额外的public关键字:

public class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0

    func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    public func update(newP: (Double, Double), newV: Double) {
        updatePoint(newPoint: newP, newVelocity: newV)
    }
}

var p = ParticleModel()
for i in stride(from: 0.0, through: 360, by: 1.0) {
    p.update(newP: (i * sin(i), i), newV:i*1000)
}

在使用整个模块优化编译此代码段时,编译器可以根据属性point、velocity和方法updatePoint推断是需要用final关键字修饰的。
相反,update不用final关键字修饰,因为update具有公共访问权限。

发布了249 篇原创文章 · 获赞 926 · 访问量 149万+

猜你喜欢

转载自blog.csdn.net/youshaoduo/article/details/103906529
今日推荐