Xcode 9.3 新增能力,优化 Swift 编译生成代码的尺寸

2,096 阅读6分钟
原文链接: mp.weixin.qq.com

在 Swift 4.1 的编译器中,提供了一个新的优化选项。可以减少代码生成尺寸。 

对生成的代码进行优化,是很多编译器都提供的功能,Swift 之前版本的编译器其实也提供了优化功能,如果你打开 XCode 项目的 Build Settings, 就可以找到一个叫做 Optimization Level 的选项,默认情况下,XCode 会对 Release 下的项目默认开启编译器优化:

这个选项在实际编译的时候,会给编译器传递一个 -O 参数,它就代表进行性能优化。 

新增的编译选项

XCode 9.3,除了上述的性能优化之外,还提供了另外一个新的选项 -Osize, 对代码尺寸进行优化:

从上图中可以看到 -O 和 -Osize 两个参数是互斥的,只能选一个。 -O 前面我们说过了,是对代码的执行速度进行优化,但执行速度提升了,就会牺牲一部分代码空间。

反之 -Osize 是专门为节省代码空间这个目的而来的优化选项。 那么他们分别能带来多少收益和损失呢? 这点在 swift.org 的官方 Blog 也有介绍:

-Osize 根据项目不同,大致可以优化掉 5% - 30% 的代码空间占用。 相比 -0 来说,会损失大概 5% 的运行时性能。 如果你的项目对运行速度不是特别敏感,并且可以接受轻微的性能损失,那么  -Osize 就值得一用。 

Single File 和 Whole Module

除了  -O 和 -Osize, 还有另外一个概念也值得说一下。 就是 Single File 和  Whole Module 。 在之前的 XCode 版本,这两个选项和 -O 是连在一起设置的,Xcode 9.3 中,将他们分离出来,可以独立设置:

Single File 和 Whole Module 这两个模式分别对应编译器以什么方式处理优化操作。 

Single File 是逐个文件进行优化,它的好处是对于增量编译的项目来说,它可以减少编译时间,对没有更改的源文件,不用每次都重新编译。并且可以充分利用多核 CPU,并行优化多个文件,提高编译速度。

但它的缺点就是对于一些需要跨文件的优化操作,它没办法处理。如果某个文件被多次引用,那么对这些引用方文件进行优化的时候,会反复的重新处理这个被引用的文件,如果你项目中类似的交叉引用比较多,就会影响性能。

Whole Module 则是将项目所有的文件看做一个整体,不会产生 Single File 模式对同一个文件反复处理的问题,并且可以进行最大限度的优化,包括跨文件的优化操作。

缺点是,不能充分利用多核处理器的性能,并且对于增量编译,每次也都需要重新编译整个项目。

XCode 的默认设置使用的是 Whole Module 模式。 我特意搜索了一下,在 Stack Overflow 上面找到了一个比较好的总结,也给大家贴出来参考一下:

这篇帖子的地址是 https://stackoverflow.com/questions/47998583/whats-the-difference-between-single-file-optimization-vs-whole-module-optimizat。

对于这个选项,我的理解是,如果没有特殊情况,使用默认的 Whole Module 优化即可。 它会牺牲部分编译性能,但的优化结果是最好的。

何为编译优化

关于编译优化,这话话题其实不小。 简单来说,现在我们用高级语言写出的代码,更大程度上是基于我们人的思维逻辑。 然后通过编译器,变成机器的逻辑。比如现在如果大家开发一个项目,更关注业务逻辑的实现,比如点击购买按钮,能不能正常调用下订单的函数。或者当用户完成某个功能,能不能按照预定的要求弹出评价提示等。

我们现在越来越少的会为怎么写一行代码能够减少电量消耗,或者如何提高多核 CPU 的利用率这类的问题花费精力。编译优化就是在一定程度上帮助我们处理这类问题的功能。

用 swift.org 中一篇 Blog 来举个例子:

struct X {    var x: Int { return 27 }}

比如上面这个代码,定义了一个属性 x, 它通过一个函数返回一个整数 27。 如果你开启了编译优化,编译器就有可能将这个属性优化为 inline 函数(因为这个函数体相对简单)。所谓 inline 函数,就是在调用它的地方直接把它展开成代码。比如这样:

let ins = X()print (ins.x)

上述代码实际在编译优化后,就成了这样:

let ins = X()print (27)

那么这样替换能带来什么好处呢,因为在程序真正执行的时候,函数调用的开销要比直接执行某段代码大很多,所以将一些比较小的函数直接优化成 inline 的,就肯定会提高程序运行的效率了。

这就是编译优化的一个例子,上面说的 inline 替换是对性能进行优化,可想而知如果你的代码中多次调用了 ins.x 这个属性,那么他们就都会被替换,我们这个例子中这个函数体还比较简单,如果函数体稍微复杂一些,你的代码总量必然会被编译优化增大。 过多的 inline 虽然会对性能提升有帮助,但无疑会增大代码的尺寸。

这也是程序设计中守恒的一个定律,同样的条件下,空间和性能不可能兼得,需要取舍。

相信通过这个解释,大家应该更能理解 -O 和 -Osize 的区别了。 以官方 Blog 的解释,-O 更着重于优化性能,同时会带来代码空间的增大。  -Osize 着重于代码尺寸,比如官方 Blog 上面就有一点明确的说明,-Osize 对于 inline 函数的优化标准就比 -O 谨慎很多。 从这个角度看, inline 优化少了,代码尺寸自然会变小,同样的运行性能就会稍微降低。

当然,上面只是通过 inline 优化给大家举个例子,目的是帮助大家更好的理解编译优化的运作原理。 实际的编译优化操作,要远比我们这里描述的复杂。

关于 -Osize  的更多介绍,大家也可以看一下 swift.org 这篇 Blog:https://swift.org/blog/osize

总结

XCode 9.3 新增的 -Osize 编译选项,给大家提供了一个新的选择。 这篇文章也通过对它的介绍,给大家分享了关于编译优化知识的一些基本概念,也许会帮你在讨论问题的时候多一些谈资。同样,现在其实有不少项目对空间尺寸的优化需求在增多,我想这也是  -Osize 这个新的编译选项提供出来的原因之一吧。 如果你的项目恰好也有这方面的需求,不妨可以试一下它。

最好,劳烦各位几秒钟,做一个小调查,什么时候你看文章最方便,按照这个调整文章推送时间。