第13章 优化构建性能:Cargo配置与源码组织指南
Cargo 的配置选项和源代码组织方式可以改善构建性能,但这需要在构建性能与其他方面之间做出取舍——具体取决于哪些方面对你更重要。
和优化运行时性能一样,一定要针对你实际关心的工作流程来测量这些改动。我们提供的只是一般性建议,你的情况可能不同,某些做法在你的使用场景下反而可能让构建变慢。
值得考虑的典型工作流程包括:
- 开发过程中的编译反馈(改完代码后运行
cargo check) - 开发过程中的测试反馈(改完代码后运行
cargo test) - CI 构建
Cargo 和编译器配置
Cargo 的默认配置是在多个方面之间寻求平衡,包括可调试性、运行时性能、构建性能、二进制体积等。本节介绍几种修改默认配置的方法,目标是在构建性能上做到最优。
覆盖默认配置的常见位置有:
Cargo.toml清单文件- 对项目所有贡献者生效
- 支持的配置项有限(扩展支持见 #12738)
$WORKSPACE_ROOT/.cargo/config.toml配置文件- 对项目所有贡献者生效
- 与
Cargo.toml不同,它的生效情况与你在哪个目录下执行cargo有关(见 #2930)
$CARGO_HOME/.cargo/config.toml配置文件- 供开发者个人控制自己环境中的默认配置
减少生成的调试信息
建议:在 Cargo.toml 或 .cargo/config.toml 中添加:
[profile.dev]
debug = "line-tables-only"
[profile.dev.package."*"]
debug = false
[profile.debugging]
inherits = "dev"
debug = true
这样做会:
- 修改
dev配置文件(开发命令的默认配置):- 仅保留工作区成员生成有用 panic 回溯所需的最少 调试信息
- 避免为依赖项生成任何调试信息
- 提供可选入口,通过
--profile debugging开启调试模式
注意: 重新评估
dev配置文件的工作正在 #15931 中跟进。
权衡:
- ✅ 更快的代码生成速度(
cargo build) - ✅ 更短的链接时间
- ✅
target目录占用磁盘空间更小 - ❌ 需要完全重新编译才能获得高质量的调试体验
使用替代代码生成后端
建议:
- 安装 Cranelift 代码生成后端的 rustup 组件
$ rustup component add rustc-codegen-cranelift-preview --toolchain nightly - 在你的
Cargo.toml或.cargo/config.toml中添加:[profile.dev] codegen-backend = "cranelift" - 使用
-Z codegen-backend运行 Cargo,或在.cargo/config.toml中启用codegen-backend特性。- 这是必需的,因为该特性目前仍处于不稳定状态。
这将使 dev 配置文件 使用 Cranelift 代码生成后端 来生成机器代码,而非默认的 LLVM 后端。Cranelift 后端的代码生成速度应快于 LLVM,从而提升构建性能。
权衡:
- ✅ 更快的代码生成速度(
cargo build) - ❌ 必须使用 nightly Rust 和 不稳定的 Cargo 特性
- ❌ 生成代码的运行时性能较差
- 加速了
cargo test的构建部分,但可能增加其测试执行部分的时间
- 加速了
- ❌ 仅支持特定目标平台
- ❌ 可能不支持所有 Rust 特性(例如 unwinding)
启用实验性并行前端
建议:在你的 .cargo/config.toml 中添加以下内容:
[build]
rustflags = "-Zthreads=8"
这条 rustflags 会启用 Rust 编译器的并行前端,并指定使用 n 个线程。n 的值应根据系统可用的核心数来定,但收益会边际递减。我们建议最多使用 8 个线程。
权衡:
- ✅ 构建速度更快(包括
cargo check和cargo build) - ❌ 必须使用 nightly Rust 和一个不稳定的 Rust 特性
使用替代链接器
可以考虑安装并配置替代链接器,例如 LLD、mold 或 wild。以在 Linux 上配置 mold 为例,你可以在 .cargo/config.toml 中添加:
[target.'cfg(target_os = "linux")']
# mold, if you have GCC 12+
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# mold, otherwise
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=/path/to/mold"]
虽然依赖项可以并行构建,但所有依赖项的链接工作会在构建的最后一次性完成。这往往会导致链接过程占据构建时间的很大一部分,尤其是在增量重建时。通常,Rust 默认使用的链接器已经相当快,换用其他链接器的提升可能微乎其微;但也不尽然。例如,除了 x86_64-unknown-linux-gnu 之外的 Linux 目标平台仍在使用较慢的 Linux 系统链接器(详见 rust#39915)。
权衡:
- ✅ 链接时间更短
- ❌ 可能无法支持所有使用场景,特别是当你依赖 C 或 C++ 库时
为整个 workspace 解析 features
可以考虑在项目的 .cargo/config.toml 中添加:
[resolver]
feature-unification = "workspace"
调用 cargo 时,feature 的激活取决于你选中的 workspace 成员。但在开发一个应用时,你可能需要构建和测试其中的多个包,而不同包可能会为共同的依赖激活不同的 feature 集合,从而引发不必要的重新构建。使用 feature-unification 后,无论当前构建或测试的是哪个包,依赖都会激活相同的 feature 集合,从而复用更多已构建的依赖。
权衡:
- ✅ 在 workspace 中构建不同包时减少重新构建
- ❌ 需要 nightly 版 Rust 和不稳定的 Cargo 特性
- ❌ 某个包激活了某个 feature,可能掩盖其他本应激活它却没有激活的包中的 bug
- ❌ 如果
--workspace的 feature 统一对你不适用,那这个选项也不适用
减少构建的代码
移除未使用的依赖
建议:定期用以下命令检查并清理未使用的依赖:
$ cargo +nightly check -Zcargo-lints --workspace --all-targets
以下情况可能产生误报:
- 依赖是否被使用由
build.rs或RUSTFLAGS动态控制
此外,还应定期查看隐藏的 cargo::unused_dependencies 结果:
$ CARGO_LOG=cargo::diagnostics::rules::unused_dependencies=debug cargo +nightly check -Zcargo-lints --workspace --all-targets
该命令会显示以下情况下可能未使用的依赖:
- 来自 registry 和 git 的依赖
- 你的
package.rust-version过旧、无法使用[lints.cargo]时 - 你的依赖可能只是用来约束某个传递依赖的版本(这种情况下应改用
[target."cfg(false)".dependencies]) - 当你的依赖可能被用于激活传递性依赖上的某个特性时
- 你的
[dev-dependencies],因为目前尚无方法确保这些依赖的所有使用者都已构建
修改代码时,很容易忽略某个依赖已不再使用,进而将其移除。
权衡利弊:
- ✅ 加快完整构建和链接速度
- ❌ 检查未使用依赖时,需要 nightly Rust 版本及一个< a href="#lintscargo">不稳定的 Cargo 特性
- ❌ 需要从误报中甄别未使用的依赖,这需要额外精力
移除依赖中未使用的特性
建议:定期使用第三方工具(如cargo-features-manager 或cargo-unused-features)审查并移除依赖中未使用的特性。
修改代码时,很容易忽略依赖的某个特性已不再使用,进而将其移除。 这样做可以减少需要构建的传递性依赖数量, 或减少被构建 crate 内的代码量。 移除特性时需格外谨慎,因为特性还可能用于实现期望的行为或性能优化, 这些影响未必能通过编译或测试直观地发现。
权衡利弊:
- ✅ 加快完整构建和链接速度
- ❌ 可能会将某些特性误报为未使用