本文是作者在腾讯实习时的文章,分享给大家。
1. Link-Time Optimization 原理介绍
Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码;这个优化也是高效的,因为可以在全局视角下优化代码。
Xcode 目前使用的是 ld 链接器,链接器自带了 ThinLTO 框架可以进行 Link-Time Optimization。苹果在 WWDC 2016 中,明确提出了这个优化的概念,What’s New in LLVM。并且说在苹果内部已经广泛地使用这个优化方法进行编译。
它的优化主要体现在如下几个方面:(后面的章节有例子,建议结合例子阅读)
-
多余代码去除(Dead code elimination):如果一段代码分布在多个文件中,但是从来没有被使用,普通的
-O3优化方法不能发现跨中间代码文件的多余代码,因此是一个“局部优化”。但是 Link-Time Optimization 技术可以在 link 时发现跨中间代码文件的多余代码; -
跨过程优化(Interprocedural analysis and optimization):这是一个相对广泛的概念。举个例子来说,如果一个 if 方法的某个分支永不可能执行,那么在最后生成的二进制文件中就不应该有这个分支的代码;
-
内联优化(Inlining optimization):内联优化形象来说,就是在汇编中不使用 “
call func_name” 语句,直接将外部方法内的语句“复制”到调用者的代码段内。这样做的好处是不用进行调用函数前的压栈、调用函数后的出栈操作,提高运行效率与栈空间利用率。
2. 用例子展现优化效果
使用 Link-Time Optimization 的方法是在 Xcode 的 build settings 选项中开启。
以下的优化均使用 -O3 的优化
2.1 多余代码去除(Dead code elimination)
这个例子主要由3个文件组成:
func1.m(用来定义func1)
func2.m(用来定义func2)
main.m(主函数)
编译后的汇编代码(未启用优化):
在以下代码中,我们可以明显地发现在 main 函数中,进行了 func1 的调用,可是 func1 并没有任何作用,属于需要去除的 “Dead code”。
编译后的汇编代码(启用优化):
启用优化后,我们可以看到,call func1 的代码已经被移除。同时,还对 func1 和 func2 进行了一些其他的优化。
2.2 跨过程和内联优化
这个例子主要由 3 个文件组成
funcs.h:声明了 extern 的函数名
funcs.m:函数的具体实现
main.m:主函数入口
编译后的汇编代码(未启用优化):
从下面的代码中,我们可以看出,func1 和 func4 是分布在不同的函数中的。
编译后的汇编代码(启用优化):
从下面的代码中,我们可以看出,func1 和 func4 是一起被“复制”到了 main 函数中。同时, push 和 pop 这类的压栈、出栈代码也都消失了,因为并没有出现类似“ call func_name”的函数调用。
2.3 总结
从上面的例子可以看出,开启这个优化后,一方面减少了汇编代码的体积,一方面提高了代码的运行效率。
3. QQ 音乐优化实例
在 QQ 音乐 iOS 版中,开启这项优化将二进制文件的体积从 130 MB 缩小至约 125 MB,缩减了约 4%。运行效率因为受网络等因素的影响,暂时无法具体测量。
从具体的文件来看,
优化前:
优化后:
从代码行数上来看,汇编代码行数几乎保持不变,说明并没有很多 “Dead code”。
4. 进一步拓展
前面在 build settings 中,我们设置了 Monilithic 的优化方式,这种方式并不支持多线程和增量链接。因此,在新的版本中,苹果使用了新的优化方式 Incremental,大大减少了链接的时间。建议开启。
小插曲:在开启
Incremental的选项后,QQ音乐项目出现了“duplicate symbols”的错误,经查是之前代码不规范引起的。全局变量在定义时,必须使用static关键字或者单独在.m文件中定义,.h文件中只能声明变量,而不应该定义变量。
推荐阅读
剖析 ARM 64 架构中的 objc_msgSend 再谈 __bridge, __bridge_transfer, __bridge_retained