【翻译】减少动态派发提高性能

和其它许多语言一样,Swift 允许子类覆写父类的方法和属性。这意味着程序需要在运行时才能确定哪个方法和属性被调用和访问。这便是通常称为的动态派发。动态派发增加了语言的可表达性,但是会牺牲恒定的性能时间。这在性能敏感的代码中时不可取的。

来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(newP, newVelocity: newV)
}
}

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

上面的代码,编译器将会执行动态派发,其流程是:

  1. 调用p的update方法
  2. 调用p的updatePoint方法
  3. 获取p的point属性
  4. 获取p的velocity属性

因为 ParticleModel 的 属性和方法可能会被其子类覆写,所以对 ParticalModel 的属性和方法的访问和调用都是动态派发的。

在 Swift 里,动态派发的实现是从函数表中查找相应的方法后间接调用。这会比直接调用要慢。另外,间接调用还会阻止许多编译器的优化使得间接调用更耗性能。所以,对于性能要求比较高的代码有几种技巧可以限制动态派发。

使用 final

final 关键字可以用在类、方法和属性上,表明这些是不可以被覆写的。这可以让编译器安全地忽略动态派发过程。例如,在下面的代码中,point 和 velocity 将通过对象存储属性的加载直接访问,并通过直接函数调用来调用 updatePoint()。另一方面,update() 因为没有使用 final 关键字,可以被重写,所以将会继续使用动态派发。

1
2
3
4
5
6
7
8
9
10
11
12
13
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(newP, newVelocity: newV)
}
}

当然也可以把整个类都加上 final 关键字,这样的话这个类就无法再被子类化,其属性和方法自然也就不可能被重写,所以就不存在动态派发了。

自动推断为final

在声明中使用 private 关键字会将可见性限制为当前文件。这样编译器就能轻松地找到所有可以被重写的方法。无法被重写的方法和属性将会被编译器自动推断为使用 final,并删除对方法和属性的间接调用。
假设在当前文件里没有类重写 ParticleModel,编译器会替换所有的动态派发为直接调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
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(newP, newVelocity: newV)
}
}

使用 Whole Module Optimization

使用默认的权限控制只在声明的模块内可见。因为 Swift 通常是按照模块分别编译的,编译器无法确定一个 internal 的声明是否会在另一个模块中被重写。但是,如果开启了 Whole Module Optimization 的话,所有模块会在一起被编译,这样编译器便可以在编译时判断哪些声明是没有被重写的,因此可以推断出哪些可以使用 final 关键字。
还是之前的代码,这次为 ParticalModel 和 update 方法添加上 public 关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(newP, newVelocity: newV)
}
}

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

当使用 Whole Module Optimization 选项编译这段代码时,编译器可以推断出 point,velocity 和 updatePoint() 方法都可以使用 final 关键字。相反,update() 因为声明为 public,所以无法被优化为使用 final。