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