大多数浏览器和
Developer App 均支持流媒体播放。
-
快速链接:缩短构建和启动时间
了解如何提升您的 App 的构建和运行时链接性能。我们将带您走进幕后,深入了解链接、选项,以及可优化 App 链接性能的最新更新。
资源
相关视频
WWDC23
WWDC22
-
下载
♪ ♪
Nick Kledzik: 大家好 我是 Nick Kledzik Apple 链接器团队的首席工程师 今天 我想和大家分享一下 如何快速链接 我将向您介绍 Apple 在 链接功能上做的改善 并帮助您了解链接过程 从而帮助您提高 App 的链接性能 那么什么是链接呢 您已经编写了代码 但也使用了其他人 以库或框架的形式编写的代码 为了让您的代码使用这些库 就需要一个链接器 现在 有两种链接类型 一种是“静态链接” 它发生在您创建 App 的时候 这将影响您的 App 开发所需的时间 以及 App 最终的大小 还有一种是“动态链接” 这发生在您启动 App 的时候 这将影响用户等待 App 启动的时间 在本次讲座中 我将讨论静态和动态链接 首先 我将通过一些示例定义 什么是静态链接 以及它的来源 接下来 我将展示Apple 的 静态链接器 ld64 的新功能 然后 以静态链接为背景 我将详细介绍静态链接的最佳实践 本讲座的后半部分将介绍动态链接 我将展示什么是动态链接和它的来源 以及在动态链接期间发生了什么 接下来 我将介绍今年 dyld 的新功能 然后 我将讨论如何提高 App 的 动态链接时间性能 最后 我们将介绍两个新工具 帮助您探索幕后的秘密 您将能够看到二进制文件中的内容 以及动态链接期间发生的情况 为了了解静态链接 我们要回到刚开始的状态 起初 程序很简单 只有一个源文件 构建也很容易 您只需在源文件上运行编译器 它就会生成可执行程序 但将所有源代码放在一个文件中 并不能扩展 如何使用多个源文件进行构建 这不仅仅是因为 您不想编辑一个大文本文件 真正的节省是无需每次构建时 都重新编译每个函数 它们所做的就是将编译器分成两部分 第一部分将源代码编译为 一个新的中间“可重定位对象”文件 第二部分读取可重定位的 .o 文件 并生成可执行程序 我们现在将第二部分称为 “ld” 即静态链接器 现在 您知道静态链接的来源了 随着软件的发展 很快人们就开始传递 .o 文件 但这样做很麻烦 有人在想 如果我们可以将一组 .o 文件打包到一个“库”中 那不是很好吗 当时 将文件捆绑在一起的标准方法 是使用归档工具 “ar” 它的用途是备份和分发 因此 工作流程变成了这样 您可以将多个 .o 文件 “ar” 到 一个存档文件中 并且链接器得到了增强 知道如何直接从存档文件中 读取 .o 文件 对于共享公共代码来说 这是一个很大的改进 当时它只是被称为资料库或档案馆 今天 我们称之为静态库 但现在最终的程序变得越来越大 因为成千上万个 来自这些资料库中的函数 被复制进来 哪怕这些函数中只有少数被使用 所以我们添加了一个巧妙的优化 链接器不会使用静态库中的 所有 .o 文件 仅仅当这么做可以解析一些 未定义的符号时 链接器才会从静态库提取 .o 文件 这意味着有人可以构建一个 很大的 libc.a 静态库 其中包含所有的 C 语言标准库函数 每个程序都可以链接到一个 libc.a 但每个程序只获得程序 实际需要的部分 libc 我们今天仍然有这种模式 但静态库的选择性加载并不明显 这让许多程序员感到困惑 为了使静态库的选择性加载更加清晰 我设置了一个简单的场景 在 main.c 中 有一个名为 main 的 函数 调用一个名为 foo 的函数 在 foo.c 中 有 foo 调用 bar 在 bar.c 中 有 bar 的实现 但也有另一个函数的实现 而这个函数恰好没有使用 最后 在 baz.c 中 有一个函数 baz 它会调用一个名为 undef 的函数 现在 我们将每个 .o 文件编译成 各自的 .o 文件 您会看到 foo bar 和 undef 没有灰框 因为它们是未定义的 也就是说 使用符号而不是定义 现在 假设您决定将 bar.o 和 baz.o 合并成一个静态库 接下来 链接两个 .o 文件 和静态库 让我们来了解一下实际发生的事情 首先 链接器按命令行顺序处理文件 它首先找到的是 main.o 它加载 main.o 并找到 “main” 的定义 如这个符号表所示 但也发现 main 有一个 未定义的 “foo” 然后 链接器解析命令行上的 下一个文件 即 foo.o 该文件添加了“foo”的定义 这意味着 foo 不再是未定义的 但加载 foo.o 也为 “bar” 增加了一个新的未定义符号 现在 命令行上的 所有 .o 文件都已加载 连接器将检查是否还有未定义的符号 在这种情况下 “bar” 仍然是未定义的 所以链接器开始查看命令行上的库 看看库是否满足缺失的 未定义符号 “bar” 链接器发现静态库中的 bar.o 定义了符号 “bar” 因此链接器将 bar.o 从档案中加载出来 此时不再有任何未定义的符号 所以链接器停止处理库 链接器进入下一阶段 为程序中的所有函数和数据 分配地址 然后 它将所有函数和数据 复制到输出文件中 看 您已经将您的程序输出啦 请注意 baz.o 位于静态库中 但没有加载到程序中 因为链接器有选择地从静态库中加载 所以没有加载它 这并不明显 但却是静态库的关键方面 现在您了解了静态链接 和静态库的基础知识 让我们看看 Apple 的 静态链接器 ld64 的 最新改进 根据大众的需求 我们今年花了一些时间优化 ld64 而且今年的链接器 对很多项目速度快了两倍 我们是怎么做到的 现在我们可以更好地利用 开发机器上的核心 我们发现在许多地方可以使用 多核并行地进行链接器工作 这包括将内容从输入复制到输出文件 并行构建 LINKEDIT 的不同部分 以及将 UUID 计算 和代码签名哈希更改为并行进行 接下来 我们改进了一些算法 事实证明 如果您改用 C++ 的 string_view 对象来表示 每个符号的字符串片段 那么导出前缀树构建器的 工作效果非常好 我们还使用了最新的加密库 它在计算二进制 UUID 时 利用了硬件加速 我们还改进了其他算法
在努力提高链接器性能时 我们注意到一些 App 中的配置问题 影响了链接时间 接下来 我将讨论您在项目中可以 做些什么来加速链接时间 我将讨论五个主题 首先 您是否应该使用静态库 然后是三个鲜为人知的选项 它们对链接时间有很大的影响 最后 我将讨论一些可能会让您 感到惊讶的静态链接行为 第一个主题是 如果您正在积极处理 构建到静态库中的源文件 那么您的构建时间就会变慢 因为在编译文件之后 必须重新构建整个静态库 包括其目录 这只是很多额外的 I/O 静态库对于稳定的代码最有意义 也就是说 代码没有被主动更改 您应该考虑将活动开发中的代码 从静态库中移出 以减少构建时间 前面我们展示了从档案中选择性加载 但这样做的缺点是 会减慢连接器的速度 这是因为要使多次构建结果一致 并遵循传统的静态库语义 链接器必须以固定的 连续的顺序处理静态库 这意味着 ld64 的一些并行优势 不能用于静态库 但如果您真的不需要这种历史行为 您可以使用链接器选项来加速构建 该链接器选项称为 -all_load 它命令链接器从所有静态库 盲目加载所有 .o 文件 如果您的 App 将有选择地 从所有静态库中 加载大部分内容 那么这将非常有帮助 使用 -all_load 可以让链接器 并行解析 所有静态库及其内容 但如果您的 App 有多个静态库 实现相同的符号 并且依赖于静态库的命令行顺序 来驱动使用哪个实现 那么这个选项就不适合您 因为链接器将加载所有的实现 而不一定获得在常规静态链接模式中 找到的符号语义 -all_load 的另一个缺点是 它可能会使您的程序变得更大 因为“未使用”的代码现在也添加进来了 为了弥补这一点 可以使用链接器选项 -dead_strip 该选项会使链接器移除 执行不到的代码和数据 现在 死代码剥离算法很快 通常通过 减少输出文件的大小来提高速度 但如果您对使用 -all_Load 和 -Dead_strie 感兴趣 那么您应该打开或关掉这些选项 并为链接器计时 看看它是否适合您的特定情况 下一个链接器选项是 -no_exported_symbols 这里要介绍一个小小的背景知识 链接器生成的 LINKEDIT 段的一部分 是一个导出前缀树 对所有导出的符号名称 地址 和标志进行编码 虽然所有 dylib 都需要导出符号 但主 App 二进制文件通常 不需要任何导出符号 也就是说 通常不会在 主可执行文件中查找符号 如果是这种情况 您可以对 App 目标 使用 -no_exported_symbols 来跳过在 LINKEDIT 中 创建前缀树数据结构 这将缩短链接时间 但如果您的 App 加载了 链接回主可执行文件的插件 或者您用 xctest 与 App 作为 主机环境来运行 xctest 程序包 那么您的 App 就必须将所有内容导出 这意味着您不能使用 -no_exported_symbols 进行配置 只有在导出前缀树很大的情况下 抑制它才有意义 您可以运行此处所示的 dyld_info 命令 来计算导出的符号数量 我们看到的一个大型 App 有大约 100 万个导出符号 链接器花了两到三秒的时间 来为这么多的符号构建导出前缀树 因此 添加 -no_exported_symbols 将该 App 的 链接时间缩短了两到三秒 我将在后面的演讲中告诉您 有关 dyld_info 工具的更多信息 下一个选项是 -no_deduplicate 几年前 我们向链接器 添加了一个新过程 用于合并指令相同但名称不同的函数 事实证明 使用 c++ 模板扩展 能产生很多这样的情况 但这是一个昂贵的算法 链接器必须递归哈希每个函数的指令 以帮助查找重复的指令 因为这样的代价 我们限制了算法 所以链接器只查看 weak-def 符号 这些是 c++ 编译器为未内联的 模板扩展 发出的代码 现在 de-dup 是一种尺寸优化 而 Debug 构建是关于快速构建的 而不是关于尺寸 因此 在默认情况下 Xcode 对 Debug 配置的链接器 通过 -no_deduplicate 来禁用 de-dup 优化 如果您使用 -O0 运行 clang 链接行 那么 clang 也会将 no-dedup 选项传递给链接器 总之 如果您使用 C++ 并有一个自定义的构建 即在 Xcode 中使用非标准配置 或使用其他构建系统 那么您就应确保 Debug 构建添加了 -no_deduplicate 以缩短链接时间 我刚才谈到的选项是 ld 的 实际命令行参数 使用 Xcode 时 您需要更改产品构建设置 在构成设置中 查找 “Other Linker Flags”
您要在这里设置 -all_load 注意 “Dead Code Stripping” 选项 也在这里 那里是 -no_exported_symbols 这里是 -no_deduplicate
现在 让我们谈谈使用静态库时 可能会遇到的一些意料之外的事情 第一个是 当您通过源码构建一个静态库 链接到你的 app ,而代码并没有出现在 最终的 App 中 例如 您给某个函数添加了 __attribute__((used)) 或者您有一个 Objective-C category 由于链接器的选择性加载 如果静态库中的那些目标文件 没有定义链接过程中需要的符号 链接器就不会加载那些目标文件 另一个有趣的交互是 静态库和死代码剥离 死代码剥离导致了 许多静态库问题被隐藏 通常 缺少符号或重复符号 会导致链接器出错 但死代码剥离会导致链接器 从 main 开始 在所有代码和数据上 运行可访问性传递 如果发现丢失的符号 来自执行不到的代码 链接器将抑制丢失的符号错误 同样 如果有来自静态库的重复符号 链接器将选择第一个 而不是错误 使用静态库的最后一个大意外是 当一个静态库被整合到多个框架中时 这些框架中的每一个 都独立运行得很好 但在某个时候 某个 App 同时 使用了这两个框架 问题来了 您会遇到由于多重定义产生的 奇怪的运行时间问题 最常见的情况是 Objective-C 运行时 警告相同类名的多个实例 总体而言 静态库是强大的 但您需要了解它们以避免陷阱 静态链接就到这里 我们现在来讨论一下动态链接 首先 让我们看一下静态链接 与静态库的原始关系图 现在 考虑随着时间的推移 随着源代码越来越多 这将如何扩展 应该清楚的是 随着越来越多的库可用 最终程序的大小可能会增长 这意味着构建该程序的静态链接时间 也将随着时间的推移而增加
现在让我们看看这些库是如何创建的 如果我们做一下转换呢 我们将 “ar” 改为 “ld” 输出库现在是一个 可执行的二进制文件 这是 90 年代动态库的开端 简言之 我们称动态库为 “dylib” 在其他平台上 它们被称为 DSO 或 dll 这到底是怎么回事 这对可扩展性有什么帮助
关键是静态链接器 以不同的方式处理与动态库的链接 链接器只是记录了一种承诺 而不是将代码从库中 复制到最终程序中 也就是说 它记录动态库中 使用的符号名称 以及库在运行时的路径 这有什么优势呢 这意味着您可以控制程序文件大小 它只包含您的代码 以及运行时所需的动态库列表 您的程序中不再有库代码的副本 程序的静态链接时间现在 与代码的大小成正比 而与链接的 dylib 的数量无关 此外 虚拟内存系统 现在可以展现优势了 当看到多个进程使用相同的动态库时 虚拟内存系统将在所有 使用该 dylib 的进程中 为该 dylib 重用相同的物理 RAM 页 我已向您展示了动态库是如何启动的 以及它们解决了什么问题 但是这些“好处”的“代价”是什么呢 首先 使用动态库的一个好处是 我们加快了构建时间 但代价是现在启动 App 的速度变慢了 这是因为启动不再只是 加载一个程序文件 现在 所有 dylib 也需要 加载并连接在一起 换句话说 您只是将部分链接成本 从构建时间推迟到了启动时间 其次 基于动态库的程序 会有更多的脏页面 在静态库的情况下 链接器会将所有 静态库中的所有全局变量放在 主可执行文件的相同 DATA 页中 但使用 dylib 时 每个库都有自己的 DATA 页面 最后 动态链接的另一个代价是 它引入了 对新事物的需求 动态链接器 还记得构建时记录在 可执行文件中的承诺吗 现在 我们需要在运行时 实现加载库的承诺 这就是动态链接器 dyld 的作用 让我们深入了解动态链接 在运行时的工作方式 可执行二进制文件分为多个段 通常至少包括 TEXT DATA 和 LINKEDIT 对于 OS 段总是页面大小的倍数 每段都有不同的权限 例如 TEXT 段具有“执行”权限 这意味着 CPU 可能将页面上的 字节视为机器码指令 在运行时 dyld 必须 使用每个段的权限 将可执行文件 mmap() 映射到内存中 如下所示 因为段是按页面大小和页面对齐的 所以虚拟内存系统只需 将程序或 dylib 文件 设置为 VM 范围的后备存储即可 这意味着在对这些页面进行 某些内存访问之前 不会将任何内容加载到 RAM 中 这会触发页面错误 进而引发 VM 系统 读取文件的适当子范围 并使用其内容填充所需的 RAM 页面 但仅仅映射是不够的 程序需要以某种方式 “连接”或绑定到 dylib 为此 我们有一个概念叫做 “fix ups”
在图中 我们看到程序设置了 指向它使用的 dylib 部分的指针 让我们深入了解一下什么是 fix-ups 这是我们的朋友 mach-o 文件 现在 TEXT 是无法修改的 事实上 在基于代码签名的系统中 它必须无法修改 那么 如果有一个 调用 malloc() 的函数呢 这又将如何运行呢 在构建程序时无法知道 _malloc 的相对地址 现实情况是 静态链接器发现 malloc 在 dylib 中 并转换了调用点 这个调用点变成对一个存根 (stub) 的调用 这个存根由链接器 在同一个 TEXT 段合成 因此在构建时相对地址是已知的 这意味着可以正确地形成 BL 指令 这样做的好处是存根从 DATA 中 加载指针并跳到该位置 现在 运行时不需要对 TEXT 进行任何更改 只需 dyld 更改 DATA 即可 事实上 理解 dyld 的秘诀在于 dyld 所做的所有 fixup 都只是 dyld 在 DATA 中设置指针
让我们深入了解 dyld 所做的 fixup 在 LINKEDIT 的某个地方 dyld 需要 一些信息来驱动 fixup 的进行 有两种 fixup 第一种被称为重定位 (rebase) 当动态库或 App 有一个指向自身的 指针时 它们就被称为重定位 (rebase) 现在有一个叫做 ASLR 的安全特性 它会导致 dyld 在随机地址加载 dylib 这意味着这些内部指针 不能只在构建时设置 相反 dyld 需要在启动时调整 或 “重定位” (rebase) 这些指针 在磁盘上 这些指针包含其目标地址 如同在地址 0 处加载动态库 这样 LINKEDIT 需要记录的 就是每个重定位的位置 然后 dyld 可以将 dylib 的 实际加载地址 添加到每个重定位的位置 以正确地修复它们
第二种 fixup 是绑定 (bind) 绑定是符号引用 也就是说 它们的目标是一个符号名 而不是一个数字 例如 指向函数 “malloc” 的指针 实际上会把字符串 “_malloc” 存储在 LINKEDIT 中 dyld 使用该字符串在 libSystem.dylib 的导出前缀树中 查找 malloc 的实际地址 然后 dyld 将该值存储在 绑定指定的位置 今年 我们宣布了一种新的 fixup 编码方法 我们称之为“链式 fixup”
第一个优势是它使 LINKEDIT 变得更小 LINKEDIT 更小 因为新格式只存储 每个 DATA 页中 第一个 fixup 位置 以及导入符号的列表 而不是存储所有 fixup 位置 然后 其余的信息被编码 在 DATA 段本身中 也就是最终设置 fixup 的地方 这种新格式得名于链式 fixup 因为 fixup 位置是“链接”在一起的 LINKEDIT 只是指出 第一个 fixup 的位置 然后在 DATA 的 64 位指针位置中 一些位包含到下一个 fixup 位置的偏移量 此外 还有一位表示 fixup 是绑定还是重定位 如果是绑定 其余的位就是符号的索引 如果是重定位其余的位是映像中 目标的偏移量 最后 在 iOS13.4 和更高版本中已经 存在对链式 fixup 的运行时支持 这意味着您可以从今天开始 使用这种新格式 只要您的部署目标是 iOS 13.4 或更高版本 链式 fixup 格式支持我们今年 发布的新操作系统功能 但要理解这一点 我需要谈谈 dyld 的工作原理
Dyld 从主可执行文件开始 比如您的 App 解析该 mach-o 以找到 依赖的 dylib 也就是它所需要的动态库 它会找到这些 dylib 并执行 mmap() 然后 对于其中的每一个 它递归并解析它们的 mach-o 结构 根据需要加载任何额外的 dylib 一旦加载了所有内容 dyld 就会查找所需的所有绑定符号 并在完成 fixup 时使用这些地址 最后 一旦所有 fixup 完成 dyld 将自底向上运行初始化程序 五年前 我们宣布了一项新的 dyld 技术 我们意识到上面绿色的步骤在 每次 App 启动时都是相同的 因此 只要程序和 dylib 没有变化 所有绿色的步骤都可以在 第一次启动时缓存 并在后续启动时重复使用 今年 我们将宣布 dyld 的 更多性能改进 我们宣布了一项新的 dyld 功能 名为页入时链接 与 dyld 在启动时将所有 fixup 应用于所有 dylib 不同 内核现在可以延迟到在页面调入时 才在 DATA 页面做 fixup 通常的情况是 在 mmap() 映射区域的某个页面中 首次使用某个地址会触发 内核读入该页面 但现在 如果它是一个 DATA 页 内核还将应用该页所需的 fixup 十多年来对于 dyld 共享缓存中 的 OS dylib 这一特定场景 我们已经采用了页入时链接 今年 我们将其推广提供给所有人 这种机制减少了脏内存和启动时间 这还意味着 DATA_CONST 页面 是干净的 意味着它们可以像 TEXT 页面一样 被驱逐和重新创建 这就减少了内存压力 这个页入时链接功能 将会出现在即将发布的 iOS macOS 和 watchOS 中 但页入时链接只适用于 用链式 fixup 构建的二进制文件 这是因为使用链式 fixup 时 大多数修复信息将编码在 磁盘上的 DATA 段中 这意味着在页入期间 内核可以使用这些信息 需要注意的是 dyld 只在 启动过程中使用这种机制 之后任何通过调用 dlopen() 加载的 dylib 都不会获得页入时链接 在这种情况下 dyld 采用传统的方法 并在 dlopen 调用期间应用 fixup 记住这一点 我们回头再看看 dyld 工作流程图 五年来 dyld 一直在优化 上面绿色的步骤 在首次启动时缓存这些工作 并在以后的启动中重用它 现在 dyld 可以优化“应用 fixup” 的步骤 不实际执行 fixup 而是让内核在页入时 延迟执行这些操作 现在您已经了解了 dyld 的新特性 接下来我们来讨论 动态链接的最佳实践 您可以做些什么来 帮助提高动态链接性能呢 正如我刚才所展示的 dyld 已经加快了 动态链接的大部分步骤 您自己可以控制 dylib 的数量 dylib 越多 dyld 加载它们的工作就越多 相反 dylib 越少 dyld 需要执行的工作就越少 接下来您可以查看静态初始化程序 它是始终运行在 main 之前运行的代码 例如 不要在静态初始化程序中 进行 I/O 或联网 任何可能耗时超过几毫秒的操作 都不应该在初始化程序中完成 正如我们所知 世界正在变得越来越复杂 您的用户想要更多的功能 因此 使用库来管理 所有这些功能是合理的 您的目标是在动态库 和静态库之间找到最佳平衡点 静态库太多 迭代构建/调试周期就会变慢 另一方面 太多的动态库 会使您的启动时间变慢 并且您的客户会注意到 但我们今年加快了 ld64 的速度 所以您的最佳选择可能已经改变了 因为您现在可以直接在 App 中使用更多的静态库 或者更多的源文件 并且仍可以在相同的时间内构建 最后 如果对您的现存用户群适用 则更新到新的部署目标 可以使工具生成链式 fixup 程序 使二进制文件更小 并缩减启动时间 最后 我要向开发者介绍 两个将帮助您探索链接内部过程的新工具 第一个工具是 dyld_usage 您可以用它来追踪 dyld 在做什么 该工具仅适用于 macOS 但您可以在模拟器中使用它来跟踪 App 的启动情况 或如果 App 是为 Mac Catalyst 构建的 以下是在 macOS 上 针对 TextEdit 运行的示例
从前几行可以看出 整个启动过程花了 15 毫秒 但由于页入时链接的缘故 fixup 只用了 1 毫秒 现在绝大多数时间都花在 静态初始化程序上
下一个工具是 dyld_info 您可以用它检查磁盘上 和当前 dyld 缓存中的二进制文件 该工具有许多选项 但我将向您展示 如何查看导出和 fixup 这里的 -fixup 选项显示了 dyld 将处理的所有 fixup 地址信息 及其目标 无论文件是旧式 fixup 还是新链式 fixup 输出都是相同的 这里的 -exports 选项将显示 dylib 中所有导出的符号 以及每个符号从 dylib 开始的偏移量 在本例中 它显示了关于 Foundation.framework 的信息 这是 dyld 缓存中的 dylib 磁盘上没有文件 但是 dyld_info 工具使用 与 dyld 相同的代码 因此可以找到它
现在您已经了解了静态库和动态库的 历史和性能权衡 您应该检查 App 所做的工作 并确定是否找到了最佳位置 接下来 如果您有一个大型 App 并注意到构建需要一段时间来链接 可以尝试 Xcode 14 它拥有更快的新链接器 如果您还想进一步提高 静态链接的速度 请查看我详述的三个链接器选项 看看它们在您的构建中是否有意义 并缩短链接时间 最后 您还可以尝试为 iOS 13.4 或更高版本 构建 App 和任何嵌入式框架 以启用链式 fixup 然后看看您的 App 是否 在 iOS 16 上更小 启动速度更快 感谢观看 祝您的 WWDC 之旅一切顺利
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。