通过减少动态派送提升性能

本文翻译自官方 swift 博客,感谢戴仓薯校稿。

和其他许多语言一样,Swift 允许一个类的子类重写方法和属性。这就意味着程序必须在运行时决定关联哪个方法或属性,从而间接调用或间接访问。这项叫动态派送的技术,以每次间接调用固定的运行时开销为代价,增强了语言的表达性。如果对于性能敏感,这种方法往往是不可取的。这篇文章将介绍3种消除动态机制来提升性能的方法:final, private 和全局模块优化。

先细读下面例子:

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)
}

如上所写,编译器会建立一个动态派送调用:

  • 调用 update()
  • 调用 updatePoint()
  • Get 属性 point 元组,
  • Get 属性 velocity

当你当时看这段代码,这可能和你预想的不一样。因为 ParticleModel 的子类可能使用计算属性重写 pointvelocity,或者重写 updatePoint()update() 方法,因此动态调用是必要的。

在 Swift,执行动态派送调用就是在方法列表里寻找函数,然后间接调用。这会慢于直接调用。另外,间接调用阻止了许多编译器优化,使得使用间接调用成本更高。在性能要求严格的情况下,这里有一些技术可以在不需要动态行为的时候提升性能。

当你确定声明不需要被重写时,使用 final

关键字 final 可以标示类、方法和属性的声明(declaration)不能被重写。编译器便会安全地省略动态派送。例如在下面这个代码,访问 pointvelocity 会直接加载对象的储存属性,并且调用 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(newP, newVelocity: newV)
}
}

把整个类标记 final 也是可行的,只需加在 class 前面。这会禁止子类继承,隐式地声明该类所有的函数和属性都是 final

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

通过应用 private 关键字,把仅在一个文件中出现的声明推断为 final

给声明(declaration)应用 private 关键字会把它限制为仅在当前文件可见。这允许编译器去寻找所有潜在可重写的声明。未存在重写的声明会使得编译器自动推断为 final,去除访问方法和属性时的间接调用。

假设没有一个类在当前文件重写了 ParticleModel,编译器会将所有 private 声明的动态派送调用替换为直接调用。

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)
}
}

上面这个例子,pointvelocity 会被直接访问,updatePoint() 会被直接调用。而 update() 则再一次被间接调用,因没有 private 标记。

final 一样,给类声明标记 private 也是可以的,这回使得类是私有的,它的所有属性和方法也是私有的。

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

使用全局模块优化,将 internal 声明推断为 final

带有 internal 访问的声明(如果没有显式声明,默认也是如此)只有在当前模块才可见。因为 Swift 通常会分开编译同一模块的不同文件,编译器不能推断一个 internal 声明会不会在其他文件中被重写。但是,如果全局模块优化可用,所有的模块会在同一时间被编译。这就允许编译器将所有模块一起处理,如果 internal 声明没有可见的重写,就推断为 final。

(译者注:全局模块优化设置位置在:Build Setting >> Swift Compiler-Code Generation >> Whole Module Optimization。)

回到最开始的代码,这次我们给 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(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)
}

当用全局模块优化编译这段代码,编译器会推断属性 point,velocity 和方法 updatePoint()final。相对的,update() 被标记为 public,不会被推断为 final