大多数浏览器和
Developer App 均支持流媒体播放。
-
深入探索 Xcode 构建中的并行
了解 Xcode 构建系统如何从您的构建中提取最大并行度。我们将探索如何借助项目的结构设计改善构建效率,介绍如何在 Xcode 中解决各个目标构建阶段之间的关系问题,并分享在使用 Swift 进行编译时充分利用现有硬件资源的做法。此外,我们还将向您介绍 Build Timeline — 这个功能强大的工具可帮助您监控构建的效率和性能。
资源
相关视频
WWDC22
-
下载
♪ ♪ Ben: 大家好 欢迎来到 WWDC 2022 我是 Ben 是 Xcode 构建系统 团队的一名工程师 Artem: 大家好 我是 Artem 是 Swift Compiler 团队的一名工程师 在本次演讲中我们将深入介绍 Xcode 的构建过程 并揭开构建内部并行化的神秘面纱 Ben 首先会介绍关于构建的核心概念 和 Xcode 提供的可用工具 来帮助大家研究构建的性能问题 然后 他将解释 Xcode 如何 在构建目标时提高并行性 在此基础上 我将解释 Xcode 如何在构建 由多个目标组成的项目时 全面并行化构建 并在最后总结要点 Ben 开始吧 我们回顾一下在 Xcode 中 按 CMD+B 构建 App 时发生的情况 作为 Xcode 的一部分 构建系统 通过整个项目的表示来调用 包括所有源文件 asset 构建设置和其他配置 如运行目标 构建系统是关于 App 应该 如何构建的唯一真相来源 它知道使用哪些设置调用哪些工具 以及生成哪些中间文件 以最终创建 App 下一步 构建系统调用工具 来处理项目的输入文件 例如编译器
Clang 和 Swift 这两个编译器 都会生成目标文件 链接器需要这些目标文件来链接 代表 App 的可执行程序 虽然这个顺序没问题 但它的来源并不明显 我们来看看该过程的示例 以及构建系统如何决定 执行所有任务的顺序
Swift 编译器使用输入源文件的方法 来捕获程序员的意图 并将其 转换为机器可执行二进制文件 同时检查源代码中的错误 这个过程可能会失败 从而取消构建 但如果成功 它将为每个输入 创建一个对象文件 这些对象文件用于调用链接器 链接器将它们组合在一起 并向外部链接库添加引用 以生成可执行文件 这两个任务的依赖关系取决于 它们的使用和生产 编译器生成的对象文件被链接器使用 这就建立了对构建系统图的依赖关系 构建系统感兴趣的不是文件内容本身 而是任务之间的依赖关系 在执行构建时 它需要确保 生成另一个任务输入的任务 在该任务开始之前完成 由于这个核心概念适用于 所有类型的任务 让我们切换到一个更通用的 可视化结构中 显示任务 A 和 任务 B 之间的 依赖关系 在这种情况下 A 产生 B 的 部分或全部输入
编译和链接只是构建整个目标 需要执行的许多不同任务 类型中的一小部分 所以我们可以向图中 添加一些更通用的任务 来表示其他类型 如编译 asset 复制文件或代码签名 它们共同表示了构建框架目标 同样 这些任务根据其输入和输出 定义了依赖关系 因此 完成任务 A 可解除运行任务 B 和 C 的障碍 而完成任务 B 可解除任务 D 和 E 的障碍 解除阻塞的任务称为 下游(downstream) 而阻塞的任务称为 上游(upstream) 许多项目包含多个 Framework 目标 所以让我们再添加两个目标 分别代表 App 和 App 扩展 目标通过显式或隐式依赖关系 来定义项目中 彼此之间的依赖关系 例如 通过将其添加到 将二进制文件与库链接 (Link Binary with Libraries)的构建阶段
在这种情况下 App 根据 Framework 嵌入 App 扩展和链接 App 扩展没有使用 Framework 所以它们没有依赖关系
执行构建图时 不同的任务需要不同的时间 这归结为完成工作所需的复杂程度 取决于所需的计算以及输入的大小 编译许多文件通常比复制几个 header 文件要花费更多的时间 考虑到这一点 最终的结果如下所示 当构建系统执行此构建时 它首先运行没有依赖关系的任务 一旦这些任务完成 它们就解锁下游任务 依此类推 遵循这个过程 直到所有计划的任务完成
在以下构建中 构建系统能够 跳过输入未更改而输出仍是 最新的任务 如果一个任务由于输入的改变 而需要重新运行 就像本例中的 App 目标 B 如果它的输出改变了 那么下游任务就必须重新运行 在迭代处理项目时 跳过所有其他任务 可以实现非常快的周转时间 这被称为增量构建(Incremental Build) 但现在我们仍使用完整构建
任务执行的依赖关系和持续时间 定义了下游任务 可以开始的第一个可能时间 有了这些信息 就可以计算出关键路径 这是构建 在理论上无限资源的情况下运行 所需的最短时间 贯穿本次演讲的一个 常见模式将是缩短这条路径 以创建一个高度并行 和可伸缩的构建图 较短的关键路径不一定会缩短 总体构建时间 但它可以确保构建随硬件扩展 关键构建路径定义了 构建速度的限制因素 即使硬件允许 它也无法更快地完成 通过分解关键路径中的 依赖关系来缩短关键路径 在查看构建如何以及 更多地了解其执行情况时 需根据数据的执行时间来绘制数据图 宽度仍然表示任务的长度 这两个宽元素表示长时间运行的任务 而这两个窄元素(Narrow Element) 则表示快速完成的任务
图的高度表示在给定时间内 并行执行任务的数量 请注意 这不会直接映射到 CPU 或内存利用率
空白空间源于任务阻塞了其下游任务 就像在这两个场景中一样 最后 元素的颜色表示其关联的目标 我很高兴地宣布 这种可视化是 Xcode14 中的新功能 有助于理解构建完成后的性能 Xcode 构建时间线(Xcode Build Timeline) 是构建日志一项很棒的新功能 它根据并行化 而不是层次结构 来进行可视化 以了解构建的性能 给定时间的行数表示该时间内的 并行度 单个任务的水平长度 表示它们完成工作所需的持续时间 图中的空白空间显示未完成任务 阻塞了下游任务开始执行的位置 应用于时间线元素的不同颜色 有助于区分构建中的不同目标 在增量构建中 时间线将只包含 实际执行的任务 从而可以发现长时间运行的任务 尤其是在此构建期间 可能不会运行的任务
下面是 Xcode 14 中 构建时间线的演示 在这个窗口中 我从 Github 打开了 一个 swift-docc 项目的副本 该项目构建了文档编译器 为了全面了解为该方案构建的目标 请查看方案编辑器 我点击方案并选择 Edit scheme 将它打开
build 选项卡包含所有目标的列表 目标可以显式添加到方案中 也可以隐式添加到方案中 方法是作为 已是方案部分目标的依赖关系 在本例中 我使用的是一个 Swift 软件包 该包具有自动生成的方案 因此清单中的所有目标 都是显式定义的
该日志表示我先前执行的方案的构建 其中包含构建系统执行的 所有任务的条目 这些条目根据它们所属的目标 组织在一个层次结构中 比如这里的 docc 目标 为了成功构建该目标的可执行文件 Xcode 运行了该节点的子节点 所表示的所有任务 由于构建日志当前处于 All 状态 因此它还显示了以前构建中 不需要在增量构建中重新运行的任务 选择 Recent 仅显示实际执行的任务 隐藏所有跳过的任务 除此之外 构建日志还支持过滤器 以便只显示出现问题甚至失败的任务
要打开此构建的构建时间线 请转到编辑器选项并打开助手 构建时间线在构建日志旁边打开 与往常一样 编辑器选项提供了 在右侧或底部 显示助手的设置 我将暂时停留在底部 基于构建的并行化 时间线将相同的数据 可视化为 Recent 构建日志 选择其中一个元素 也会选择另一个元素 这样就可以在上下文中 查看任务的执行情况 从这里的时间线可以了解到 与所选任务平行执行的任务 我正在触控板上 使用收缩手势再次缩小
在时间线中选择一个元素 会在构建日志中显示它 由于构建日志是基于 层次结构进行可视化的 因此可以查看哪些文件 作为编译器调用的一部分进行了编译 它还允许查看该调用的整个命令行
按住 Option 键 同时在构建时间线中选择一个区域 可以调整视口以适应该时间框架 在这里 我们可以验证 目标 ArgumentParser 的链接 实际上正在等待相同目标的编译 在向上滚动时按住 Option 键 可以快速缩放 时间线中的行数表示 当时并行运行的任务数 这样的空白空间表示 任务正在等待未产生的输入 理想情况下 时间线是垂直填充的 并且具有尽可能少的空白空间 这将最大程度地扩展构建图 并使构建速度更快 硬件速度越快 为了实现这一目标 Xcode 今年进行了许多改进 以缩短关键路径 接下来 让我们看看 Xcode 如何定义和构建单个目标 以及它如何提高并行性 在配置目标时 构建阶段描述 生成目标产品所需要做的工作 它们是在项目编辑器中定义的 可以包含一组要编译的 源代码文件和 asset 需要复制的文件 如 header 或资源 以及应该链接的库或应该执行的脚本 许多构建阶段使用来自其它 构建阶段的输入或输出来描述任务 从而在它们之间创建依赖关系 例如 目标的源文件 必须在链接之前编译 然而 这并不适用于所有的构建阶段 构建系统将考虑构建阶段的 输入和输出 以确定它们是否可以并行运行 而不是以线性顺序 运行每个构建阶段的任务 例如 编译和资源复制可以并行运行 因为它们都不依赖于对方的任何输出 但是 链接仍然必须在编译之后进行 因为它依赖于该阶段生成的目标文件 现在 让我们考虑一个包含 Run Script 构建阶段的不同目标 与其他构建阶段不同 脚本阶段的输入和输出 必须在目标编辑器中手动配置 因此 构建系统将一次运行 一个连续的脚本阶段 以避免在构建过程中引入数据竞争 如果目标中的脚本配置为 基于依赖关系分析运行 并指定其输入和输出的完整列表 则构建设置 FUSE_BUILD_SCRIPT_PHASES 可以被设置为 YES 指示构建系统应尝试并行运行它们 但是 当并行运行脚本阶段时 构建系统必须依赖于 指定的输入和输出 所以请注意 脚本阶段的输入或输出列表不完整 可能会导致数据竞争 这很难调试 为了缓解这种情况 Xcode 支持用户脚本沙盒 以精确声明每个脚本阶段的依赖关系 沙盒是一种可选功能 它可以阻止 Shell 脚本 意外访问源文件和中间构建对象 除非它们被显式声明为 该阶段的输入或输出 在本例中 input 和 output.txt 都没有声明为该脚本阶段的依赖关系 沙盒将阻止脚本在构建项目时 读写这两个文件 当脚本妨碍沙盒时 读取失败 并显示非零退出代码 从而导致构建失败 除此之外 Xcode 还会列出脚本阶段 试图访问但没有正确声明的所有路径 将这两个文件作为依赖关系信息 添加到此脚本阶段 可以解决此问题 通过这种方式 沙盒可以确保 脚本不会错误地访问 它声明的输入和输出之外的任何文件 现在 让我们研究一个 具有多个脚本阶段的示例 看看沙盒如何防止数据竞争 和不正确的构建 有两个脚本阶段 第一个文件读取文本文件 计算其内容的校验和 并将该值写入派生的 DERIVED_FILE_DIR 中的中间文件 另一个脚本读取相同的文本文件 以及生成的校验 并将它们注入 html 文件中 以便稍后在 App 中显示 如果没有声明这些阶段的 输入和输出依赖关系的精确集合 当 FUSE_BUILD_SCRIPT_PHASES 打开时 Xcode 将并行运行这两个脚本 我们详细检查一下这个有问题的场景 我们假设 Generate HTML 缺少 checksum.txt 的输入声明 但两个脚本的所有其他输入和输出 都已正确声明 如果不使用沙盒 这种错误配置可能不会引起注意 从而在构建中引起问题 这意味着 Xcode 将无法推断 两个阶段之间的依赖关系 并计划在 FUSE_BUILD_SCRIPT_PHASES 打开时 并行运行它们 这里存在一些危险 由于 checksum.txt 没有被列为 Generate HTML 的输入依赖关系 在清理构建过程中 脚本将试图读取 文件系统中没有的文件 另一个危险是 如果 checksum.txt 由于 Calculate Checksum 之前的运行 而在磁盘上可用 当两个脚本并行运行时 Generate HTML 可能会拾取过时的文件 这是一个用户错误 在沙盒中执行脚本 有助于防止这个问题 打开沙盒后 Generate HTM 将在尝试读取 checksum.txt 时立即失败 错误信息将指导您添加 该构建阶段缺少的输入 正确定义输入和输出 将引导 Xcode 遵从 两个阶段之间的依赖关系 从而使 calculate checksum 在 Generate HTML 之前运行 而不相关的构建阶段 仍然可以并行执行 要想为目标启用 Sandboxed Shell Scripts 就请在构建设置编辑器或 xcconfig 文件中 将 ENABLE_USER_SCRIPT_SANDBOXING 设置为 YES 总而言之 sandboxed shell script 允许有正确的依赖关系信息 以实现更快 更强健的增量构建 因为构建系统有信心在 输入没有变化 且输出仍然有效的情况下 跳过脚本阶段 否则就重新运行脚本 启用脚本目标的构建设置会阻止 对项目源根目录以及派生数据 目录中的文件的访问 前提是这些文件 未在项目中明确定义为 脚本的输入或输出 沙盒不会阻止对任何其他 目录的未授权访问 所以不要认为这是一个安全功能 使用此功能有助于 调试现有脚本阶段的 缺失输入或输出 以确保配置有效 并且结合之前解释的构建设置 FUSE_BUILD_SCRIPT_PHASES 通过沙盒正确定义 依赖边缘的脚本阶段 可以并行执行 以减少构建的关键路径 这就是并行化构建目标的步骤 现在 Artem 将为大家揭开 构建多个目标时 并行化的神秘面纱 谢谢 Ben 现在 我们已经介绍了 构建系统任务的基础知识 以及在项目中构建目标 可能需要的阶段 让我们以更全面的视角 来探究 Xcode 如何使用 Swift 目标之间的依赖关系 来最大限度地提高构建的并行性 以及项目的结构和组织 如何影响构建时间 您的项目可能有多个层次结构 例如 一个 App 目标 依赖于一组本地库 这些库按照语义边界 和在几个框架中被分解成多个目标 每个目标包含许多不同的 构建阶段和步骤 在其他目标的构建阶段之间 产生和使用文件依赖关系 随着项目规模的增长 这些任务图的规模和复杂性也在增加 而 Xcode 构建系统将这些 层次结构扁平化 将构建分解成与所有目标的构建阶段 相对应的大量任务 对于 Swift 目标来说 有一种特殊的任务是编译 将 Swift 目标的源代码 构建为二进制产品 是一项复杂的操作 通常包含 许多构建规划 编译和链接的子任务 协调这些任务被委托给 Xcode 工具链中的 一个专门的工具 Swift Driver Driver 拥有专门的知识 知道何时以及如何为 目标的源代码构建所需的 编译器和链接器调用 任何包含 Swift 代码的目标 还对应一个代码分发单元 模块 捕获该目标的公共接口的 二进制模块文件 是下游目标开始编译 所需的构建产品 我们仔细看看 Swift Driver 是如何构建其中一个目标的 您的目标可能由几个 源文件的集合组成 在发布或优化版本中 driver 将调度一个包括所有 源文件的编译器任务 以最大化地优化机会 这个单一的编译任务还将生成 目标的 Swift 模块 在调试或增量编译模式下 Swift Driver 将所需的编译工作分解为 可以并行运行的较小子任务 其中一些任务可能不需要 在增量构建上重新运行 然后 生成 Swift 模块需要 一个额外的步骤 将每个编译任务的 部分中间产品合并在一起 如本例中所示 如果目标中的源文件数量很多 则根据构建系统的启发 也可以将单个文件 分配给批编译子任务 构建日志突出显示有哪些源文件 被分配给 批处理编译作业 每个文件的诊断信息都有单独的条目 对于更快和更小的增量构建来说 能够在不同的源文件之间 并行化目标的构建是至关重要的 因此请确保调试构建 使用增量编译模式设置 在 Xcode14 之前 由于 Xcode 构建系统 和 Swift Driver 之间的边界 目标构建阶段的协调 以及由 Driver 的每个目标实例 产生的编译子任务 彼此独立地进行 每个组件都尽其所能 最大限度地利用可用的系统资源 我们以这个构建图为例 深入了解如何安排编译阶段之间的 相互关系 正如我们之前了解到的 Swift 的目标依赖关系是通过 让它们的依赖关系提供一个 二进制模块文件来解决的 该文件捕获了依赖关系的公共接口 解决了这些依赖关系后 我们得出了以下顺序 时间线显示了每个目标的顶级 Swift Driver 任务 及其各自的子任务 在 Xcode 14 中 由于 Swift Driver 的全新实现 它本身现在是用 Swift 编写的 构建系统和编译器完全集成在一起 在编译代码所必须执行的所有任务中 Xcode 构建系统充当中央调度器 这种中央计划机制允许 Xcode 做出精细的调度决策 从而更好地保证构建项目 将只使用尽可能多的可用资源 而不会过度占用 CPU 并降低整体系统性能
以前是 Xcode Build System 权限之外的 子任务孤岛集合 现在则完全属于 构建系统调度程序的领域
对于中央任务池中的所有单个子任务 必须考虑构建调度程序所做的权衡 例如 在 8 核机器上 调度程序的默认设置是将可用任务 分配给八个可用执行槽中的一个 这些任务的依赖关系 已得到满足并准备就绪 一旦其中一个插槽空闲下来 构建系统就会尝试用 更多未完成的工作来填充它 在核数更高的机器上 我们能够执行更多的并行的工作 但这也意味着我们更有可能 拥有空闲的内核 可以执行更多的工作 但所有未完成的任务 仍在等待它们的输入 就像当前正在进行 或等待的其他任务所产生的一样 新的集成构建系统允许调度程序 显著减少这种空闲时间 要了解这一点 我们再来看一下 编译的目标依赖关系 即二进制模块文件 是如何被解析的
如前所述 编译子任务的部分结果 被合并到目标的最终模块产品中 一旦该产品可用 下游目标就可以开始编译 在 Xcode 14 和 Swift 5.7 中 新增了一个功能 目标模块的构建是在一个 单独的 emit-module 任务中 直接从所有的程序源文件中完成的 这意味着目标的依赖关系可以在 emit-module 任务完成后立即开始编译 而无需等待依赖关系目标的 所有其他编译任务 能够更快地解除下游目标编译阻塞 减少了等待空闲 CPU 内核 可用工作的时间 即构建时间线中活动间隙的空闲空间
将此扩展到项目的其余部分表明 尽管我们执行的总体工作量相似 但构建系统能够更有效地 使用计算机资源 通常可以更快地完成构建
现在 让我们看看构建系统 在构建 Swift–Eager Linking 时 可以执行的第二个 跨目标优化 在前一个示例的基础上 我们为每个目标添加了链接器任务 它们都位于构建的关键路径上 在这种情况下 由于目标 B 链接目标 A 目标 B 的链接任务必须等待 目标 A 的链接输出生成 以及它自己的编译任务完成之后 才能运行 然而 使用即时链接 目标 B 的链接任务可以依赖于 目标 A 的 emit-module 任务 因此 目标 B 可以 在构建的早期开始链接 与链接目标 A 并行运行 并缩短关键路径 这是怎么做到的 通常 具有链接产品依赖关系的 两个目标的依赖关系图如下所示 除了目标自身的编译输出之外 链接依赖目标还需要其 依赖目标的链接产品 当急切链接时 这种依赖关系被打破 从而允许依赖目标更早地开始链接 现在它不再依赖于 依赖关系的链接产品 而是依赖于 emit-module 任务 在构建过程的 早期产生的基于文本的动态库存根 这个存根包含一个符号列表 这些符号将出现在链接的产品中 供依赖者使用 您可以使用屏幕上显示的 Xcode 构建设置启用此优化 Eager Linking 适用于由其依赖对象 动态链接的所有纯 Swift 目标 总而言之 Xcode 构建系统 是一个复杂的调度引擎 它试图通过并行运行构建阶段 来获取尽可能多的并行性 像 Script Sandboxing 这样的功能 确保您的构建 最大程度地并行和可靠 Xcode 和 Swift 比以往任何时候 都更加一体化 项目结构 其模块化 由目标产品之间的依赖关系 构建阶段的数量 和复杂性组成的图形的整体形状 以及机器的可用计算资源 所有这些都是 Xcode 能够并行化 和加速构建的因素 有了这些知识 再加上 构建时间线等强大的新工具 使您可以很好地检查项目 并深入了解构建 如果您想了解更多 幕后的技术细节 我们介绍的 Xcode 使用的 许多技术都是开源的 您可以在下面的链接中找到 GitHub 上的 Swift Driver 资源库 要了解更多关于 Xcode 的精彩讲座 请在讲座 What's new in Xcode 中 查看今年以来的 所有新功能和改进 在讲座 Link fast: Improve build and launch times 中 了解 Xcode 14 的链接器 如何将链接时间提高两倍 感谢收看 我们希望您能了解一些 关于 Xcode 构建的新见解 我们期待您的创造成果 祝您余下的研讨会之旅一切顺利
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。