cargo build 背后其实是两个工具的分工:cargo 负责依赖解析、编译顺序调度和缓存判断,真正做编译的是 rustc——每次调用 rustc 只编译一个 crate。理解 rustc 内部的流水线,才能明白各类编译选项到底作用于哪一层。

编译逻辑链

rustc 采用多趟(multi-pass)流水线,源码逐级降阶(lowering)为越来越低级的中间表示(IR),最终交给 LLVM 生成机器码:

rustc 编译流水线:源代码经 AST、HIR、MIR、LLVM IR 逐级降阶,最终生成机器码并链接为二进制
图:rustc 各阶段及其主要工作。宏展开发生在 AST 阶段,借用检查和单态化发生在 MIR 阶段,LLVM 只拿到已经单态化完成的代码。

各阶段的关键工作:

  1. 解析与宏展开:词法、语法分析得到 AST。声明宏(macro_rules!)、过程宏(derive、attribute)在这一阶段展开,#[cfg] 条件编译也在此裁剪,后续阶段看到的只有展开后的代码。
  2. AST → HIR:语法脱糖——for 循环变回 loop + match? 运算符展开为 matchasync fn 展开为返回 impl Future 的普通函数。同时完成名称解析(每个标识符绑定到具体的定义)。
  3. 类型检查:基于 HIR 做类型推断和 trait 求解,绝大多数编译错误(类型不匹配、trait 未实现)在这里报出。
  4. HIR → MIR:降阶为控制流图形式的 MIR。借用检查、drop 细化(drop elaboration)、常量求值(const eval)以及泛型单态化都在 MIR 上进行;之后还会跑 MIR 级优化(内联、常量传播、死代码消除)。
  5. 代码生成:单态化后的 MIR 翻译为 LLVM IR,进入 LLVM 优化管道(函数内联、循环向量化、死代码消除等),再经指令选择、寄存器分配输出目标文件(.o)。
  6. 链接:调用平台链接器(或内置的 rust-lld),把所有 .o 与依赖的 rlib、系统库合并为可执行文件或动态库。

两点值得记住:

  • 单态化在 MIR 阶段完成:泛型函数对每个具体类型各生成一份代码,LLVM 看到的是完全具体化的函数。这是泛型"零成本"的来源,也是编译慢、二进制膨胀的来源。
  • 查询系统与增量编译:rustc 内部是按需计算的查询架构(如"求某函数的类型"“生成某函数的 MIR”),增量编译就是缓存这些查询结果,只重算受改动影响的部分。

观察中间产物

排查编译器行为时,可以让 rustc 吐出任意一层的中间表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 稳定版可用的输出
cargo rustc -- --emit mir # MIR 文本(.mir 文件)
cargo rustc -- --emit llvm-ir # LLVM IR(.ll 文件)
cargo rustc -- --emit asm # 汇编

rustc --emit=help # 列出所有可输出的产物

# nightly 专属:查看任一阶段的脱糖结果
cargo +nightly rustc -- -Zunpretty=hir
cargo +nightly rustc -- -Zunpretty=mir
cargo +nightly rustc -- -Zunpretty=expanded

# 只看宏展开结果(第三方 crate,日常最常用)
cargo install cargo-expand
cargo expand

-Zunpretty=expandedcargo expand 功能类似,都展示宏展开后的完整源码;后者对 cargo 项目更方便。

rustc 常用编译选项

输出控制

1
2
3
4
rustc --crate-type=rlib lib.rs   # rlib/dylib/cdylib/staticlib/bin
rustc --edition 2024 main.rs # 语言版本 2015/2018/2021/2024
rustc --target x86_64-unknown-linux-musl main.rs
rustc --emit=link,metadata main.rs

crate-type 的区别:rlib 是 Rust 专用静态库(含元数据,cargo 默认);staticlib/cdylib 是给 C/C++ 用的系统静态库/动态库;dylib 是 Rust 动态库(需同版本 rustc,几乎不用于分发)。

优化选项(-C)

1
2
3
4
5
6
7
rustc -C opt-level=3        # 0/1/2/3/s/z,s 和 z 优化体积
rustc -C debuginfo=2 # 0 无 / 1 行表 / 2 完整
rustc -C codegen-units=16 # 并行单元,越多编译越快、优化越差
rustc -C lto=fat # off/thin/fat,跨 crate 链接时优化
rustc -C target-cpu=native # 启用本机 CPU 指令集(AVX2 等)
rustc -C panic=abort # panic 直接终止,不展开栈
rustc -C strip=symbols # 剥离符号表,减小体积

codegen-units 影响的是 MIR 级并行:crate 被切成多个单元并行翻译和优化,release 默认 16。设为 1 时优化最充分但最慢,常与 lto=fat 搭配做发布构建。

查询选项(–print)

1
2
3
4
5
6
rustc --print cfg           # 当前目标生效的 cfg 键值
rustc --print target-list # 所有支持的 target triple
rustc --print target-cpus # target-cpu 可选值
rustc --print sysroot # 工具链根目录
rustc -C help # 全部 -C 选项
rustc -W help # 全部 lint

rustc --print cfg --target <triple> 可以查任意目标的 cfg,写 #[cfg(...)] 条件编译时很有用。

Cargo profile:日常配置的入口

实际项目很少直接调 rustc,上述 -C 选项大多通过 Cargo.toml 的 profile 配置:

1
2
3
4
5
6
7
8
[profile.release]
opt-level = 3 # 优化级别
debug = false # 调试信息,true 等价 debuginfo=2
lto = "thin" # 链接时优化
codegen-units = 1 # 充分优化
panic = "abort" # 去掉栈展开代码
strip = "symbols" # 发布时剥离符号
overflow-checks = false # 整数溢出检查,默认随 debug-assertions

补充说明:

  • [profile.dev] 默认 opt-level = 0 且开启增量编译(incremental = true);想让调试版依赖也优化(如测试、图像处理场景),可用 [profile.dev.package."*"] opt-level = 2
  • panic = "abort" 会使 catch_unwind 失效,FFI 场景(cdylib)反而推荐 abort,避免 panic 跨越 FFI 边界。
  • 可自定义 profile 并继承:[profile.release-dist] inherits = "release",配合 cargo build --profile release-dist 使用。

选项的传递途径与优先级

除了 profile,-C 选项还可以通过三条途径传给 rustc:

1
2
3
4
5
# 1. 环境变量
RUSTFLAGS="-C target-cpu=native" cargo build --release

# 2. 只传给顶层 crate,依赖不受影响
cargo rustc --release -- -C target-cpu=native
1
2
3
# 3. .cargo/config.toml
[build]
rustflags = ["-C", "target-cpu=native"]

优先级规则:target.<triple>.rustflags > RUSTFLAGS > build.rustflags,三者互斥,命中高优先级后低优先级整体失效。注意 RUSTFLAGS 对 workspace 里所有 crate(包括依赖)生效,改了它会触发全量重编。

交叉编译与链接器

1
2
rustup target add aarch64-unknown-linux-gnu
cargo build --target aarch64-unknown-linux-gnu

rustc 本身可以生成任意已安装 target 的目标文件,真正的瓶颈在链接器——需要告诉 cargo 用哪个交叉链接器:

1
2
3
# .cargo/config.toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

常见的两个实用场景:

1
2
3
4
5
6
7
# 静态链接 musl,生成无依赖的 Linux 二进制
rustup target add x86_64-unknown-linux-musl
cargo build --release \
--target x86_64-unknown-linux-musl

# 用内置 rust-lld 代替系统链接器(nightly 或部分 target 默认)
RUSTFLAGS="-C linker-flavor=gnu-lld-cc" cargo build

编译时间排查

1
2
3
4
5
6
7
8
9
# 查看每个 crate 的编译耗时分布
cargo build --release --timings

# 分析泛型单态化膨胀(第三方工具)
cargo install cargo-llvm-lines
cargo llvm-lines | head -20

# nightly:查看 rustc 各阶段耗时
cargo +nightly rustc -- -Ztime-passes

编译慢的典型原因和对应手段:codegen-units 过多导致优化不足(release 降到 1)、单态化膨胀(用 cargo llvm-lines 定位)、链接耗时(换 lld/mold 链接器)。dev 模式下的编译速度则主要靠增量编译和减少依赖数量保证。