WWDC22 之 更快的静态链接与动态链接

Tags
iOS
Apple
Date
Jun 30, 2022
 
原标题:Link fast: Improve build and launch times
官方翻译:快速链接:缩短构建和启动时间
 
这篇尽可能图文并茂且简明扼要地介绍视频中的每一页 PPT,涉及的内容比较多。

前言

 
什么是链接器?当你的源码用到别人写好打包的 library 的时候,就需要链接器。
 
notion image
 
有两种类型。
静态链接:影响构建耗时和包大小。
动态链接:影响启动耗时。
 
notion image
 

静态链接

 
以前一开始只有一个源文件,一个编译器就能生成可执行程序。
 
notion image
 
后来,为了不在一个大文件里编辑,更是为了不重复编译,将编译器(cc)和链接器(ld)分开。编译器将源码编译为 .o 文件(称为 relocatable object,可重定向目标文件),链接器将 .o 文件链接生成可执行程序。这就是静态链接。
 
notion image
 
再后来,为了方便共享代码,将一堆 .o 文件打包在一起,于是有了打包工具 ar ,打包为 .a 文件 (称为 archive file 或 library)。
 
notion image
 
libc.a 是共享的一套 C 标准库代码,这里有个问题是,每个程序链接了整个 libc.a 之后,体积会非常大。于是有了优化,每个程序只从 libc.a 中链接个别必需的 .o 文件,来解决 undefined symbol 的问题。
 
这种静态链接的选择性加载(selective loading)沿用至今,举个例子。
 
notion image
 
其中 bar.c 和 baz.c 打包成 libbarbaz.a
 
notion image
 
根据符号定义,从 main 开始,按链接器的命令行顺序,将相应的 .o 逐个加到 symbol table 中。注意 bar.o 已被加载(包括 unused 函数),但 baz.o 没有被加载。
 
notion image
 
然后分配地址
 
notion image
 
将所有符号复制到目标应用程序中。
 
notion image

ld64 优化

 
ld64 是苹果的静态链接器,今年快了两倍(对于大多数项目)。
 
notion image
 
具体的优化如下,最主要的应该是利用了多核的并行。这里就不翻译了,因为一般开发者可以不关心。
 
That includes copying content from the input to the output file, building the different parts of LINKEDIT in parallel, and changing the UUID computation and codesigning hashes to be done in parallel. Next, we improved a number of algorithms. Turns out the exports-trie builder works really well if you switch to use C++ string_view objects to represent the string slices of each symbol. We also used the latest crypto libraries which take advantage of hardware acceleration when computing the UUID of a binary, and we improved other algorithms too.
 
notion image

静态链接最佳实践

 
涉及几个话题,以下展开说明。
 
notion image
 

何时使用静态库

 
修改一个源文件,会使得相关的静态库重新链接。由于上文提到的选择性加载,这个链接过程是串行的,会很慢。所以静态库适合不经常修改的稳定代码。
 
notion image
 

-all_load 或 -force_load

 
但如果,你不需要这种选择性加载。那么,你可以使用 -all_load 的链接选项,加载所有静态库的所有 .o 文件 (如果是 -force_load xxx_static_library ,则加载指定静态库的所有 .o 文件),并行执行会更快。
但又如果,你的多个静态库实现了相同的符号,并行加载全部之后会有重复符号定义的错误。而原来的选择性加载,是根据链接顺序,使用第一个发现的符号实现。
 
另外,-all_load 会使得程序包大小增加。可以使用 -dead_strip 的链接选项,移除无法到达的代码。
 
notion image
 

-no_exported_symbols

 
通常来说,动态库需要导出符号,但 App 的主二进制文件不需要导出符号。可以通过 -no_exported_symbols 链接选项来跳过这个导出过程。
 
一百万个导出符号,链接器需要花费两到三秒来完成导出。可以通过以下命令来查看二进制的导出符号数量。文章后面会介绍到 dyld_info 。
 
dyld_info -exports /path/to/binary | wc -l
 
notion image
 

-no_deduplicate

 
几年之前,苹果增加了一个 deduplicate 特性,将相同指令的多个 C++ 函数合并成一个。但这是个 expensive 的算法,链接器需要 hash 每个函数来检查重复,因此算法限制在只查找 weak-def 的符号。
 
考虑到 deduplicate 是优化体积的算法,而 Debug 阶段追求的是更快的链接。所以 Xcode 在 Debug 配置下默认使用了 -no_deduplicate 选项,关闭了这个体积优化。
 
notion image
 

静态库的坑

 
这个 surprise 翻译为坑真的最合适了。
 
有一个坑是,如果一个静态库同时打包到不同的动态库,而这不同的动态库又集成到你的 App 中。那么在运行时,就会发现同一个类名,会有不同的类实例。这就是启动后控制台报的 warning 。
Class ABC is implemented in both xxx and yyy. One of the two will be used. Which one is undefined.
 
notion image
 

动态链接

 
如果只使用静态链接,那么应用程序就会越来越大。
 
notion image
 
如 graphics 库,将静态链接改为动态链接(.dylib),应用程序体积就可以减少。注意 .o 文件集合是通过 ld 链接生成 dylib 的。
 
notion image
 
动态链接只记录了符号的名字以及动态库的运行时路径。
 
notion image
 
对于不同的应用程序(不同进程)使得了同一个动态库,则这个动态库只占用一份内存。
 
notion image
 
动态库的优点:
  1. 减少了包体积
  1. 减少了内存占用
  1. 减少了静态链接耗时
但带来了以下缺点:
  1. 更慢的启动耗时(将链接过程从编译时改为运行时)
  1. 更多的脏内存(每个动态库都有自己的 DATA )
 
notion image
 
dyld 是动态库的链接器
 
notion image
 
dyld 通过 mmap 将应用程序和动态库加载到内存。然后进行符号修正 (fixups) 。
 
notion image
 
举个例子,应用程序中调用了系统动态库中的 malloc 。那么静态链接器就会将调用位置(call site)改为一个在 TEXT 段的 stub ,这个 stub 从 DATA 段中加载一个指针作为地址用于跳转。这样使得,编译期的执行指令是确定的,即 TEXT 段是确定的,运行期 dyld 只需要修改 DATA 段中的指针即可。
 
notion image
 
fixups 有两种。
一种是 rebases,将 Mach-O (动态库或 App )里的内部符号地址加上一个偏移量。这是因为 ASLR(Address Space Layout Randomization,地址空间随机化) 的存在。
 
 
notion image
 
另一种是 binds ,将 Mach-O (动态库或 App)引用到的外部符号修正为内存中的实际地址。
 
notion image
 
iOS 13.4 开始支持 chained fixups。
优点是 LINKEDIT 更小,因为新的格式仅存储了第一个 fixup 的位置以及需要导入的符号。而不是所有 fixup 的位置。
如图所示,链表结构,每个节点存储了下一个 fixup 节点的偏移、有一个 bit 表示是 bind/rebase、以及 target 的信息。如果是 bind,则 target 为这个符号的序号;如果是 rebase,则 target 为这个符号在动态库中的偏移地址。
 
notion image
 
2017年,dyld 从 2 升级到 3,一个重要的特性是启动闭包缓存,将每次启动不会改变的过程缓存起来。
 
notion image
 

dyld 优化

 

page-in linking

 
今年,dyld 新的特性来了,page-in linking,我理解为动态库自动懒加载。
这个 page-in linking 原来已经应用在系统动态库上好多年了,现在可以支持非系统的动态库。
 
优点如下:
  1. 减少脏内存
  1. 减少启动时间
  1. 干净的 DATA_CONST (意味可以回收重建,减少内存压力)
 
这个 page-in linking 是基于 chained fixups 的,所以一样要求动态库的编译链接是 target iOS 13.4 起步,而且需要在 iOS 16 上运行。
 
要注意的是,page-in linking 只应用在启动加载的动态库。如果你自己使用 dlopen() 来实现自己的动态库懒加载,只是在 dlopen() 调用的时候进行 fixups,是不会有 page-in linking 的。
 
notion image
 
具体来说,支持 page-in linking 的动态库,在启动时的 fixups 阶段,并没有真正执行 fixups。而是等到 page-in 的时候才执行。
 
notion image
 

动态链接最佳实践

 
苹果建议:
  1. 使用更少的动态库
  1. 优化或减少静态的初始化代码(在 pre-main 执行)
  1. 鼓励升级使用新的动态库格式(包体积更小,启动耗时更快)
 
notion image
 

新的 dyld 命令行工具

dyld_usage

 
notion image
notion image

dyld_info

 
notion image
notion image
 

Loading Comments...