WWDC Quick Look 💓 By SwiftGGTeam
Link fast: Improve build and launch times

Link fast: Improve build and launch times

观看原视频

Highlight

Apple 将 ld64 静态链接器速度提升了一倍,并在 dyld 中引入了 chained fixups 和 page-in linking 技术,让 App 构建更快、启动更快、体积更小。

核心内容

静态链接:从编译到可执行文件

你写的代码需要和其他库一起才能运行。静态链接器(ld)负责把多个 .o 文件和静态库合并成一个可执行文件。静态库(.a)本质上是用 ar 打包的 .o 文件集合,链接器只加载能解析未定义符号的 .o 文件。

这个选择性加载机制很巧妙,但也带来问题:链接器必须按固定顺序串行处理静态库,无法充分利用多核。

01:57

ld64 今年快了一倍

Apple 的静态链接器 ld64 在今年进行了大幅优化。通过并行化内容拷贝、LINKEDIT 构建、UUID 计算和代码签名哈希,链接速度提升了一倍。

07:06

动态链接:启动时的隐形成本

动态库(dylib)在构建时只记录符号引用和库路径,不复制代码。这减小了 App 体积,但把成本推迟到了启动时:dyld 需要在启动时加载、映射、解析符号、执行 fixup。

15:47

Chained fixups:更小的二进制,更快的启动

今年 Apple 引入了 chained fixups 格式。传统 fixup 需要在 LINKEDIT 中记录每个 fixup 位置,而 chained fixups 只在 LINKEDIT 中记录每页的第一个 fixup 位置,其余信息编码在 DATA 段本身的指针中。这大幅减小了 LINKEDIT 体积。

22:37

Page-in linking:内核帮你做 fixup

配合 chained fixups,dyld 引入了 page-in linking。启动时不再一次性对所有 dylib 执行 fixup,而是由内核在首次访问 DATA 页时惰性应用 fixup。这减少了启动时间和脏内存,DATA_CONST 页变为 clean page,可以被回收和重建。

25:07

详细内容

静态库的选择性加载机制

// main.c
int main() {
    foo();
    return 0;
}

// foo.c
void foo() {
    bar();
}

// bar.c
void bar() {
    // implementation
}
void unused_func() {
    // never called
}

// baz.c
void baz() {
    undef(); // undefined symbol
}

编译后得到 main.o、foo.o、bar.o、baz.o。将 bar.o 和 baz.o 打包成静态库 lib.a。

链接命令:

ld main.o foo.o -l.a -o app

关键点:

  • 链接器按命令行顺序处理文件
  • 先加载 main.o,发现 main 定义和 foo 未定义
  • 再加载 foo.o,解析 foo,但新增 bar 未定义
  • 检查静态库 lib.a,发现 bar.o 定义了 bar,加载 bar.o
  • baz.o 虽然也在 lib.a 中,但没有未定义符号需要它,所以不被加载
  • 这就是静态库的选择性加载,未使用的 .o 文件不会进入最终程序

加速静态链接的四个选项

1. -all_load:并行加载所有静态库内容

# Other Linker Flags
-all_load

适用场景:App 最终会加载静态库中的大部分内容。此选项让链接器并行解析所有静态库,绕过串行限制。但如果多个静态库实现了相同符号,会导致冲突。

2. -dead_strip:移除未使用代码

# Build Settings -> Dead Code Stripping -> YES
# 或在 Other Linker Flags 中添加
-dead_strip

配合 -all_load 使用,补偿因加载全部内容而增加的体积。链接器会移除从 main 不可达的代码和数据。

3. -no_exported_symbols:跳过导出符号表构建

# Other Linker Flags
-no_exported_symbols

主 App 通常不需要导出符号。跳过 exports trie 构建可节省数秒链接时间。先用以下命令检查导出符号数量:

dyld_info -exports /path/to/YourApp

不适用场景:App 加载了链接回主可执行文件的插件,或用 XCTest 以 App 为 host 运行测试。

4. -no_deduplicate:Debug 构建跳过去重

# Other Linker Flags
-no_deduplicate

C++ 模板展开会产生大量相同指令的函数,链接器会去重以减小体积。但去重算法很耗时。Xcode 14 默认在 Debug 配置中传递此选项。如果你使用自定义构建系统,确保 Debug 构建添加此选项。

动态链接的 fixup 机制

App TEXT 段:
  call _malloc    →  实际变成:
  
  // stub 代码(TEXT 段)
  ldr x16, [DATA, #malloc_ptr]
  br x16

App DATA 段:
  malloc_ptr: 0x0  ← dyld 启动时填入 libSystem 中 malloc 的实际地址

关键点:

  • TEXT 段不可变(代码签名要求)
  • 调用外部 dylib 函数时,链接器生成 stub 代码
  • stub 从 DATA 段加载函数指针并跳转
  • dyld 启动时只修改 DATA 段,完成符号绑定
  • 所有 fixup 本质上都是 dyld 在 DATA 段设置指针

Chained fixups 格式

传统 fixup:LINKEDIT 记录每个 fixup 的地址位置。

Chained fixups:

  • LINKEDIT 只记录每页 DATA 的第一个 fixup 位置
  • 后续 fixup 通过指针中的偏移位”链式”链接
  • 指针中 packed 了:是否是 bind、下一个 fixup 的偏移、符号索引或内部偏移

要求:

  • 部署目标 iOS 13.4+
  • Xcode 14 自动为符合条件的 target 生成

Page-in linking 工作流程

启动流程(优化前):
1. 加载主可执行文件
2. 递归加载依赖 dylib
3. 解析所有 bind 符号
4. 对所有 dylib 执行 fixup  ← 耗时
5. 运行 initializers

启动流程(优化后):
1. 加载主可执行文件
2. 递归加载依赖 dylib
3. 解析所有 bind 符号
4. fixup 由内核在 page-in 时惰性执行  ← 省时间
5. 运行 initializers

关键点:

  • 首次启动时 dyld 缓存绿色步骤的工作
  • 后续启动复用缓存
  • page-in linking 进一步优化 fixup 步骤
  • 仅适用于启动时加载的 dylib,dlopen 的 dylib 仍由 dyld 传统处理

诊断工具

dyld_usage:追踪 dyld 启动行为

dyld_usage -t /Applications/TextEdit.app/Contents/MacOS/TextEdit

输出示例:

launch time: 15ms
fixups: 1ms   ← page-in linking 的贡献
static initializers: 12ms  ← 现在是大头

dyld_info:检查二进制文件

# 查看 fixup 信息
dyld_info -fixups /path/to/binary

# 查看导出符号
dyld_info -exports /System/Library/Frameworks/Foundation.framework/Foundation

# 查看 dyld cache 中的库
dyld_info -exports Foundation

核心启发

  • 评估静态库 vs 动态库的平衡点:ld64 今年快了一倍,你可以承受更多静态库或直接编译进 App 的源文件,减少 dylib 数量以降低启动成本。用 dyld_usage 测量启动时间,找到适合你项目的平衡点。

  • Debug 构建启用 -no_deduplicate:如果你使用 C++ 或自定义构建系统,确保 Debug 配置添加 -no_deduplicate,可显著缩短链接时间而不影响 Release 体积优化。

  • 检查并启用 chained fixups:将部署目标提升到 iOS 13.4+,Xcode 14 会自动生成 chained fixups 格式。用 dyld_info -fixups 验证你的二进制是否已使用新格式,在 iOS 16 上获得 page-in linking 的启动加速。

  • 用 -no_exported_symbols 加速大型 App 链接:对主 App target 尝试添加 -no_exported_symbols,先用 dyld_info -exports 确认导出符号数量。如果数量很大(如数十万),此选项可节省数秒链接时间。

  • 减少静态初始化器中的工作dyld_usage 会显示 static initializers 的耗时。检查代码中全局变量的初始化逻辑,避免在 initializer 中做 I/O、网络请求或复杂计算。

关联 Session

评论

GitHub Issues · utterances