Rust 编译基础:cargo 与 rustc 的分工及编译选项
cargo build 背后其实是两个工具的分工:cargo 负责依赖解析、编译顺序调度和缓存判断,真正做编译的是 rustc——每次调用 rustc 只编译一个 crate。理解 rustc 内部的流水线,才能明白各类编译选项到底作用于哪一层。
编译逻辑链
rustc 采用多趟(multi-pass)流水线,源码逐级降阶(lowering)为越来越低级的中间表示(IR),最终交给 LLVM 生成机器码:
图:rustc 各阶段及其主要工作。宏展开发生在 AST 阶段,借用检查和单态化发生在 MIR 阶段,LLVM 只拿到已经单态化完成的代码。
各阶段的关键工作:
- 解析与宏展开:词法、语法分析得到 AST。声明宏(macro_rules!)、过程宏(derive、attribute)在这一阶段展开,
#[cfg]条件编译也在此裁剪,后续阶段看到的只有展开后的代码。 - AST → HIR:语法脱糖——
for循环变回loop+match,?运算符展开为match,async fn展开为返回impl Future的普通函数。同时完成名称解析(每个标识符绑定到具体的定义)。 - 类型检查:基于 HIR 做类型推断和 trait 求解,绝大多数编译错误(类型不匹配、trait 未实现)在这里报出。
- HIR → MIR:降阶为控制流图形式的 MIR。借用检查、drop 细化(drop elaboration)、常量求值(const eval)以及泛型单态化都在 MIR 上进行;之后还会跑 MIR 级优化(内联、常量传播、死代码消除)。
- 代码生成:单态化后的 MIR 翻译为 LLVM IR,进入 LLVM 优化管道(函数内联、循环向量化、死代码消除等),再经指令选择、寄存器分配输出目标文件(.o)。
- 链接:调用平台链接器(或内置的 rust-lld),把所有 .o 与依赖的 rlib、系统库合并为可执行文件或动态库。
两点值得记住:
- 单态化在 MIR 阶段完成:泛型函数对每个具体类型各生成一份代码,LLVM 看到的是完全具体化的函数。这是泛型"零成本"的来源,也是编译慢、二进制膨胀的来源。
- 查询系统与增量编译:rustc 内部是按需计算的查询架构(如"求某函数的类型"“生成某函数的 MIR”),增量编译就是缓存这些查询结果,只重算受改动影响的部分。
观察中间产物
排查编译器行为时,可以让 rustc 吐出任意一层的中间表示:
1 | |
-Zunpretty=expanded 与 cargo expand 功能类似,都展示宏展开后的完整源码;后者对 cargo 项目更方便。
rustc 常用编译选项
输出控制
1 | |
crate-type 的区别:rlib 是 Rust 专用静态库(含元数据,cargo 默认);staticlib/cdylib 是给 C/C++ 用的系统静态库/动态库;dylib 是 Rust 动态库(需同版本 rustc,几乎不用于分发)。
优化选项(-C)
1 | |
codegen-units 影响的是 MIR 级并行:crate 被切成多个单元并行翻译和优化,release 默认 16。设为 1 时优化最充分但最慢,常与 lto=fat 搭配做发布构建。
查询选项(–print)
1 | |
rustc --print cfg --target <triple> 可以查任意目标的 cfg,写 #[cfg(...)] 条件编译时很有用。
Cargo profile:日常配置的入口
实际项目很少直接调 rustc,上述 -C 选项大多通过 Cargo.toml 的 profile 配置:
1 | |
补充说明:
[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 | |
1 | |
优先级规则:target.<triple>.rustflags > RUSTFLAGS > build.rustflags,三者互斥,命中高优先级后低优先级整体失效。注意 RUSTFLAGS 对 workspace 里所有 crate(包括依赖)生效,改了它会触发全量重编。
交叉编译与链接器
1 | |
rustc 本身可以生成任意已安装 target 的目标文件,真正的瓶颈在链接器——需要告诉 cargo 用哪个交叉链接器:
1 | |
常见的两个实用场景:
1 | |
编译时间排查
1 | |
编译慢的典型原因和对应手段:codegen-units 过多导致优化不足(release 降到 1)、单态化膨胀(用 cargo llvm-lines 定位)、链接耗时(换 lld/mold 链接器)。dev 模式下的编译速度则主要靠增量编译和减少依赖数量保证。






