磁盘空间和 LTO 改进

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

感谢 Nicholas NethercoteAlex Crichton 的工作,最近在减少编译库的大小和提高编译时性能方面取得了一些进展,尤其是在使用 LTO 时。本文深入探讨了一些变化的细节,并对收益进行了评估。

这些更改在过去三个月中逐步添加,最新的更改在几天前才进入 nightly 通道。大部分改进将在 1.46 稳定版(2020-08-27 发布)中找到。如果任何使用 LTO 的项目在 nightly 通道(从 2020-06-13 版本开始)上进行测试并报告出现的任何问题,那就太好了。

背景

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

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

LTO 是一种可以执行程序整体分析的优化技术。它一次性分析来自每个库的所有位码,并执行优化和代码生成。rustc 支持多种形式的 LTO

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

发生了什么变化

已对 rustc 和 Cargo 进行了更改,以根据项目的 配置文件 LTO 设置来控制哪些库应包含目标代码,哪些库应包含位码。如果项目未使用 LTO,则 Cargo 将指示 rustc 不要在 rlib 文件中放置位码,这应该可以减少使用的磁盘空间量。这可能会在性能方面略有提高,因为 rustc 不再需要压缩和写出位码。

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

现在有两个 rustc 标志可用于控制 rlib 的构造方式

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

此外,位码嵌入到 rlib 中的方法已更改。以前,rustc 会将压缩的位码作为 .bc.z 文件放置在 rlib 存档中。现在,位码作为未压缩的部分放置在 rlib 存档中每个 .o 目标文件 中。这有时可能会带来很小的性能优势,因为它避免了压缩位码的成本,有时由于需要将更多数据写入磁盘而速度可能会变慢。此更改有助于简化实现,并且还与 clang 的 -fembed-bitcode 选项的行为相匹配(通常与 Apple 基于 iOS 的操作系统一起使用)。

改进

以下是少数中小型实际项目中观察到的改进摘要。项目的改进将很大程度上取决于代码、优化设置、操作系统、环境和硬件。这些记录是在 Linux 上使用 2 到 32 之间的并行作业设置的 2020-06-21 nightly 版本进行的。

调试版本的性能提升在 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=位码,非 LTO=目标代码),但实现它需要大量的准备工作。需要考虑许多极端情况和特定于平台的行为,以及要执行的测试。当然,还有关于新命令行标志名称的强制性争论。这带来了性能方面的显着提高,尤其是对于 LTO 版本,以及磁盘空间使用方面的巨大改进。感谢所有为此做出贡献的人!