18 编译时间

缩短编译时间

译文 · 基于 The Rust Performance Book

编译时间

原文链接: https://nnethercote.github.io/perf-book/compile-times.html

虽然本书主要关注提升 Rust 程序的运行性能,本节讨论缩短 Rust 程序的编译时间,因为这是许多人关心的相关话题。

缩短编译时间一节讨论了通过构建配置选择来缩短编译时间的方法。本节其余部分讨论需要修改程序代码才能缩短编译时间的方式。

更多缩短编译时间的技巧,请参阅 Corrode 整理的加快 Rust 编译时间的技巧 完整列表。

可视化

Cargo 提供可视化程序编译过程的功能。使用以下命令构建:

1
cargo build --timings

完成后会打印一个 HTML 文件名。在网页浏览器中打开该文件。其中包含甘特图,展示程序中各 crate 之间的依赖关系。这能显示 crate 图中的并行程度,并提示是否应将拖慢编译的大型 crate 拆分。详见文档。

宏

有些宏会生成大量代码,这些代码随后需要编译时间。Rust 编译器的 -Zmacro-stats 标志有助于识别此类情况。

例如,若只想测量项目中的叶子 crate:

1
cargo +nightly rustc -- -Zmacro-stats

编译器会打印过程宏和声明宏所生成代码量的信息。前者通常更值得关注。

或者,若要测量项目中所有 crate:

1
RUSTFLAGS="-Zmacro-stats" cargo +nightly build

要查看生成的代码本身,可以使用 cargo-expand。

不必为生成少量代码的宏操心,但若某宏生成的代码量与手写代码相当,或许可以完全去掉该宏,或换成更轻量的替代方案。 示例。

或者,可以修改宏以生成更少代码。 示例 1, 示例 2。

LLVM IR

Rust 编译器使用 LLVM 作为后端。LLVM 的执行可能占编译时间的很大部分,尤其是当前端生成大量 IR、LLVM 优化耗时很长时。

可用 cargo llvm-lines 诊断这些问题,它会显示哪些 Rust 函数生成了最多 LLVM IR。泛型函数往往最重要,因为在大型程序中可能被实例化数十次甚至数百次。

若泛型函数导致 IR 膨胀,有几种修复方式。最简单的是让函数更小。 示例 1, 示例 2。

另一种方式是把函数的非泛型部分移到单独的非泛型函数中,该函数只会被实例化一次。是否可行取决于泛型函数的具体细节。可行时,非泛型函数常可整洁地写成泛型函数内的内部函数,如 std::fs::read 的代码所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
    fn inner(path: &Path) -> io::Result<Vec<u8>> {
        let mut file = File::open(path)?;
        let size = file.metadata().map(|m| m.len()).unwrap_or(0);
        let mut bytes = Vec::with_capacity(size as usize);
        io::default_read_to_end(&mut file, &mut bytes)?;
        Ok(bytes)
    }
    inner(path.as_ref())
}

示例。

有时像 Option::map 和 Result::map_err 这样的通用工具函数会被多次实例化。用等价的 match 表达式替换它们有助于缩短编译时间。

这类改动对编译时间的影响通常较小,但偶尔可能很大。 示例。

此类改动也能减小二进制体积。

最后修改 August 23, 2026: 更新 (499855b16)