对 WWDC 2019 Session 409 的一些理解~
主要关注 session 中提到的这几点:
- 新平台的支持
- 底层代码大小优化
- 语言层面的代码大小优化
新平台支持
Watch Series 4 的芯片是 64 位的,而 AppStore 之前所提供的 App 均是 32 位。但神奇的是所有的 App 却能继续运行,这其实是 Bitcode 的作用
什么是 bitcode
开启了 bitcode 的工程在编译过程会输出 source.bc
文件,当我们上传二进制包到 AppStore 时,会帮我们完成后续的编译动作,输出对应平台的二进制包

开发过 Apple Watch Apps 的同学应该知道,如果不勾选 bitcode 那是无法提交到 App Store 的,这也是为什么 64 bit 的 Series 4 能够支持 32 bit 的 Apps
底层代码大小优化
代码的大小会直接影响包大小,Xcode 11 增加了一个新的 optimization level -- -Oz

编译流程
了解 -Oz
如何做优化代码大小之前,我们先梳理一下整个编译流程:

源代码
经过编译输出了 .IR
文件,接下来不是直接生成 .bc
文件而是先生成了 MIR
文件 (Machine IR)。LLVM 会在 .MIR
上进行代码大小优化 -- Function Outlining
Function Outlining
这是在编译后期做的优化操作,它实际上跟语言本身无关,这里我们用汇编代码来做解释:

hasse
跟 kakutani
拥有两段相同的代码片段,此时 LLVM 会把这些片段抽离成一个新的函数 -- OUTLINED_FUNCTION_0
,接着将 OUTLINED_FUNCTION_0
替换到原来的代码处:
上面的例子会替换成一条单独的汇编调用指令 --
b OUTLINED_FUNCTION_0
,这样的结果很明显,一下子减少了几行汇编代码,从而缩减了代码体积(是否也同时减少了设备耗电?)苹果给出的预期优化收益是:

同时像 ARM
prologue
以及 epilogue
这类编译器产生的代码也会被 Function outlining 优化掉:
毫无疑问,这一系列的优化都是有代价的~
代码控制流变化
假设我们对以下 collatz
函数执行优化操作:

此时整个代码控制流发生了改变:

如果此时
collatz
函数 crash 了,那么当前的 crash 堆栈会发生改变:
这相当于降低了代码的 可调试性
代码的执行时间增加
将许多代码片段抽离成单独函数,再执行调用,这一点毫无疑问增加了执行时间。
当我们添加了 -Oz
之后,编译器只会关心 代码大小 这一指标,而性能相关会被忽略。
对于 性能要求高的代码在使用 -Oz
前要三思 !!!
Xcode 目前提供了的几个优化 level 都有不同的考虑:

-
-O3
-O2
可以让代码的执行时间变少,但结果就是代码大小增加,典型的空间换时间。 -
-Oz
-Os
则是牺牲时间,减少了代码大小
对于 Xcode 来说默认的优化等级是 -Os
使用
除了在 Xcode 中的 optimization level 中进行设置,我们还可以做更细颗粒度的操作:

如何验证包大小改变
Mac 自带了一个查看二进制文件的命令行工具 size
,它可以查看具体文件中 Mach-O 各个 Section 的大小:

语言层面的代码大小优化
除了底层代码,上层的代码也有大小相关的优化
合并 Objective-C 中冗余的 block 代码
假设有这样两段代码都调用了 fuseWithCallbackBlock:
,但是 block 内部的代码没有直接联系:

对于 block 内 capture 的内容,底层会生成一个 struct
来记录:

对应产生两个
struct
:
其中高亮部分则是系统优化的地方,
copy_helper
以及 destroy_helper
这两个指针都被指向了系统创建的函数:
这样的优化结果是:

直接继承 NSObject 对象的 ivar 偏移量优化
Objective-C 中会有 ivar layout 的概念,比如存在一个 Card
类,它在内存中的 ivar layout 是这样的:

而在编程领域中我们会提到一个叫 Fragile base class 的问题,当一个基类的成员变量被修改后,所有继承它的子类在没有重新编译之前都将无法使用,C++ 就存在这个问题
而对于
NSObject
来说,Apple 称它是属于 iOS 平台 ABI 的一部分,继承 NSObject
的基类只有在执行 init
方法时才会去获取整个类的 ivar layout,所以继承该基类的子类可以直接得到新的 layout,无需重新编译:

针对直接继承
NSObject
的 ivar layout 可确定性,Xcode 11 针对 property 代码进行了以下优化:
在 Xcode 11 之前,想要访问一个 property 需要先执行寻址,然后放入寄存器,最后访问;而现在对于直接继承
NSObject
的对象,直接通过一条 orr
直接就可以将结果写入目标寄存器,又节省了几行汇编代码,这一系列的操作带来的收益是:

网友评论