返回 登录
0

Swift性能探索和优化分析

作者简介: 王巍(@onevcat),江湖人称“喵神”。iOS/Unity3D开发者,现居日本,就职于LINE。ObjC中国项目发起者,维护VVDocumenter-Xcode及Kingfisher等开源项目,著有《Swifter : 100个Swift 2开发必备Tip》。

Apple在推出Swift时就将其冠以先进、安全和高效的新一代编程语言之名。前两点在Swift的语法和语言特性中已经表现得淋漓尽致,诸如尾随闭包、枚举关联值、可选值和强制的类型安全等,都是Swift显而易见的优点。但对于高效一点,就没有那么明显了。在2014年WWDC大会上,Apple宣称Swift具有超越Objective-C的性能,甚至某些情况下可以媲美和超过C。但在正式发布后,很多开发者发现,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具有良好的内存使用策略和结构。其标准库中绝大部分类型都是struct,对值类型的使用范围之广,在近期的编程语言中可谓首屈一指。直接在栈上进行存储以及值类型的传递,按理说对性能提升作用为负,但是Swift巧妙地规避了不必要的值类型复制,仅只在必要时进行内存分配。这使得Swift在享受不可变性带来的便利以及避免不必要的共享状态的同时,还能保持性能上的优秀。

对性能进行测试

《计算机程序设计艺术》和LaTeX的作者Donald Ervin Knuth(高纳德)曾经在论文中说过:

过早的优化是万恶之源。

和很多人理解的不同,这并不是说我们不应该在项目的早期就开始进行优化,而是指我们需要弄清代码中性能真正的问题和希望达到的目标后再开始进行优化。因此,我们需要知道性能问题到底出在哪儿。对程序性能的测试一定是优化的第一步。
在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()
}

CACurrentMediaTime和log的方法适合于我们对既有代码进行探索,另一种有效的方法是使用Instruments的Time Profiler来在更高层面寻找代码的性能弱点。将程序挂载到Time Profiler后,每一个方法调用的耗时都将被记录。

当我们寻找到需要进行优化的代码路径后,为其建立一个单元测试来持续地检测代码的性能是很好的做法。Xcode中默认的测试框架XCTest提供了检测并汇报性能的方法:measureBlock。通过将测试的代码块放到measureBlock中,Xcode在测试时就会多次运行这段代码,并统计平均耗时。更方便的是,你可以设定一个基准,Xcode会记录每次的耗时并在性能没有达到预期时进行提醒。这保证了随着项目开发,关键的代码路径不会发生性能上的退化(如图1所示)。

func testPerformance() {
    measureBlock() {
        // 需要性能测试的代码
    }
}

图片描述

图1 Test Measure

优化手段,常见误用及对策

  • 多线程、算法及数据结构优化

在确定了需要改善性能的代码后,一个最根本的优化方式是在程序设计层面进行改良。在移动客户端,对于影响了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类型

为了和Objective-C协同工作,很多Swift标准库类型和对应的Cocoa类型是可以隐式的类型转换的,比如Swift.Array与NSArray、Swift.String和NSString等。虽然我们不需要在语言层面做类型转换,但是这个过程却不是免费的。在转换次数很多的时候,这往往会成为性能的瓶颈。一个常见的Swift和Objective-C混用的例子是JSON解析。考虑以下代码:

let jsonData: NSData = //...
let jsonObject = try? NSJSONSerialization
        .JSONObjectWithData(jsonData, options: []) as? [String: AnyObject]

这是我们日常开发中很常见的代码,使用NSJSONSerialization将数据转换为JSON对象后,得到的是一个NSObject对象。在Swift中使用时,我们一般会先将其转换为[String: AnyObject],这个转换在一次性处理成千上万条JSON数据时会带来严重的性能退化。Swift 3中我们可能可以基于Swift的Foundation框架来解决这个问题,但是现在,如果存在这样的情况,一种处理方式是避免使用Swift的字典类型,而使用NSDictionary。另外,适当地使用lazy加载的方法,也是避免一次性进行过多的类型转换的好思路。

尽可能避免混合地使用Swift类型和NSObject子类,会对性能的提高有所帮助。

  • 避免无意义的log,保持好的编码习惯

在调试程序时,很多开发者喜欢用输出log的方式对代码的运行进行追踪,帮助理解。Swift编译器并不会帮我们将print或者debugPrint删去,在最终App中它们会把内容输出到终端,造成性能的损失。当然,我们可以在发布时用查找的方式将所有这些log输出语句删除或者注释掉,但是更好的方法是通过添加条件编译来将这些语句排除在Release版本外。在Xcode的Build Setting中,在Other Swift flags的Debug栏中添加-D DEBUG即可加入一个编译标识(如图2所示)。

图片描述

图2 Debug Flag

之后我们就可以通过将print或者debugPrint包装一下:

func dPrint(item: Any) {
    #if DEBUG
    print(item)
    #endif
}

这样,在Release版本中,dPrint将会是一个空方法,所有对这个方法的调用都会被编译器剔除掉。需要注意的是,在这种封装下,如果你传入的items是一个表达式而不是直接的变量的话,这个表达式还是会被先执行求值的。如果这对性能也产生了可测的影响的话,最好用@autoclosure修饰参数来重新包装print。这可以将求值运行推迟到方法内部,这样在Release时这个求值也会被一并去掉:

func dPrint(@autoclosure item: () -> Any) {
    #if DEBUG
    print(item())
    #endif
}
dPrint(resultFromHeavyWork())
// Release 版本中 resultFromHeavyWork() 不会被执行

总结

Swift还是一门很新的语言,并且处于高速发展中。由于现在Swift只用于Cocoa开发,因此它和Cocoa框架还有着千丝万缕的联系。很多时候由于这些原因,我们对于Swift性能的评估并不公正。这门语言本身设计就是以高性能为考量的,而随着Swift的开源和进一步的进化,以及配套框架的全面重写,相信在语言层面上我们能获得更好的性能和编译器的支持。

最好的优化就是不用优化。在软件开发中,保证书写正确简洁的代码,在项目开始阶段就注意可能存在的性能缺陷,将可扩展性的考虑纳入软件构建中,按照实际需求进行优化,不要陷入为了优化而优化的怪圈,这些往往都可以让我们避免额外的优化时间,让我们的工作更加愉快。

参考

本文为《程序员》杂志2月版特约文章,订阅详情可点击:http://dingyue.programmer.com.cn/

第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。

图片描述

2016年3月18日-19日,由CSDN重磅打造的数据库核心技术与实战应用峰会、互联网应用架构实战峰会将在上海举行。这两场峰会将邀请业内顶尖的架构师和技术专家,共同探讨高可用/高并发系统架构设计、新技术应用、移动应用架构、微服务、智能硬件架构、云数据库实战、新一代数据库平台、产品选型、性能调优、大数据应用实战等领域的热点话题与技术。

评论