2 构建配置

编译器与链接器选项

译文 · 基于 The Rust Performance Book

构建配置

原文链接: https://nnethercote.github.io/perf-book/build-configuration.html

你可以在不修改代码的情况下,仅通过更改构建配置来大幅改变 Rust 程序的性能。每个 Rust 程序都有许多可能的构建配置。所选配置会影响编译后代码的多种特性,例如编译时间、运行时速度、内存占用、二进制体积、可调试性、可分析性以及编译后的程序可在哪些架构上运行。

大多数配置选择会改善一种或多种特性,同时恶化一种或多种其他特性。例如,常见的权衡是接受更长的编译时间以换取更高的运行时速度。对你的程序而言,正确的选择取决于你的需求和程序的具体情况,与性能相关的选择(其中大多数属于此类)应通过基准测试来验证。

值得仔细阅读本章以了解所有构建配置选项。不过,对于急躁或健忘的读者,cargo-wizard 封装了这些信息,可帮助你选择合适的构建配置。

请注意,Cargo 只查看工作区根目录 Cargo.toml 文件中的 profile 设置。依赖项中定义的 profile 设置会被忽略。因此,这些选项主要与二进制 crate 相关,而非库 crate。

Release 构建

最重要的构建配置选择很简单,但容易被忽视:当你需要高性能时,确保使用 release 构建而非 dev 构建。这通常通过在 Cargo 中指定 --release 标志来完成。

Dev 构建是默认的。它们适合调试,但未经过优化。运行 cargo build 或 cargo run 时会生成。(或者,不带额外选项运行 rustc 也会生成未优化的构建。)

考虑以下 cargo build 运行的最后一行输出。

1
Finished dev [unoptimized + debuginfo] target(s) in 29.80s

此输出表明已生成 dev 构建。编译后的代码将放在 target/debug/ 目录中。cargo run 将运行 dev 构建。

相比之下,release 构建经过更多优化,省略调试断言和整数溢出检查,并省略调试信息。相比 dev 构建,10–100 倍的加速很常见!运行 cargo build --release 或 cargo run --release 时会生成。(或者,rustc 有多种优化构建选项,例如 -O 和 -C opt-level。)由于额外的优化,这通常比 dev 构建耗时更长。

考虑以下 cargo build --release 运行的最后一行输出。

1
Finished release [optimized] target(s) in 1m 01s

此输出表明已生成 release 构建。编译后的代码将放在 target/release/ 目录中。cargo run --release 将运行 release 构建。

有关 dev 构建(使用 dev profile)与 release 构建(使用 release profile)之间差异的更多详情,请参阅 Cargo profile 文档。

release 构建中使用的默认构建配置在上述编译时间、运行时速度和二进制体积等特性之间提供了良好的平衡。但还有许多可能的调整,如下各节所述。

最大化运行时速度

以下构建配置选项主要用于最大化运行时速度。其中一些还可能减小二进制体积。

代码生成单元

Rust 编译器将 crate 拆分为多个代码生成单元以并行化(从而加速)编译。然而,这可能导致编译器错过一些潜在的优化。你可以通过将单元数设置为 1 来改善运行时速度并减小二进制体积,代价是增加编译时间。在 Cargo.toml 文件中添加以下行:

1
2
[profile.release]
codegen-units = 1

示例 1, 示例 2。

链接时优化

链接时优化(LTO)是一种全程序优化技术,可将运行时速度提高 10–20% 或更多,同时减小二进制体积,代价是更长的编译时间。它有多种形式。

第一种 LTO 形式是瘦本地 LTO(thin local LTO),一种轻量级 LTO。默认情况下,编译器对任何涉及非零优化级别的构建使用此形式,包括 release 构建。要显式请求此 LTO 级别,在 Cargo.toml 文件中添加以下行:

1
2
[profile.release]
lto = false

第二种 LTO 形式是瘦 LTO(thin LTO),稍微更激进,可能改善运行时速度并减小二进制体积,同时增加编译时间。在 Cargo.toml 中使用 lto = "thin" 来启用。

第三种 LTO 形式是胖 LTO(fat LTO),更加激进,可能进一步改善性能并减小二进制体积(但并非总是如此),同时再次增加构建时间。在 Cargo.toml 中使用 lto = "fat" 来启用。

最后,可以完全禁用 LTO,这可能会降低运行时速度并增加二进制体积,但会减少编译时间。在 Cargo.toml 中使用 lto = "off"。注意,这与 lto = false 选项不同,如上所述,lto = false 仍保留瘦本地 LTO。

替代分配器

可以将 Rust 程序使用的默认(系统)堆分配器替换为替代分配器。确切效果取决于具体程序和所选的替代分配器,但实践中已观察到运行时速度的大幅改善和内存占用的大幅减少。效果也因平台而异,因为每个平台的系统分配器各有优劣。使用替代分配器也可能增加二进制体积和编译时间。

jemalloc

Linux 和 Mac 上一种流行的替代分配器是 jemalloc,可通过 tikv-jemallocator crate 使用。要使用它,在 Cargo.toml 文件中添加依赖:

1
2
[dependencies]
tikv-jemallocator = "0.5"

然后在 Rust 代码中添加以下内容,例如在 src/main.rs 顶部:

1
2
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

此外,在 Linux 上,jemalloc 可配置为使用透明大页(THP)。这可以进一步加速程序,但可能以增加内存占用为代价。

在构建程序之前,通过适当设置 MALLOC_CONF 环境变量(或 _RJEM_MALLOC_CONF)来实现,例如:

1
MALLOC_CONF="thp:always,metadata_thp:always" cargo build --release

运行编译后程序的系统也必须配置为支持 THP。更多详情见这篇博客文章。

mimalloc

另一种适用于许多平台的替代分配器是 mimalloc,可通过 mimalloc crate 使用。要使用它,在 Cargo.toml 文件中添加依赖:

1
2
[dependencies]
mimalloc = "0.1"

然后在 Rust 代码中添加以下内容,例如在 src/main.rs 顶部:

1
2
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

特定 CPU 指令

如果你不关心二进制在较旧(或其他类型)处理器上的兼容性,可以告诉编译器为特定 CPU 架构生成最新(可能最快)的指令,例如为 x86-64 CPU 生成 AVX SIMD 指令。

要从命令行请求这些指令,使用 -C target-cpu=native 标志。例如:

1
RUSTFLAGS="-C target-cpu=native" cargo build --release

或者,要从 config.toml 文件(对一个或多个项目)请求这些指令,添加以下行:

1
2
[build]
rustflags = ["-C", "target-cpu=native"]

这可以改善运行时速度,尤其是当编译器在代码中发现向量化机会时。

如果你不确定 -C target-cpu=native 是否工作正常,比较 rustc --print cfg 和 rustc --print cfg -C target-cpu=native 的输出,看后者是否正确检测到 CPU 特性。如果没有,可以使用 -C target-feature 来针对特定特性。

配置文件引导优化

配置文件引导优化(PGO)是一种编译模型:先编译程序,在示例数据上运行并收集性能分析数据,然后使用该数据引导程序的第二次编译。这可将运行时速度提高 10% 或更多。 示例 1, 示例 2。

这是一项需要一定设置工作的高级技术,但在某些情况下值得投入。详情见 rustc PGO 文档。此外,cargo-pgo 命令使使用 PGO(以及类似的 BOLT)优化 Rust 二进制文件更加容易。

遗憾的是,PGO 不支持托管在 crates.io 上并通过 cargo install 分发的二进制文件,这限制了其可用性。

最小化二进制体积

以下构建配置选项主要用于最小化二进制体积。它们对运行时速度的影响各不相同。

优化级别

你可以通过在 Cargo.toml 文件中添加以下行来请求以最小化二进制体积为目标的优化级别:

1
2
[profile.release]
opt-level = "z"

这也可能降低运行时速度。

另一种选择是 opt-level = "s",以稍不激进的方式针对最小二进制体积。与 opt-level = "z" 相比,它允许稍多内联以及循环向量化。

在 panic! 时中止

如果你不需要在 panic 时展开栈,例如因为程序不使用 catch_unwind,可以告诉编译器在 panic 时直接中止。panic 时程序仍会产生回溯。

这可能略微减小二进制体积并略微提高运行时速度,甚至可能略微减少编译时间。在 Cargo.toml 文件中添加以下行:

1
2
[profile.release]
panic = "abort"

剥离符号

你可以通过在 Cargo.toml 中添加以下行,告诉编译器从 release 构建中剥离符号:

1
2
[profile.release]
strip = "symbols"

示例。

然而,剥离符号可能使编译后的程序更难调试和分析。例如,如果剥离了符号的程序 panic,产生的回溯可能包含比正常情况更少的有用信息。确切效果取决于平台。

release 构建不需要剥离调试信息。默认情况下,本地 release 构建不生成调试信息,标准库的调试信息在 release 构建中自 Rust 1.77 起已自动剥离。

其他思路

有关更高级的二进制体积最小化技术,请参阅优秀的 min-sized-rust 仓库中的全面文档。

最小化编译时间

以下构建配置选项主要用于最小化编译时间。

链接

编译时间的很大一部分实际上是链接时间,尤其是在小幅修改后重新构建程序时。在某些平台上,可以选择比默认更快的链接器。

一种选择是 lld,在 Linux 和 Windows 上可用。lld 自 Rust 1.90 起在 Linux 上已成为默认链接器。在 Windows 上尚未成为默认,但对大多数用例应该有效。

要从命令行指定 lld,使用 -C link-arg=-fuse-ld=lld 标志。例如:

1
RUSTFLAGS="-C link-arg=-fuse-ld=lld" cargo build --release

或者,要从 config.toml 文件(对一个或多个项目)指定 lld,添加以下行:

1
2
[build]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

有一个 GitHub Issue 跟踪对 lld 的完整支持。

另一种选择是 mold,目前在 Linux 上可用。只需在上述说明中将 lld 替换为 mold。mold 通常比 lld 更快。 示例。 它也更新,可能在某些情况下无法工作。

最后一种选择是 wild,目前仅在 Linux 上可用。它可能比 mold 更快,但成熟度较低。

在 Mac 上,不需要替代链接器,因为系统链接器已经很快。

与本章其他选项不同,选择另一个链接器没有权衡。只要链接器对你的程序工作正常——除非你做一些不寻常的事情,否则这很可能是真的——替代链接器可以大幅加速而没有任何缺点。

禁用调试信息生成

尽管 release 构建提供最佳性能,许多人在开发时使用 dev 构建,因为它们构建更快。如果你使用 dev 构建但不常使用调试器,考虑禁用 debuginfo。这可以显著改善 dev 构建时间,最多可达 20–40%。 示例。

要禁用调试信息生成,在 Cargo.toml 文件中添加以下行:

1
2
[profile.dev]
debug = false

注意,这意味着堆栈跟踪将不包含行信息。如果你想保留行信息,但不需要调试器的完整信息,可以使用 debug = "line-tables-only",这仍能带来大部分编译时间收益。

实验性并行前端

如果你使用 nightly Rust,可以启用实验性的并行前端。它可能以减少编译时间为代价增加编译时内存占用。它不会影响生成代码的质量。

可以通过向 RUSTFLAGS 添加 -Zthreads=N 来实现,例如:

1
RUSTFLAGS="-Zthreads=8" cargo build --release

或者,要从 config.toml 文件(对一个或多个项目)启用并行前端,添加以下行:

1
2
[build]
rustflags = ["-Z", "threads=8"]

可以使用 8 以外的值,但 8 往往是效果最好的数字。

在最佳情况下,实验性并行前端可将编译时间减少最多 50%。但效果差异很大,取决于代码及其构建配置的特性,对某些程序则没有编译时间改善。

Cranelift 代码生成后端

如果你使用 nightly Rust,可以在某些平台上启用 Cranelift 代码生成后端。它可能以减少生成代码质量为代价减少编译时间,因此建议用于 dev 构建而非 release 构建。

首先,用此 rustup 命令安装后端:

1
rustup component add rustc-codegen-cranelift-preview --toolchain nightly

要从命令行选择 Cranelift,使用 -Zcodegen-backend=cranelift 标志。例如:

1
RUSTFLAGS="-Zcodegen-backend=cranelift" cargo +nightly build

或者,要从 config.toml 文件(对一个或多个项目)指定 Cranelift,添加以下行:

1
2
3
4
5
[unstable]
codegen-backend = true

[profile.dev]
codegen-backend = "cranelift"

更多信息请参阅 Cranelift 文档。

自定义 profile

除了 dev 和 release profile 外,Cargo 还支持自定义 profile。例如,如果你发现 dev 构建的运行时速度不足,而 release 构建的编译时间对日常开发来说太慢,创建一个介于 dev 和 release 之间的自定义 profile 可能很有用。

总结

构建配置方面有许多选择。以下要点将上述信息总结为一些建议。

  • 如果你想最大化运行时速度,考虑以下所有选项:codegen-units = 1、lto = "fat"、替代分配器以及 panic = "abort"。
  • 如果你想最小化二进制体积,考虑 opt-level = "z"、codegen-units = 1、lto = "fat"、panic = "abort" 以及 strip = "symbols"。
  • 无论哪种情况,如果不需要广泛的架构支持,考虑 -C target-cpu=native;如果与你的分发机制兼容,考虑 cargo-pgo。
  • 如果你在支持更快链接器的平台上,始终使用更快的链接器,因为这样做没有任何缺点。
  • 如果你需要额外帮助来做这些选择,使用 cargo-wizard。
  • 对所有更改逐一进行基准测试,确保它们产生预期效果。

最后,此 issue 跟踪 Rust 编译器自身构建配置的演变。Rust 编译器的构建系统比大多数 Rust 程序更奇特、更复杂。尽管如此,此 issue 可能有助于展示如何将构建配置选择应用于大型程序。

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