磁盘空间和 LTO 改进

2020 年 6 月 29 日 · Eric Huss 代表 Cargo 团队

感谢 Nicholas NethercoteAlex Crichton 的工作,最近有一些改进减少了编译库的大小,并提高了编译时性能,特别是在使用 LTO 时。本文深入探讨了这些变化的细节以及效益估算。

这些更改在过去三个月里逐步添加,最新的更改几天前已登陆 nightly channel。大部分改进将包含在 1.46 稳定版本(2020 年 8 月 27 日发布)中。对于任何使用 LTO 的项目,非常欢迎在 nightly channel(从 2020 年 6 月 13 日发布开始)上进行测试,并报告遇到的任何问题。

背景

编译库时,rustc 将输出保存到 rlib 文件中,这是一个 存档文件。历史上,这个文件包含以下内容:

  • 目标代码(Object code),即代码生成的结果。这在常规链接时使用。
  • LLVM bitcode,这是 LLVM 中间表示的二进制形式。它可用于链接时优化 (LTO)。
  • Rust 特定的元数据(metadata),涵盖了关于 crate 的各种数据

LTO 是一种优化技术,可以执行全程序分析。它一次性分析所有库中的所有 bitcode,并执行优化和代码生成。rustc 支持几种形式的 LTO:

  • Fat LTO。这执行“完全”LTO,可能需要很长时间才能完成,并可能需要大量内存。
  • Thin LTO。此 LTO 变体比 fat LTO 支持更好的并行性。它可以在利用更多 CPU 的情况下花费更少的总时间,同时实现与 fat LTO 相似(有时甚至更好!)的性能提升。
  • Thin-local LTO。默认情况下,rustc 会将 crate 分割成多个“代码生成单元”(codegen units),以便 LLVM 可以并行处理它们。但这会阻止一些优化,因为代码被分离到不同的代码生成单元中并独立处理。Thin-local LTO 将在单个 crate 的代码生成单元之间执行 thin LTO,从而恢复一些原本会因分离而丢失的优化。当优化级别 (opt-level) 大于 0 时,这是 rustc 的默认行为。

变化了什么

rustc 和 Cargo 都进行了更改,以根据项目的 profile LTO 设置来控制哪些库应包含目标代码,哪些应包含 bitcode。如果项目不使用 LTO,Cargo 将指示 rustc 不在 rlib 文件中放置 bitcode,这应会减少磁盘空间的使用。这可能会稍微提高性能,因为 rustc 不再需要压缩和写入 bitcode。

如果项目使用 LTO,Cargo 将指示 rustc 不在 rlib 文件中放置目标代码,从而避免昂贵的代码生成步骤。这应能缩短从头构建时的构建时间,并减少磁盘空间的使用。

现在可以使用两个 rustc 标志来控制 rlib 的构建方式:

  • -C linker-plugin-lto 导致 rustc 只将 bitcode 放置在 .o 文件中,并跳过代码生成。此标志最初添加是为了支持跨语言 LTO。当 rlib 仅用于 LTO 时,Cargo 现在使用此标志。
  • -C embed-bitcode=no 导致 rustc 完全避免在 rlib 中放置 bitcode。当不使用 LTO 时,Cargo 使用此标志,这会减少一些磁盘空间的使用。

此外,在 rlib 中嵌入 bitcode 的方法也发生了变化。之前,rustc 会将压缩的 bitcode 作为 .bc.z 文件放置在 rlib 存档中。现在,bitcode 作为未压缩的节放置在 rlib 存档中每个 .o 目标文件内。这有时会带来小的性能优势,因为它避免了压缩 bitcode 的开销;有时也可能因为需要写入更多数据到磁盘而变慢。这一变化有助于简化实现,并且与 clang 的 -fembed-bitcode 选项的行为相匹配(通常用于苹果的 iOS 操作系统)。

改进

以下是对少量中小型真实世界项目观测到的改进总结。项目的改进程度将很大程度上取决于代码、优化设置、操作系统、环境和硬件。这些数据是使用 2020-06-21 nightly 版本在 Linux 上记录的,并行任务设置在 2 到 32 之间。

调试构建的性能提升范围为 0% 到 4.7%。较大的二进制 crate 往往比小型库 crate 表现更好。

LTO 构建的性能提升范围为 4% 到 20%。Thin LTO 的表现始终优于 fat LTO。

并行任务的数量对改进程度也有很大影响。并行任务数量较低时,收益远大于数量较高时。使用 -j2 构建的项目可以快 20%,而使用 -j32 构建的同一项目只会快 1%。这可能是因为代码生成阶段受益于更高的并发性,因此它占用的总时间百分比相对较小。

调试构建的总目标目录大小通常会减少 20% 到 30%。LTO 构建的改进没有那么大,范围在 11% 到 19% 之间。

更多细节

Nicholas Nethercote 在 https://blog.mozilla.org/nnethercote/2020/04/24/how-to-speed-up-the-rust-compiler-in-2020/ 一文中写了实现这些更改的过程。为此,rustc 和 Cargo 仓库中提交了多个 PR:

结论

尽管这是一个概念上简单的变化(LTO=bitcode,非 LTO=object code),但要实现它需要相当多的准备和工作。需要考虑许多边界情况和平台特定行为,并进行测试。当然,还有对新命令行标志名称的惯例性无谓争论。这带来了相当大的性能提升,特别是对于 LTO 构建,并在磁盘空间使用方面有了巨大改进。感谢所有为此做出贡献的人!