Swift 性能探索和优化分析
Apple 在推出 Swift 时就将其冠以先进,安全和高效的新一代编程语言之名。前两点在 Swift 的语法和语言特性中已经表现得淋漓尽致:像是尾随闭包,枚举关联值,可选值和强制的类型安全等都是 Swift 显而易见的优点。但是对于高效一点,就没有那么明显了。在 2014 年 WWDC 大会上 Apple 宣称 Swift 具有超越 Objective-C 的性能,甚至某些情况下可以媲美和超过 C。但是在 Swift 正式发布后,很多开发者发现似乎 Swift 性能并没有像宣传的那样优秀。甚至在 Swift 经过了一年半的演进的今天,稍有不慎就容易掉进语言性能的陷阱中。本文将分析一些使用 Swift 进行 iOS/OS X 开发时性能上的考量和做法,同时,笔者结合自己这一年多来使用 Swift 进行开发的经验,也给出了一些对应办法。
为什么 Swift 的性能值得期待
Swift 具有一门高效语言所需要具备的绝大部分特点。与 Ruby 或者 Python 这样的解释型语言不需要再做什么对比了,相较于其前辈的 Objective-C,Swift 在编译期间就完成了方法的绑定,因此方法调用上不再是类似于 Smalltalk 的消息发送,而是直接获取方法地址并进行调用。虽然 Objective-C 对运行时查找方法的过程进行了缓存和大量的优化,但是不可否认 Swift 的调用方式会更加迅速和高效。
另外,与 Objective-C 不同,Swift 是一门强类型的语言,这意味 Swift 的运行时和代码编译期间的类型是一致的,这样编译器可以得到足够的信息来在生成中间码和机器码时进行优化。虽然都使用 LLVM 工具链进行编译,但是 Swift 的编译过程相比于 Objective-C 要多一个环节 -- 生成 Swift 中间代码 (Swift Intermediate Language,SIL)。SIL 中包含有很多根据类型假定的转换,这为之后进一步在更低层级优化提供了良好的基础,分析 SIL 也是我们探索 Swift 性能的有效方法。
最后,Swift 具有良好的内存使用的策略和结构。Swift 标准库中绝大部分类型都是 struct,对值类型的使用范围之广,在近期的编程语言中可谓首屈一指。原本值类型不可变性的特点,往往导致对于值的使用和修改意味着创建新的对象,但是 Swift 巧妙地规避了不必要的值类型复制,而仅只在必要时进行内存分配。这使得 Swift 在享受不可变性带来的便利以及避免不必要的共享状态的同时,还能够保持性能上的优秀
对性能进行测试
这输入标题《计算机程序设计艺术》和 TeX 的作者高德纳曾经在论文中说过:
过早的优化是万恶之源。
在 Cocoa 开发中,对于性能的测试有几种常见的方式。其中最简单是直接通过输出 log 来监测某一段程序运行所消耗的时间。在 Cocoa 中我们可以使用
CACurrentMediaTime
来获取精确的时间。这个方法将会调用 mach 底层的mach_absolute_time()
,它的返回是一个基于 Mach absolute time unit 的数字,我们通过在方法调用前后分别获取两次时刻,并计算它们的间隔,就可以了解方法的执行时间:
let start = CACurrentMediaTime() // ...
let end = CACurrentMediaTime()
print("测量时间:\(end - start)")
为了方便使用,我们还可以将这段代码封装到一个方法中,这样我们就能在项目中需要测试性能的地方方便地使用它了:
func measure(f: ()->()) {
let start = CACurrentMediaTime()
f()
let end = CACurrentMediaTime()
print("测量时间:\(end - start)")
}
measure {
doSomeHeavyWork()
}
优化手段,常见误用及对策
多线程、算法及数据结构优化
在确定了需要进行性能改善的代码后,一个最根本的优化方式是在程序设计层面进行改良。在移动客户端,对于影响了 UI 流畅度的代码,我们可以将其放到后台线程进行运行。Grand Central Dispatch (GCD) 或者 NSOperation
可以让我们方便地在不同线程中切换,而不太需要去担心线程调度的问题。一个使用 GCD 将繁重工作放到后台线程,然后在完成后回到主线程操作 UI 的典型例子是这样的:
let queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)
dispatch_async(queue) {
// 运行时间较长的代码,放到后台线程运行
dispatch_async(dispatch_get_main_queue()) {
// 结束后返回主线程操作 UI
}
}
将工作放到其他线程虽然可以避免主线程阻塞,但它并不能减少这些代码实际的执行时间。进一步地,我们可以考虑改进算法和使用的数据结构来提高效率。根据实际项目中遇到的问题的不同,我们会有不同的解决方式,在这篇文章中,我们难以覆盖和深入去分析各种情况,所以这里我们只会提及一些共通的原则。
对于重复的工作,合理地利用缓存的方式可以极大提高效率,这是在优化时可以优先考虑的方式。Cocoa 开发中 NSCache
是专门用来管理缓存的一个类,合理地使用和配置 NSCache
把开发者中从管理缓存存储和失效的工作中解放出来。关于 NSCache
的详细使用方法,可以参看 NSHipster 关于这方面的文章以及 Apple 的相关文档。
在程序开发时,数据结构使用上的选择也是重要的一环。Swift 标准库提供了一些很基本的数据结构,比如 Array
、Dictionary
和 Set
等。这些数据结构都是配合泛型的,在保证数据类型安全的同时,一般来说也能为我们提供足够的性能。关于这些数据的容器类型方法所对应的复杂度,Apple 都在标准库的文档或者注释中进行了标记。如果标准库所提供的类型和方法无法满足性能上的要求,或者没有符合业务需求的数据结构的话,那么考虑使用自己实现的数据结构也是可选项。
如果项目中有很多数学计算方面的工作导致了效率问题的话,考虑并行计算能极大改善程序性能。iOS 和 OS X 都有针对数学或者图形计算等数字信号处理方面进行了专门优化的框架:Accelerate.framework,利用相关的 API,我们可以轻松快速地完成很多经典的数字或者图像处理问题。因为这个框架只提供一组 C API,所以在 Swift 中直接使用会有一定困难。如果你的项目中要处理的计算相对简单的话,也可以使用 Surge,它是一个基于 Accelerate 框架的 Swift 项目,让我们能在代码里从并行计算中获得难以置信的性能提升。
最编译器优化
Swift 编译器十分智能,它能在编译期间帮助我们移除不需要的代码,或者将某些方法进行内联 (inline) 处理。编译器优化的强度可以在编译时通过参数进行控制,Xcode 工程默认情况下有 Debug 和 Release 两种编译配置,在 Debug 模式下,LLVM Code Generation 和 Swift Code Generation 都不开启优化,这能保证编译速度。而在 Release 模式下,LLVM 默认使用 "Fastest, Smallest [-Os]",Swift Compiler 默认使用 "Fast [-O]",作为优化级别。我们另外还有几个额外的优化级别可以选择,优化级别越高,编译器对于源码的改动幅度和开启的优化力度也就越大,同时编译期间消耗的时间也就越多。虽然绝大部分情况下没有问题,但是仍然需要当心的是,一些优化等级采用的是激进的优化策略,而禁用了一些检查。这可能在源码很复杂的情况下导致潜在的错误。如果你使用了很高的优化级别,请再三测试 Release 和 Debug 条件下程序运行的逻辑,以防止编译器优化所带来的问题。
值得一提的是,Swift 编译器有一个很有用的优化等级:"Fast, Whole Module Optimization",也即 -O -whole-module-optimization。在这个优化等级下,Swift 编译器将会同时考虑整个 module 中所有源码的情况,并将那些没有被继承和重载的类型和方法标记为 final,这将尽可能地避免动态派发的调用,或者甚至将方法进行内联处理以加速运行。开启这个额外的优化将会大幅增加编译时间,所以应该只在应用要发布的时候打开这个选项。
虽然现在编译器在进行优化的时候已经足够智能了,但是在面对编写得非常复杂的情况时,很多本应实施的优化可能失效。因此保持代码的整洁、干净和简单,可以让编译器优化良好工作,以得到高效的机器码。
Swift 还是一门很新的语言,并且处于高速发展中。因为现在 Swift 只用于 Cocoa 开发,因此它和 Cocoa 框架还有着千丝万缕的联系。很多时候由于这些原因,我们对于 Swift 性能的评估并不公正。这门语言本身设计就是以高性能为考量的,而随着 Swift 的开源和进一步的进化,以及配套框架的全面重写,相信在语言层面上我们能获得更好的性能和编译器的支持。
最好的优化就是不用优化。在软件开发中,保证书写正确简洁的代码,在项目开始阶段就注意可能存在的性能缺陷,将可扩展性的考虑纳入软件构建中,按照实际需求进行优化,不要陷入为了优化而优化的怪圈,这些往往都可以让我们避免额外的优化时间,让我们的工作得更加愉快
未来CTO
关注我CTO之路从此开始
微信号:wlaicto