Farlanki

Swift代码性能优化

字数统计: 1.5k阅读时长: 5 min
2016/07/28 Share

Swift作为苹果推出并且大力推动的一种语言,其一出现便受到了极大关注.在看了Understanding Swift PerformanceOptimizing Swift Performance,Building Better Apps with Value Types in Swift这几个WWDC session之后,我对swift有了更深入的理解.

三个量度

既然要讨论性能优化,那么必须先定义如何衡量一段代码的性能好坏.下面是用来衡量一段程序性能好坏的三个方面:

  • 实例是在堆还是在栈上分配?
  • 当使用实例的时候,需要进行多少的引用计数管理?
  • 当调用实例的方法时,这个方法是被静态调度还是被动态调度的?

内存分配

变量在内存中可以被分配到堆上或者栈上.当变量被分配在栈上的时候,速度非常快,因为系统所需要做的只是管理一个指向栈顶的指针.
对比之下,变量在堆上分配没那么快,但是动态性比较高,需要一个更加复杂的数据结构.当变量被分配到堆上的时候,系统会搜索堆中符合该变量大小的未使用的区域进行分配.更进一步,因为可能同时有多条线程在分配堆上的空间,堆需要一个同步机制来确保数据完整性,这种情况会耗费大量的系统资源.
举个简单的例子,使用结构体的时候,和使用类的时候就是分别属于被分配在栈和堆上的情况.


使用enum代替string,尽量使用值类型等,可以让变量的内存被分配在栈上.

引用计数

和性能相关的第二个量度就是使用了多少引用计数.引用计数不仅是计数的加减这么简单,它还要求线程安全.频繁的引用计数可能会导致性能瓶颈.
在一个结构体内包含引用也会导致修改引用计数的情况增加.
对于拥有多个引用类型的类,使用一个wrapper能减少操作引用计数的情况.

方法调度

方法调度有两种情况:静态调度和动态调度.
静态调度:

  • 在运行时直接跳到方法实现的地方运行
  • 能进行如内联等优化
    动态调度:
  • 在运行时查表来寻找方法的实现,再跳到该实现
  • 不能进行内联等优化
1
2
3
4
5
6
7
8
9
struct Point {
var x, y: Double
func draw() {
// Point.draw implementation
} }
func drawAPoint(_ param: Point) {
param.draw()
}
let point = Point(x: 0, y: 0) paorinatm.draw()

当我们写出类似这样的代码时,实际上编译器为我们生成的是:

1
2
3
4
5
6
7
8
9
10
struct Point {
var x, y: Double
func draw() {
// Point.draw implementation
} }
func drawAPoint(_ param: Point) {
param.draw()
}
let point = Point(x: 0, y: 0)
// Point.draw implementation

当我们的代码实现了多态性,需要用到动态调用的情况下,编译器实际上生成了查找V-Table,并且执行正确的方法的代码.

###private和final
如果我们调用的方法属于的实例所属的类被final关键字修饰,因为该类没有子类,那么编译器就可以知道这时候应该调用的方法究竟是哪个方法,进行静态调度.另外,如果某个方法被private修饰,也会让编译器知道该方法对子类不可见,从而得知多态性对于该方法不存在,进行静态调度.所以,如果我们在不需要多态性的情况下使用了多态性,那就会带来性能上的影响

##协议类型
作为世界上第一种面向协议的编程语言,Swift中的多态性可以脱离继承和引用类型,换言之,结构体也拥有了多态性,其是使用协议类型实现的.
那么具体又是怎样实现的呢?
每个协议类型都拥有一个 protocol witness table (PWT),指向实现了协议所规定的方法的实现.这样,就可以找到协议中每个方法的实现.


但是,只有大小相同的实例才能被放入数组,这又要怎么实现呢?
答案:使用Existential Container.Existential Container拥有5个字节,前三个被称为inline Value Buffer.对于少于等于三个字节的数据,直接放入inline Value Buffer,否则,数据储存在堆中,inline Value Buffer中使用一个指针指向堆中的数据.


另外,还有另一个表Value Witness Table (VWT)复制维护这个协议类型实例的生命周期.


下面是原始的代码和编译器生成的代码的对比.

写时复制

问题又来了.我们知道结构体是值类型,如果我们构造的结构体拥有数个字节数大于3的协议类型的变量,而我们又需要将这些结构体进行拷贝,那么配在堆上的空间岂不是很多?
Swift为这种情况做出了优化,方法是写时复制.在协议类型需要被改写的时候,通过引用计数的不同而采取不同的方法.如果引用计数为1,直接改写,否则先复制,在改写.


另外,如果一个结构体中包含一个类对象,也会使用写时复制这种方法.

泛型

使用泛型的话,就可以拥有更强大的特性:静态多态性.Swift会为每个使用使用了泛型作为参数的函数创建一个新的函数,该函数由调用函数时传递的参数的实际类型确定.更进一步,由于Swift编译器强大的代码优化能力,静态多态性并不会增加代码的数量.

使用Whole Module Optimizations,使同一个Module内的代码一起编译,能让处于不同文件的优化成为可能.

CATALOG
  1. 1. 三个量度
  2. 2. 内存分配
  3. 3. 引用计数
  4. 4. 方法调度
  5. 5. 写时复制
  6. 6. 泛型