陈斌彬的技术博客

Stay foolish,stay hungry

Swift-性能考虑

在 WWDC 14 的 Keynote 上,Swift 相比于其他语言的速度优势被特别进行了强调。但是这种速度优势是有条件的,虽然由于编译器的进步我们可能可以在不了解语言特性的时候随便写也能得到性能上的改善,但是如果能够稍微理解背后的机制的话,我们就能投 “编译器所好”,写出更高效的代码。

相比于 Objective-C,Swift 最大的改变就在于方法调用上的优化。在 Objective-C 中,所有的对于 NSObject 的方法调用在编译时会被转为 objc_msgSend 方法。这个方法运用 Objective-C 的运行时特性,使用派发的方式在运行时对方法进行查找。因为 Objective-C 的类型并不是编译时确定的,我们在代码中所写的类型不过只是向编译器的一种“建议”,不论对于怎样的方法,这种查找的代价基本都是同样的。

这个过程的等效的表述可能类似这样 (注意这只是一种表述,与实际的代码和工作方式无关):

methodToCall = findMethodInClass(class, selector);
// 这个查找一般需要遍历类的方法表,需要花费一定时间

methodToCall();  // 调用

Objective-C 运行时十分高效,相比于 I/O 这样的操作来说,单次的方法派发和查找并不会花费太多的时间,但实事求是地说,这确实也是 Objective-C 性能上可以改进的地方,这种改善在短时间内大量方法调用时会比较明显。

Swift 因为使用了更安全和严格的类型,如果我们在编写代码中指明了某个实际的类型的话 (注意,需要的是实际具体的类型,而不是像 Any 这样的抽象的接口),我们就可以向编译器保证在运行时该对象一定属于被声明的类型。这对编译器进行代码优化来说是非常有帮助的,因为有了更多更明确的类型信息,编译器就可以在类型中处理多态时建立虚函数表 (vtable),这是一个带有索引的保存了方法所在位置的数组。在方法调用时,与原来动态派发和查找方法不同,现在只需要通过索引就可以直接拿到方法并进行调用了,这是实实在在的性能提升。这个过程大概相当于:

let methodToCall = class.vtable[methodIndex]
// 直接使用 methodIndex 获取实现

methodToCall();  // 调用

更进一步,在确定的情况下,编译器对 Swift 的优化甚至可以做到将某些方法调用优化为 inline 的形式。比如在某个方法被 final 标记时,由于不存在被重写的可能,vtable 中该方法的实现就完全固定了。对于这样的方法,编译器在合适的情况下可以在生成代码的阶段就将方法内容提取到调用的地方,从而完全避免调用。

通过 Benchmark 我们可以看出,在一些基本的算法上,Swift 的速度是要远胜过 Objective-C 的,而就算相较于世界上无可匹敌的 C,也没有逊色太多。

所以对于性能方面,我们应该注意的地方就很明显了。如果遇到性能敏感和关键的代码部分,我们最好避免使用 Objective-C 和 NSObject 的子类。在以前我们可能会选择使用混编一些 C 或者 C++ 代码来处理这些关键部分,而现在我们多了 Swift 这个选项。相比起 C 或者 C++,Swift 的语言特性上要先进得多,而使用 Swift 类型和标准库进行编码和构建的难度,比起使用 C 或 C++ 来要简单太多。另外,即使不是性能关键部分,我们也应该尽量考虑在没有必要时减少使用 NSObject 和它的子类。如果没有动态特性的需求的话,保持在 Swift 基本类型中会让我们得到更多的性能提升。