加快 Rust 编译速度的技巧
Rust 构建太慢?
这里有一些加速编译时间的技巧。这份清单最初发布在我的个人博客上,但我决定更新它并迁移到这里。
所有技巧大致按影响程度排序,你可以从顶部开始逐条尝试。
通用技巧
更新 Rust 编译器和工具链
确保你使用最新的 Rust 版本:
rustup update让 Rust 编译器更快是一项持续进行的工作。多亏了他们的辛勤努力,今年以来编译器速度已全面提升了 30-40%,部分项目甚至获得了 45% 以上的改进。保持工具链最新是值得的。
用 cargo check 代替 cargo build
# 慢 🐢
cargo build
# 快 🐇(2 到 3 倍提速)
cargo check大多数时候,你根本不需要真正编译项目;你只是想知道自己是否在哪里搞错了。只要有可能,就完全跳过编译。你真正需要的是极快的代码 lint、类型检查和借用检查。
尽可能用 cargo check 代替 cargo build。它只会检查代码中的错误,而不会生成可执行二进制文件。
比较一下左侧 cargo check 与中间 cargo debug 的指令数量差异。(注意两者的刻度不同。)

我常用的一个妙招是用 cargo watch 在后台运行它。这样,每当你修改一个文件时,它就会自动执行 cargo check。
加分项:使用 cargo watch -c 可以在每次运行前清屏。
移除未使用的依赖
# 安装 cargo-machete 🔪️
cargo install cargo-machete && cargo machete
# 安装 cargo-shear ✂️🐑
cargo install cargo-shear
# 安装 cargo-udeps 🧼🧹️
cargo install cargo-udeps --locked重构之后,依赖有时会变得多余。时不时检查一下能否移除未使用的依赖会很有帮助。
上述工具会列出项目中所有未使用的依赖。每个工具都有其局限性,既可能产生误报也可能产生漏报。三个工具一起使用效果最佳。
Analyzing dependencies of crates in this directory...
cargo-machete found the following unused dependencies in <project>:
crate1 -- <project>/Cargo.toml:
clap
crate2 -- <project>/crate2/Cargo.toml:
anyhow
async-once-cell
dirs
log
tracing
url更多信息请见 cargo-machete、cargo-shear 和 cargo-udeps 的项目页面。
感谢读者 Nicholas Nethercote(尼古拉斯·内瑟科特)提到 cargo-shear 和 cargo-udeps,他是《Rust Performance Book(《Rust 性能之书》)》的作者,也是著名的《How to speed up the Rust compiler 系列》的作者。
更新依赖
- 运行
cargo update更新到最新的 semver 兼容版本。 - 运行
cargo outdated -wR查找更新的、可能不兼容的依赖。更新它们并按需修改代码。 - 运行
cargo tree --duplicate找出存在多个版本的依赖。目标是统一为单一版本,方法是更新那些依赖旧版本的依赖。(感谢 /u/dbdr 指出这一点。)
(操作说明来自 Reddit 上的 /u/oherrala。)
除此之外,还可以使用 cargo audit 来获知需要处理的漏洞或需要替换的已弃用 crate。
找出代码库中编译慢的 crate
cargo build --timings这会提供每个 crate 编译耗时的信息。

图中红线显示当前正在等待编译(且被其他 crate 阻塞)的单元(crate)数量。如果有大量 crate 被某个单一 crate 卡住瓶颈,就把注意力集中在优化那个 crate 上以提升并行度。
各颜色的含义:
- Waiting(等待)(红色)—— 正在等待 CPU 空闲槽位的 crate。
- Inactive(未激活)(蓝色)—— 正在等待其依赖完成的 crate。
- Active(激活)(绿色)—— 当前正在编译的 crate。
更多信息请见文档。
分析编译耗时
如果你想比 cargo --timings 挖得更深,可以用 cargo rustc -- -Zself-profile 对 Rust 编译过程进行性能剖析。生成的 trace 文件可以用火焰图或 Chromium 分析器来可视化:

另一个绝佳的工具是 cargo-llvm-lines,它会显示最终二进制文件中生成的行数以及每个泛型函数被复制的次数。这可以帮助你找出哪些函数的编译开销最大。
$ cargo llvm-lines | head -20
Lines Copies Function name
----- ------ -------------
30737 (100%) 1107 (100%) (TOTAL)
1395 (4.5%) 83 (7.5%) core::ptr::drop_in_place
760 (2.5%) 2 (0.2%) alloc::slice::merge_sort
734 (2.4%) 2 (0.2%) alloc::raw_vec::RawVec<T,A>::reserve_internal
666 (2.2%) 1 (0.1%) cargo_llvm_lines::count_lines
490 (1.6%) 1 (0.1%) <std::process::Command as cargo_llvm_lines::PipeTo>::pipe_to
476 (1.5%) 6 (0.5%) core::result::Result<T,E>::map
440 (1.4%) 1 (0.1%) cargo_llvm_lines::read_llvm_ir
422 (1.4%) 2 (0.2%) alloc::slice::merge
399 (1.3%) 4 (0.4%) alloc::vec::Vec<T>::extend_desugared
388 (1.3%) 2 (0.2%) alloc::slice::insert_head
366 (1.2%) 5 (0.5%) core::option::Option<T>::map
304 (1.0%) 6 (0.5%) alloc::alloc::box_free
296 (1.0%) 4 (0.4%) core::result::Result<T,E>::map_err
295 (1.0%) 1 (0.1%) cargo_llvm_lines::wrap_args
291 (0.9%) 1 (0.1%) core::char::methods::<impl char>::encode_utf8
286 (0.9%) 1 (0.1%) cargo_llvm_lines::run_cargo_rustc
284 (0.9%) 4 (0.4%) core::option::Option<T>::ok_or_else排查缓慢的增量构建
如果你的增量构建比预期慢,可能是被 rustc 后端中的某个特定 crate 卡住了瓶颈。
要诊断这个问题,可以使用 samply 配合 -Zhuman_readable_cgu_names=yes 标志对编译器进行性能剖析,找出哪些 codegen unit(代码生成单元)编译耗时最长:
samply record cargo build这能帮你判断某个特定 crate 是否是构建中最耗时的环节。一旦找到瓶颈,你或许可以通过重构或拆分问题 crate 来缩短编译时间。
关于 Rust 编译器后端并行化的更多细节,请参阅 Nicholas Nethercote 的博客文章。
另一个有用的标志是 -Zprint-mono-items=yes,它会在编译期间打印所有单态化条目。这有助于你了解每个 CGU 中生成了什么内容。
RUSTFLAGS="-Zprint-mono-items=yes" cargo +nightly build ⏎感谢 Caspar(Reddit 上的 asparck)提供这条技巧!
替换重量级依赖
时不时地为流行的 crate 寻找更轻量的替代品是很有帮助的。
同样,cargo tree 在这里是你的好帮手,它能帮你了解哪些依赖相当重:它们需要许多其他 crate,造成过多的网络 I/O 并拖慢你的构建。然后去寻找更轻量的替代品。
此外,cargo-bloat 有一个 --time 标志,可以显示每个 crate 的构建时间。非常实用!
举几个例子:
| Crate | 替代品 |
|---|---|
| serde | miniserde、nanoserde |
| reqwest | ureq |
| clap | lexopt |
有一个例子中,更换 crate 后编译时间从 2 分 22 秒降到了 26 秒。
使用工作区把大 crate 拆分成小 crate
Cargo 有一个叫工作区(workspace)的巧妙功能,允许你把一个大 crate 拆分为多个小 crate。这种代码拆分非常适合避免重复编译,因为只有发生变更的 crate 才需要重新编译。servo 和 vector 这类大型项目大量使用工作区来减少编译时间。
禁用 crate 依赖中未使用的特性
cargo-features-manager 是一个较新的工具,可帮助你禁用依赖中未使用的特性。
cargo install cargo-features-manager
cargo features prune定期检查依赖的特性标志。许多库维护者都花心思把自己的 crate 拆分成可以按需关闭的独立特性。也许你并不需要每个 crate 的全部默认功能?
例如,tokio 有大量特性,如果不需要就可以禁用。
另一个例子是 bindgen,它在作为二进制程序使用时默认启用 clap 支持。而对于库的使用场景——这也是最常见的用法——并不需要这个功能。禁用该特性后,rust-rocksdb 的编译时间在 debug 和 release 构建下分别缩短了约 13 秒和约 9 秒。感谢读者 Lilian Anatolie Moraru 提到这一点。
郑重提醒
关闭特性并不总能缩短编译时间。(参见 tikv 的经验。)不过,通过缩小代码的攻击面来提升安全性,这样做仍然是个好主意。此外,禁用特性还有助于精简依赖树。
使用 cargo add 安装 crate 时,你会看到它的特性列表。
如果你想查询某个 crate 的特性标志,可以在 docs.rs 上查看。例如,看看 tokio 的特性标志。
移除未使用的特性后,检查一下 Cargo.lock 文件的 diff,就能看到所有被清理掉的多余依赖。
为高开销代码添加特性
[features]
# Basic feature for default functionality
default = []
# Optional feature for JSON support
json = ["serde_json"]
# Another optional feature for more expensive or complex code
complex_feature = ["some-expensive-crate"]项目中的代码并非每一部分的编译开销都相同。你可以利用 Cargo 特性,以比 crate 更细粒度的方式把代码拆分成更小的块。这样,你只需编译自己需要的功能。
这是库开发中的常见做法。例如,serde 有一个名为 derive 的特性,用于启用序列化和反序列化的代码生成。它并非总是必需的,所以默认是禁用的。类似地,Tokio 和 reqwest 也有大量可以启用或禁用的特性。
你也可以在自己的代码中这样做。在上面的例子中,Cargo.toml 里的 json 特性启用 JSON 支持,而 complex_feature 特性则启用另一条高开销的代码路径。
找出重新构建的根本原因
有时即使代码没有任何改动,很多 crate 也会重新构建,这通常是由于不同构建进程之间的环境变量差异造成的(比如你的 Makefile 构建 vs rust-analyzer vs CI 构建)。
使用 cargo 的指纹日志来准确查明触发重新构建的原因:
export CARGO_LOG="cargo::core::compiler::fingerprint=info"
export RUST_LOG=trace
cargo build -vv查找显示重新构建原因的日志行。例如:
INFO prepare_target: cargo::core::compiler::fingerprint: dirty: EnvVarChanged { name: "VIRTUAL_ENV", old_value: None, new_value: Some("/path/to/.venv") }
Dirty pyo3-build-config v0.24.1: the env variable VIRTUAL_ENV changed看到那行 “Dirty” 了吗?它会确切地告诉你是什么导致了重新构建。你可以 grep 该行来找出所有的重建原因。
常见的罪魁祸首包括:
- 环境变量(CC、CXX、VIRTUAL_ENV、PATH 的变化)
- 不同构建工具之间的特性标志不匹配
- 使用了不同的 cargo profile
- 生成文件的时间戳差异
找到原因后,确保所有构建进程之间保持一致。对于 VS Code 中的 rust-analyzer,你可以在 .vscode/settings.json 中配置匹配的环境变量:
{
"rust-analyzer.check.extraEnv": {
"CC": "clang",
"CXX": "clang++",
"VIRTUAL_ENV": "/path/to/your/.venv"
},
"rust-analyzer.cargo.features": "all"
}当不同工具使用不一致的环境时,这种调试方法可以大幅减少不必要的重新构建。
致谢:该技术出自 Ishan Bhanuka 的这篇文章。
用 sccache 缓存依赖
另一个不错的项目是 Mozilla 的 sccache,它缓存已编译的 crate 以避免重复编译。
我在笔记本上运行过一段时间,但说实话收益微乎其微。如果你同时开发许多共享依赖(且版本相同)的独立项目,它的效果最好。一个常见场景是共享构建服务器。
Cranelift:Rust 的替代编译器
你知道吗?Rust 项目正在使用一个替代编译器,它在每次 CI 构建时与 rustc 并行运行。
rustc_codegen_cranelift,也称为 CG_CLIF,是基于 Cranelift 编译器框架的 Rust 编译器实验性后端。
下面是 rustc 与 Cranelift 在一些流行 crate 上的对比(蓝色表示更好):

该编译器生成的可执行二进制文件完全可以运行。虽然优化程度不如前者,但对本地开发来说非常好用。
更详细的介绍见 Jason Williams 的页面,项目代码在 Github 上。
换用更快的链接器
什么是链接器?
链接器(linker)是一种将多个目标文件合并成单个可执行文件的工具。
它是编译过程的最后一步。
你可以通过以下命令检查链接器是否是瓶颈:
cargo clean
cargo +nightly rustc --bin <your_binary_name> -- -Z time-passes它会输出每个步骤的耗时,包括链接时间:
...
time: 0.000 llvm_dump_timing_file
time: 0.001 serialize_work_products
time: 0.002 incr_comp_finalize_session_directory
time: 0.004 link_binary_check_files_are_writeable
time: 0.614 run_linker
time: 0.000 link_binary_remove_temps
time: 0.620 link_binary
time: 0.622 link_crate
time: 0.757 link
time: 3.836 total
Finished dev [unoptimized + debuginfo] target(s) in 42.75s如果 link 步骤很慢,可以尝试换用更快的替代方案:
| 链接器 | 平台 | 可用于生产环境 | 描述 |
|---|---|---|---|
lld | Linux/macOS | 是 | 系统链接器的即插即用替代品 |
mold | Linux | 是 | 针对 Linux 优化 |
zld | macOS | 否(已弃用) | Apple ld 链接器的即插即用替代品 |
仅限 macOS:更快的增量 debug 构建
Rust 1.51 增加了一个可在 macOS 上加速增量 debug 构建的标志。它可以让 debug 构建快好几秒(取决于你的使用场景)。有工程师报告称,仅这一个标志就能将 macOS 上的编译时间缩短 70%。
把它加到你的 Cargo.toml 里:
[profile.dev]
split-debuginfo = "unpacked"这个标志可能很快就会成为 macOS 上的标准配置。它已经是nightly 版本的默认值了。
仅限 macOS:将 Rust 编译排除在 Gatekeeper 之外
Gatekeeper 是 macOS 上的一个系统机制,会对二进制文件执行安全检查。这可能使每次 Rust 构建变慢几秒。解决办法是把你的终端加入开发者工具(Developer Tools),这样由它启动的进程就会被排除在 Gatekeeper 之外。
- 在终端中运行
sudo spctl developer-mode enable-terminal。 - 打开系统偏好设置,然后进入安全性与隐私。
- 在“隐私”标签页下,前往
Developer Tools。 - 确保你的终端已被列出并启用。如果你使用 iTerm 或 Ghostty 等第三方终端,也要把它们加入列表。
- 重启你的终端。

仅限 Windows:为 Rust 设置 Dev Drive
Windows 11 包含 Dev Drive,这是一种专为开发优化的文件系统。据微软称,使用 Dev Drive 可以获得大约 20-30% 的速度提升:

为了提升 Rust 编译速度,可以把以下内容移到 Dev Drive 上:
- Rust 工具链目录(
CARGO_HOME) - 你的项目代码
- Cargo 的
target目录
你还可以更进一步,把上述文件夹也加入杀毒软件的排除项,以获得额外的潜在提速。排除设置位于 Windows 安全中心的“病毒和威胁防护”设置中。

感谢 nextest 团队提供这条技巧。
调整 Codegen 选项和编译器标志
Rust 自带一整套代码生成设置。浏览一下列表并为你的项目调整参数会有帮助。
完整的 codegen 选项列表中有许多宝藏。作为参考,这里有 bevy 用于加速编译的配置。
避免过程宏 crate
如果你的项目大量使用过程宏(例如使用了 serde),那么在 Cargo.toml 中调整 opt-level 可能值得一试。
[profile.dev.build-override]
opt-level = 3正如读者 jfmontanaro 在 Github 上所说:
我认为它之所以有助于缩短构建时间,是因为它只作用于构建脚本和 proc-macro。构建脚本和 proc-macro 很特殊,因为在正常构建过程中,它们不仅会被编译,还会被执行(proc-macro 还可能被反复执行)。当你的项目使用大量 proc-macro 时,优化宏本身理论上可以节省大量时间。
另一种方法是用 watt 绕开宏对编译时间的影响,这是一个把宏编译转移到 WebAssembly 上的工具。
摘自文档:
通过提前把宏编译成 Wasm,我们让所有下游用户无需自行编译宏逻辑或其依赖。
相反,他们要编译的是一个小巧自包含的 Wasm 运行时(约 3 秒,所有宏共享),以及每个宏 crate 一个微小的 proc macro shim,用于把 Wasm 字节码交给 Watt 运行时(每个所依赖的 proc-macro crate 约 0.3 秒)。这远低于编译复杂的过程宏及其依赖所需的 20 多秒。
请注意,这个 crate 仍处于实验阶段。
找出开销大的过程宏
RUSTFLAGS="-Zmacro-stats" cargo +nightly build有些宏有很大的编译期开销;但到底有多大?量化这些成本有助于判断一个宏是否值得优化(或移除)。一种方法是弄清楚它们究竟生成了多少代码。
一种做法是使用 cargo expand 查看生成的代码,但这无法扩展到大型代码库,而且输出难以量化。
另一种选择是使用 -Zmacro-stats 标志来识别生成大量代码的过程宏。这个工具已经在 Bevy 和 Arbitrary 等项目中带来了成功的优化。
欲了解更多信息,请阅读 Nicholas Nethercote 关于该主题的博客文章。
过程宏的条件编译
过程宏需要解析 Rust 代码,这是一项相对复杂的任务。依赖过程宏的 crate 必须等过程宏编译完成后才能开始编译。例如,serde 可能成为编译时间的瓶颈并限制 CPU 利用率。
为了提升 Rust 编译时间,可以考虑采用策略性的方式处理 Serde 序列化,尤其是在具有共享 crate 结构的项目中。与其把 Serde 直接放在项目各部分共用的共享 crate 中,不如通过 Cargo 特性把 Serde 设为可选依赖。
使用 cfg 或 cfg_attr 属性,让共享 crate 中的 Serde 用法和 derive 受特性开关控制。这样,它就成为一个可选依赖,只在真正执行序列化/反序列化的叶子 crate 中启用。
这种方式可以避免整个项目都在等待 Serde 依赖的编译——如果 Serde 是共享 crate 的非可选直接依赖,就会出现这种情况。
下面用一个简化的例子来说明。假设你有一个 Rust 项目,其中包含一个共享库 crate 和几个依赖它的其他 crate。你不希望在构建不需要 Serde 的项目部分时不必要地编译 Serde。
以下是如何组织你的项目以使用 Cargo 可选特性的方法:
在共享 crate 的 Cargo.toml 中,把 serde 声明为可选依赖:
[package]
name = "shared"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", optional = true }在这个 crate 中,使用条件编译,只在特性启用时引入 serde:
#[cfg(feature = "serde")]
use serde::{Serialize, Deserialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MySharedStruct {
// Your struct fields
}在其他 crate 中,如有需要可为共享 crate 启用 serde 特性:
[package]
name = "other"
version = "0.1.0"
edition = "2021"
[dependencies]
shared = { path = "../shared", features = ["serde"] }现在你可以在启用 Serde 功能的情况下使用 MySharedStruct,同时又不会拖累那些不需要它的 crate 的编译。
泛型:使用内部非泛型函数
如果你有一个泛型函数,它会为你使用的每种类型分别编译一份。当你有很多不同的类型时,这就成了问题。
常见的解决方案是使用一个内部非泛型函数。这样,编译器只需编译一次内部函数。
这是标准库中经常使用的技巧。例如,以下是 read_to_string 的实现:
pub fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
fn inner(path: &Path) -> io::Result<String> {
let mut file = File::open(path)?;
let size = file.metadata().map(|m| m.len() as usize).ok();
let mut string = String::with_capacity(size.unwrap_or(0));
io::default_read_to_string(&mut file, &mut string, size)?;
Ok(string)
}
inner(path.as_ref())
}你可以在自己的代码中做同样的事:外层函数是泛型的,而它调用的内层非泛型函数完成实际工作。
用 cargo-hakari 改善工作区构建时间
你是否有一个大型 Rust 工作区,其中的依赖满足以下条件:
- 被多个 crate 使用
- 在这些 crate 中具有不同的特性集?
这种情况会导致漫长的构建时间,因为 cargo 会根据当前构建的是哪个 crate,用不同特性多次构建同一个依赖。这正是 cargo-hakari 的用武之地。它是一个旨在自动管理“workspace-hack” crate 的工具。
在某些场景下,它可以将连续构建时间缩短多达 50% 或更多。想了解更多,请查阅官方 cargo-hakari 文档中的使用说明和基准测试。
用动态库加速 Rust 增量编译
# Install the tool
cargo install cargo-add-dynamic
# Add a dynamic library to your project
cargo add-dynamic polars --features csv-file,lazy,list,describe,rows,fmt,strings,temporal这会围绕 polars 创建一个包装 crate,并将其编译为动态库(Linux 上是 .so,macOS 上是 .dylib,Windows 上是 .dll)。
本质上,它会用如下配置修补该依赖:
[lib]
crate-type = ["dylib"]借助这一技巧,当你只修改自己的代码时,可以省去依赖的链接时间。只有在你更改特性或版本时,依赖本身才会重新编译。当然,这对任何 crate 都适用,不只是 polars。
更多内容请阅读 Robert Krahn 的这篇博文以及该工具的主页。
切换到新的并行编译器前端
在 nightly 版本中,你现在可以启用新的并行编译器前端。要试用它,请带 -Z threads=8 选项运行 nightly 编译器:
RUSTFLAGS="-Z threads=8" cargo +nightly build如果你发现它对你效果不错,可以通过在 ~/.cargo/config.toml 文件中加入 -Z threads=8 将其设为默认:
[build]
rustflags = ["-Z", "threads=8"]或者,你可以在 shell 配置文件(如 ~/.bashrc 或 ~/.zshrc)中为 cargo 设置别名:
alias cargo="RUSTFLAGS='-Z threads=8' cargo +nightly"当前端在使用 -Z threads=8 的多线程设置下运行时,基于真实代码的基准测试表明编译时间最多可降低 50%。不过收益会因所编译的代码而异。但无论如何,值得一试。
下面是并行编译器前端实际运行的可视化:

更多信息请见 Rust 官方博客上的公告。
使用临时磁盘加速构建
你的文件系统可能就是瓶颈。考虑为构建目录使用内存文件系统之类的方案。
传统的临时文件系统(如 tmpfs)受限于内存加交换空间的大小,对于会产生大型中间产物的构建可能会有问题。
相反,在 Linux 上,可以用以下选项挂载一个 ext4 卷:
-o noauto_da_alloc,data=writeback,lazytime,journal_async_commit,commit=999,nobarrier如果你的内存足够,文件会先存放在页缓存中,稍后再写回。请把它当作临时文件系统对待,因为在崩溃或断电后数据可能会丢失或损坏。
投资更好的硬件
如果你已经看到这里,那么进一步提升编译时间最简单的办法大概就是花钱买顶级硬件了。
就笔记本电脑而言,Apple 新款 Macbook 的 M 系列芯片在 Rust 编译方面表现出色。
搭载 M1 Max 的 Macbook Pro 的基准测试结果简直离谱——即使是与本来就已经很快的 M1 相比也是如此:
| 项目 | M1 Max | M1 Air |
|---|---|---|
| Deno | 6m11s | 11m15s |
| MeiliSearch | 1m28s | 3m36s |
| bat | 43s | 1m23s |
| hyperfine | 23s | 42s |
| ripgrep | 16s | 37s |
性能整整提升了 2 倍。
不过如果你更喜欢留在 Linux 阵营,人们用 AMD Ryzen Threadripper 加 32 GB 内存这样的多核 CPU 也取得了很好的成绩。
在便携设备上,编译会消耗电池电量而且速度慢。为了避免这一点,我把家里的一台 6 核 AMD FX 6300、12GB 内存的机器用作构建机。我可以将它与 Visual Studio Code Remote Development 结合使用。
在云端编译
如果你没有专用机器,也可以把编译过程卸载到云端。GitHub Codespaces 直接在 GitHub 内为你提供一个完整配置好的云端开发环境。点击任意仓库的 Code 按钮并选择 Open with Codespaces 即可启动一个 codespace。机器规格从 2 核一直到 32 核不等,而且每个 GitHub 账户都包含每月免费额度(目前是 2 核机器 60 小时)。
Codespaces 支持通过 .devcontainer/devcontainer.json 文件进行 dev container 配置,让你预先安装 Rust 工具链和其他依赖,环境一启动即可使用。这在审查 pull request 时特别有用——直接在 codespace 中打开 PR,一切都已经为你准备好了。
如果你想对构建环境有更多控制权,Northflank 是另一个选择。它可以让你在专用云基础设施上运行任意构建任务,套餐从共享 0.1 vCPU 容器到 32 个专用 vCPU 不等。有免费的沙箱档位,付费方案按秒计费,因此你只需为实际构建时间付费。
在本地缓存所有 crate
如果你的网络连接很慢,初始构建过程中很大一部分时间是从 crates.io 抓取那些闪亮的 crate。为了缓解这个问题,你可以提前下载所有 crate 并在本地缓存。criner 正是干这个的:
git clone https://github.com/the-lean-crate/criner
cd criner
cargo run --release -- mine归档大小出奇地合理,目前大约需要 50GB 磁盘空间。
测试执行
用 Cargo Nextest 代替 cargo test
cargo install cargo-nextest
cargo nextest runcargo 自带一个小型测试运行器固然很好,但尤其是当你需要构建多个测试二进制文件时,得益于其并行执行模型,cargo nextest 可以比 cargo test 快达 60%。以下是一些快速的基准测试:
| 项目 | 修订版本 | 测试数量 | cargo test(秒) | nextest(秒) | 提升 |
|---|---|---|---|---|---|
| crucible | cb228c2b | 483 | 5.14 | 1.52 | 3.38× |
| guppy | 2cc51b41 | 271 | 6.42 | 2.80 | 2.29× |
| mdBook | 0079184c | 199 | 3.85 | 1.66 | 2.31× |
| meilisearch | bfb1f927 | 721 | 57.04 | 28.99 | 1.96× |
| omicron | e7949cd1 | 619 | 444.08 | 202.50 | 2.19× |
| penumbra | 4ecd94cc | 144 | 125.38 | 90.96 | 1.37× |
| reqwest | 3459b894 | 113 | 5.57 | 2.26 | 2.48× |
| ring | 450ada28 | 179 | 13.12 | 9.40 | 1.39× |
| tokio | 1f50c571 | 1138 | 24.27 | 11.60 | 2.09× |
把所有集成测试合并到单个二进制文件中
你有集成测试吗?(就是 tests 目录里的那些。)你知道吗?Rust 编译器会为每一个集成测试创建一个独立的二进制文件,而每个二进制文件都必须单独链接。这可能会占用大部分构建时间,因为链接真的太太太慢了。🐢 原因在于许多系统链接器(如 ld)是单线程的。
为了让链接器的工作轻松一点,你可以把所有测试放进一个 crate。(基本上就是在测试目录里创建一个 main.rs,并把测试文件作为 mod 加进去。)
这样链接器就只需构建单个二进制文件。听起来不错,但要小心:这仍是一种权衡,因为你需要暴露内部的类型和函数(也就是把它们设为 pub)。
如果你有大量集成测试,这可以带来 50% 的提速。
这条技巧来自 Luca Palmieri、Lucio Franco 和 Azriel Hoh。谢谢!
把慢测试放到环境变量后面
#[test]
fn completion_works_with_real_standard_library() {
if std::env::var("RUN_SLOW_TESTS").is_err() {
return;
}
...
}如果你有一些很慢的测试,可以把它们放到一个环境变量后面,使其默认禁用。这样,你可以在本地跳过它们,只在 CI 上运行。
(这是我从 matklad(Alex Kladov)的文章学来的一个好技巧。)
CI 构建
CI 构建技巧
本文中的许多技巧同样适用于 CI 构建。关于 CI 特定的优化和最佳实践,请查看我的专门指南《Tips for Faster CI Builds》,其中涵盖了缓存策略、工作流优化以及 GitHub Actions 相关的改进。
为依赖使用缓存
特别是对于 GitHub Actions,你还可以使用 Swatinem/rust-cache。
只需在工作流中添加一个步骤即可:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --all这样一来,你的依赖会在各次构建之间被缓存,可以期待显著的提速。
拆分编译和测试步骤
- name: Compile
run: cargo test --no-run --locked
- name: Test
run: cargo test -- --nocapture --quiet这样可以更容易地看出有多少时间花在编译上、多少时间花在运行测试上。
在 CI 中禁用增量编译
env:
CARGO_INCREMENTAL: 0由于 CI 构建更接近从零开始的构建,增量编译会增加不必要的依赖跟踪和 IO 开销,降低缓存的有效性。这里是如何禁用它。
关闭 Debug 信息
[profile.dev]
debug = 0
strip = "debuginfo"避免链接 debug 信息可以加快构建过程,尤其是当你很少使用真正的调试器时。有两种避免链接 debug 信息的方法:设置 debug=0 跳过编译它,或设置 strip="debuginfo" 跳过链接它。遗憾的是,更改这些选项可能会触发 Cargo 的全量重新构建。
- 在 Linux 上,两者都设置以获得更好的构建时间。
- 在 Mac 上,使用
debug=0,因为 rustc 使用外部 strip 命令。 - 在 Windows 上,两种设置都测试一下,看哪个更快。
注意,没有 debug 信息时,回溯信息只会显示函数名,不会显示行号。如有需要,可以使用 split-debuginfo="unpacked" 作为折中方案。
作为一个不错的副作用,这也有助于缩小 ./target 的体积,提高缓存效率。
这里有一个示例配置展示如何应用这些设置。
通过环境变量拒绝警告
避免在代码中使用 #![deny(warnings)],以免重复声明。此外,本地开发时出现警告也没关系。
相反,应在 RUSTFLAGS 中加入 -D warnings,以便在 CI 上全局拒绝所有 crate 的警告。
env:
RUSTFLAGS: -D warnings换用更快的 GitHub Actions 运行器
- runs-on: ubuntu-latest
+ runs-on: ubicloudUbicloud、BuildJet 或 RunsOn 等服务可以为你的 GitHub Actions 构建提供更快的 worker。尤其对于 Rust 流水线,核心数量会对编译时间产生显著影响,所以值得一试。
下面是 Facebook Folly 项目使用 Ubicloud 的例子。诚然,这是一个 C++ 项目,但它展示了更快运行器的潜力:
注册该服务后,你只需要更改 GitHub Actions 工作流文件中的 runner。
更快的 Docker 构建
用 cargo-chef 加速 Docker 构建
要从 Rust 代码构建 Docker 镜像?这类构建出了名地慢,因为 cargo 尚不支持只构建项目的依赖,一不小心 Docker 缓存就会在每次构建时失效。cargo-chef 来救场!⚡
cargo-chef可以用来充分利用 Docker 层缓存,从而大幅加速 Rust 项目的 Docker 构建。在我们的商业代码库(约 1.4 万行代码、约 500 个依赖)上,我们测得了 5 倍提速:我们把 Docker 构建时间从约 10 分钟缩短到了约 2 分钟。
如果你感兴趣,这里是一个示例 Dockerfile:
# Step 1: Compute a recipe file
FROM rust as planner
WORKDIR app
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
# Step 2: Cache project dependencies
FROM rust as cacher
WORKDIR app
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
# Step 3: Build the binary
FROM rust as builder
WORKDIR app
COPY . .
# Copy over the cached dependencies from above
COPY --from=cacher /app/target target
COPY --from=cacher /usr/local/cargo /usr/local/cargo
RUN cargo build --release --bin app
# Step 4:
# Create a tiny output image.
# It only contains our final binary.
FROM rust as runtime
WORKDIR app
COPY --from=builder /app/target/release/app /usr/local/bin
ENTRYPOINT ["/usr/local/bin/app"]cargo-chef 可以帮助加速你使用 GitHub Actions 的持续集成或部署到 Google Cloud 的流程。
考虑用 Earthly 获得更好的构建缓存
Earthly 是一个较新的构建工具,旨在取代 Makefile、Dockerfile 及其他构建工具。它为 CI 提供快速的增量 Rust 构建。
Earthly 通过有效实现 Cargo 的缓存和 Rust 的增量编译来加速 CI 中的 Rust 构建。这种方法显著减少了 CI 中不必要的重新构建,达到与本地 Rust 构建相当的效率。
他们使用一套称为 Satellites 的系统,这是保留本地缓存数据的持久远程构建运行器。通过消除缓存的上传和下载,这可以大幅缩短 CI 构建时间。他们不是把缓存数据搬到计算节点,而是让缓存数据和计算节点同址部署,从而彻底消除缓存传输。更少的 I/O 意味着更快的构建。
Earthly 还提供了一个 lib/rust 库,它把缓存配置完全抽象掉了。它确保 Rust 在 CI 中正确缓存并进行增量构建。可以这样在你的 Earthfile 中使用:
IMPORT github.com/earthly/lib/rust如果你好奇的话,Earthly 的 Rust 指南详细介绍了一个带有优化缓存和编译步骤的简单 Rust 示例。
IDE 相关优化
如果你发现开发环境中的构建时间很慢,这里还有一些额外的技巧可以尝试。
Visual Studio Code 中缓慢的调试会话
如果你在使用 Visual Studio Code 时发现调试会话很慢,请确保没有设置太多断点。每个断点都可能拖慢调试会话。
关闭无关的项目
如果你在 Visual Studio Code 中打开了多个项目,每个实例都会运行自己的 rust-analyzer 副本。这可能会拖慢你的机器。如果不需要,请关闭无关的项目。
修复 Rust Analyzer 缓存失效问题
如果你在 VS Code 中使用 rust-analyzer,发现保存更改后构建时间变长,可能是缓存失效了。这也会导致像 serde 这样的依赖被频繁重新构建。
你可以通过为 rust-analyzer 配置单独的目标目录来解决这个问题。把下面的内容加到 VS Code 设置中(最好是用户设置):
{
"rust-analyzer.cargo.targetDir": true
}这会让 rust-analyzer 在 target/rust-analyzer 内而不是默认的 target/ 目录中进行构建,从而避免干扰你正常的 cargo run 构建。
一些用户报告因此获得了显著的提速:
before: 34.98s user 2.02s system 122% cpu 30.176 total
after: 2.62s user 0.60s system 84% cpu 3.803 total这也有助于解决 rust analyzer 阻塞 debug 构建的问题。
致谢:这条技巧由 Reddit 上的 asparck 分享。
总结
在这篇文章中,我们涵盖了很多内容。我们探讨了如何通过更好的硬件、优化代码和使用更好的工具来加速 Rust 构建。
关于 CI 特定的优化,别忘了查看《Tips for Faster CI Builds》,它与本文讨论的技术相辅相成。
我希望你能运用其中的一些技巧来加速你的 Rust 构建。如果你发现了其他加速 Rust 构建的方法,或者有任何问题或反馈,我很乐意听到你的声音。
更多资源
Tips for faster CI builds —— 我收集的加速 CI 构建的技巧。
The Rust Perf Book 有专门讲编译时间的章节。
8 Solutions for Troubleshooting Your Rust Build Times 是 Dotan Nahum 写的一篇好文章,我完全赞同。
将一个较大的 Rust 项目(lemmy)的构建时间改善了 30%。
arewefastyet(已下线)测量 Rust 编译器编译常见 Rust 程序所需的时间。
Speeding up the Rust edit-build-run cycle:一种以基准测试为导向的改善 Rust 编译时间的方法。
随机一篇博客
