改进 Rust 编译时间以实现内存安全
Rust 编译时间经常被视为需要改进的领域,包括在年度社区调查中。缓慢的编译时间可能会成为其被采用的障碍,因此改进编译时间有助于扩大 Rust 语言的影响。这也是我们的目标之一:提高更多内存安全软件的潜力,特别是在互联网基础设施的最关键部分(例如网络、TLS、DNS、操作系统内核等)。
Rust 历来更关注运行时性能而不是编译时间,就像 LLVM(Rust 编译中使用的最重要组件之一)一样。我觉得这对于现代编译器来说是一个常见的故事,无论是在工程界还是学术界。与其他一些专为拥有闪电般快速的单遍编译器而定制的旧语言相比,这并不是 Rust 设计中最重要的原则。设计人员的主要关注点是确保该语言能够提供所需的安全保证,而不影响程序的性能。
尽管如此,编译时间最近受到了更多关注,并且多年来得到了显着改善。不过我们还有更多工作可以做。为了帮助推动事情向前发展,我们不断采用和完善工具和流程,以帮助培育绩效文化。示例包括:
- 一套编译器基准测试,用于合并的每个 PR。每一个都在不同的用例下进行基准测试:在检查、调试或发布模式下,有或没有增量编译(具有不同的更改粒度),随着时间的推移记录和绘制许多不同的硬件指标,以及来自编译器的内部分析基础设施。
- 这些基准可以在合并之前根据 PR 的需要触发,以避免出现意外。
- 结果摘要会发布在每个合并的 PR 上,以便在 PR 需要关注时通知作者、审阅者和工作组。
- 每周的分类流程,总结一周的结果,并有一个友好的人员在循环中,以防有需要拨打的电话:帮助清除结果中的噪音源(有时会发生),可以忽略的无关紧要的小回归或者需要更多工作,或者需要恢复的不可预见的性能问题。我们也庆祝胜利!
- 这些摘要用于在每周会议上通过 Rust 中的本周向编译器团队以及社区通报最新进展。
优先事项
我与 Prossimo 就以下优先事项进行了合作:
- 使流水线编译尽可能高效
- 提高原始编译速度
- 改进对持久、缓存和分布式构建的支持
我们首先寻找速度慢的部分。从板条箱编译层面全面地看待这一点,可能会提供新的见解,特别是因为我们以前很少这样做。我从 crates.io 收集了 1000 个最受欢迎的 crate,并收集了完整的货物构建(包括依赖项)的数据。我还收集了 rustc 自分析数据以获取更高级别的视图,并分析高内存使用率的来源。所有这些都是在检查、调试和发布模式下完成的,具有不同程度的并行性。
从这个高层次的角度来看,我们可以看到一些有希望的前进方式:
- 编译管道的改进:分析以查找缓慢的根源,然后找到这些问题的解决方案和缓解措施。这可能是在 rustc、cargo 甚至 rustup 中。
- 提高目标 crate 的编译时间:如果流行的 crate 包含缓慢的来源,这反过来会影响所有间接依赖它的 crate。在某些情况下,除了编译器和工具之外,还可以改进包本身。
- 防止未来的缓慢:分析、跟踪、减轻回归和错误(例如,增量编译问题可能导致关闭该功能,就像以前发生的那样)。
- 最后,帮助人们实现上述目标(贡献者和板条箱作者)。人们通常希望看到项目中缓慢的根源,并且让编译器显示此信息将帮助他们相应地组织或重构代码。
根据这些发现,编译器性能工作组起草了一份路线图,更新了我们的基准测试套件,以便它们保持相关性并代表人们使用的实践,并制定了一项政策来定期更新我们的基准测试,以便它们保持相关性。我们在这些新包中看到了新的热点和低效率,以及一些与管道和调度、构建脚本的常见存在以及 proc 宏的相对重要性相关的令人惊讶的发现。
我所从事项目的概述
随着对语言“const”部分的持续改进和扩展,编译时函数评估引擎得到了更多的使用。它的效率很重要,并且在未来会越来越重要:通过将其限制为已知包含引用或内部可变性的分配,在分配的实习中(这在遍历静态数组时很重要)实现了一些加速。其中一些缓冲区可能很大(这是将来可以进行改进的另一个领域),并且还具有派生数据(例如掩码)来跟踪分配的每个字节是否已正确初始化,但是编译器使用的哈希算法( FxHash )更适合较短的密钥,我们可以通过散列较少的整体数据来限制一些昂贵的实习工作。
哈希在编译器中大量使用,对 FxHash 的改进(或使用不同的算法)将对编译时间产生显着影响。虽然它出人意料地有效,但我们尝试并测量了一个有趣的变化,但我们最终选择不落地:它在 Intel 与 AMD CPU 上的表现略有不同(并且在某些指标上后者看起来稍好一些)。例如,在更大的缓冲区上仍有可能进行改进,我们可以更好地利用 SIMD,但目前 rustc 仍然以基准 x86-64 CPU (SSE2) 为目标,因此这是一个留给未来的工作项目。
一个相关领域是内存分配(有一种说法是“rustc 是变相的哈希和内存分配基准”),我们已经在 Linux 和 macOS 上使用 jemalloc。由于这是系统软件性能工作的一个活跃领域,因此我们会定期尝试替代分配器,因为它们不断改进。例如微软的mimalloc或者snmalloc。我们还跟踪、测试了 rustc,并将其更新为期待已久的 jemalloc 5.3 版本。由于 rustc 的架构方式是为了从我们的其他工具中使用,因此与常规 rust 程序相比,使用自定义分配器不太容易(无法使用 #[global_allocator] ,并且由于与 LLVM 交互而存在额外要求)所以这里仍然需要进行一些改进:macOS 上仍然存在一些(难以避免的)效率低下的问题,并且 Windows 仍然使用默认的系统分配器。
正如我们上面提到的,针对 x86-64 目标的 rustc 是针对 x86-64 基准 CPU 构建和分发的,因此我们还尝试看看使用更新的微架构级别是否会提高性能:x64-v2(“SSE4”)和 x64 -v3(“AVX/AVX2”)。虽然通过自动矢量化有所改进,但分发此类工件给基础设施和 CI 带来的复杂性还不值得。然而,对于库和编译器团队来说,这都是未来工作的一个有趣领域:编译器和标准库能够更轻松地利用现代 SIMD 算法,将带来改进自动矢量化的好处。
我们上面提到,一些 linux 和 macOS 目标正在使用自定义分配器,但这是为了更好的性能而设计的一组更大的配置和分发功能的一部分,并且可以独立启用或禁用(主要取决于 CI 构建者是否有能力完成额外的工作)。例如,除了拥有自定义分配器之外,在构建 LLVM 或 rustc 时进行链接时优化、对 LLVM 或 rustc 进行配置文件引导优化、使用工具优化最终二进制文件(例如 BOLT)。所有这些都在 x86_64-unknown-linux-gnu 上启用。在最近的调查中,60-70% 的受访者回答主要针对 Linux,但超过 30% 的人也针对 macOS,同样超过 30% 的人也针对 Windows。因此,我们致力于对后两个目标进行改进,以匹配 Linux 用户所看到的完善程度。我能够更新我们的引导代码、CI 和性能收集器,以便能够在 Windows 上使用 PGO 进行 LLVM 和 rustc,并在构建 rustc 时启用 ThinLTO 以进行其他改进。当时 macOS 开发者没有能力支持这样的更改,但情况后来有所改善,并且将来也会进行类似的改进。适用于 rustc 的 ThinLTO 已在 nightly 上启用)。
我们能够升级和删除一些 rustc 依赖项(并删除现在未使用的 jemalloc 包装器),通过改进本地和 CI 上的 rustc 编译时间,使贡献者体验稍好一些。
我们研究了通过在查找时减少遍历来加速处理负 impl(标准库部分中使用的不稳定功能)的“特征一致性”部分(检查任何给定类型是否最多有一个特征实现)。因其专用属性。
Cargo 和 rustc 支持“流水线”以加快编译速度:如果不涉及链接,则包可以在其依赖项完全完成构建之前提前开始编译。它只需要来自它们的所谓“元数据”。因此,cargo 要求 rustc 发出它,当它可用时,如果有足够的并行性,它可以开始构建依赖的 crate。在基准测试中,我们看到流行的库 hyper 没有看到这种管道,它的用户在构建它时也没有看到这种管道。有不同的方法可以解决这个问题,其中一种方法利用了仍在开发中的货物功能。因此,我们选择帮助测试和基准 Cargo 的 --crate-type 不稳定选项,并帮助 hyper 使用它来解决问题。
基准测试显示,某些 build.rs 脚本编译速度可能很慢,而且非常常见。他们展示了语言中缺失的部分,需要这种特定于货物的概念来实现特定于目标或语言版本 (MSRV) 的目标,构建速度比我们预期的要慢,并且涉及 rustup 的开销。我们帮助恢复了 RFC,以便将来可以删除其中一些脚本并缩短编译时间,并制定了加快编译速度的计划。
大多数时候,用户不会构建他们的整套依赖项(只有在升级 rustc、更改 RUSTFLAGS 或功能组合等之后),但这经常发生在 CI 上(在内核数量普遍较低的构建器上)和我们可以对这里的货物进行一些改进。首先,在编译构建依赖项(构建脚本和 proc-macros 及其依赖项)时可以选择一些更好的默认值,特别是,debuginfo 在这里不像实际的二进制/库那么有用。我们制作了一个原型并在 1000 个箱子数据集上对其进行了基准测试。有公开的 PR 可以将其添加到货物中,但审核尚未完成。
然后我们还看到 proc-macros 可能会阻碍构建并行性,并且似乎比我们预期的要晚构建,有时会参与管道停顿。查看货物的时序图,在某些情况下可以通过更改 crate 图的调度来改进编译。制作了一些原型并在 1000 个箱子数据集上进行了基准测试。第一个成功地利用了货物中现有的“优先级”概念(取决于板条箱的工作项数量的代理),以便在选择下一个要构建的板条箱时偏向于更高优先级的板条箱,并且此后已装货。这在存在大量依赖项或核心数量较低时非常明显(因此与 CI 上看到的配置相匹配,尤其是在免费套餐中)。第二个原型还允许调度提示,以便用户可以为某些 crate 分配更高的优先级(本着此功能请求的精神),例如更快地构建常见的 proc-macro crate(或任何可以通过不同的调度方式更好地利用并行性的 crate) )。
最后,为了帮助用户了解 proc-macros 是否减慢了他们的构建速度,我扩展了自我分析器以显示 proc-macro 扩展,并根据需要选择更精细的细节,以便能够分析每个 proc-macro 的使用及其持续时间。 (类似地支持常规宏将来会很有趣)。
本着同样的精神,由于单态化通常是不易察觉的编译时间成本,因此已经开始了一些尝试显示通用函数实例化的数量和大小的工作。 (这很有帮助,但未来仍需改进:目前这是在 MIR 上计算的,而实际发出的 LLVM IR 的统计数据(例如 Cargo llvm-lines)会更有帮助)。
我们能够在完整的板条箱以及特定的构建依赖项上对 bjorn3 的cranelift代码生成后端进行基准测试(因为它们也是为 cargo check 构建而构建的,并且总是在没有优化的情况下构建):没有任何问题,并且表现令人印象深刻。它正在成为 LLVM 后端调试构建的可行替代方案。
由于改进特定流行板条箱的编译时间将对整个生态系统产生影响(特别是当 rustc 本身在修复给定问题时可能有不同的权衡时),我们注意到一些热点并制作了 PR 来帮助解决 async-std、quote、diesel 问题。
我还重新开始了一项仍在进行中的长期工作:做好能够在 Linux 上默认切换到使用 lld 链接器所需的准备工作。人们倾向于使用默认链接器(通常是 ld.bfd ),它比 lld 慢很多。对于尚未使用更快的链接器的项目来说,让 Cargo/rustc 使用它会很明显。
我们看到了从 crate 元数据中删除某些项目的机会:有时信息可能会被冗余存储,有时它仅由工具(例如 rustdoc)使用,并且已经开始进行清理。未来在这个方向上的更多工作可能会很有趣(其他贡献者已经完成了很多工作):该领域的改进适用于所有板条箱的加载,特别是加快解码 libstd/libcore 的速度(尽管这已经相当快了)最终将适用于大多数编译会话。
正如前面提到的,变得更快的一个重要部分并不是变得更慢,因此在性能方面进行了很多回归分析(包括特定于我们的基础设施的分析,我们经常将多个 PR 汇总为一个以节省 CI 时间)例如增量编译。
结论
这是对 2022 年取得的一些工作的快速回顾,工作组的其他成员也发表了类似的报告:Nick Nethercote 经常这样做,Jakub Beránek 最近也这样做。许多其他人也贡献了各种改进,从标准库到基础设施,所有这些工作结合在一起导致了编译时间的显着改进。
编译器性能工作组已经完成了路线图中的许多(如果不是全部)项目,但性能工作从未真正完成,并且在我们发言时仍在继续。正在起草 2023 年的探索和计划,例如重新致力于更好地利用编译器中的并行性等。