感谢 Nicholas Nethercote 和 Alex 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
- #66598 — 最初的方法,被认为过于简单。
- #66961 — 概述所采用策略的问题。
- #70289 #70297 #70345 #70384 #70644 #70729 #71374 #71716 #71754 — 一系列重构,为新行为做准备并进行一些清理。
- #71323 — 引入了一个新标志来控制是否嵌入位码。
- #70458 #71528 — 切换了嵌入 LLVM 位码的方式。
- #8066 #8192 #8204 #8226 #8254 #8349 — 一系列 Cargo 更改,以实现新功能。
结论
尽管这是一个概念上简单的更改(LTO=位码,非 LTO=目标代码),但实现它需要大量的准备工作。需要考虑许多极端情况和特定于平台的行为,以及要执行的测试。当然,还有关于新命令行标志名称的强制性争论。这带来了性能方面的显着提高,尤其是对于 LTO 版本,以及磁盘空间使用方面的巨大改进。感谢所有为此做出贡献的人!