Rust 编译器内部探秘:从 HIR/MIR 到 LLVM IR
目录
2.1 AST (Abstract Syntax Tree) - 抽象语法树
2.2 HIR (High-level IR) - 高级IR
2.3 MIR (Mid-level IR) - 中级IRR
2.4 借用检查 (Borrow Check) 在 MIR 上的实现
📝 文章摘要
Rust 编译器(rustc)是 Rust 语言最核心的资产之一,它不仅保证了内存安全,还负责生成高度优化的机器码。rustc 的编译流程是一个复杂的多阶段管道(Pipeline)。本文将深入 Rust 编译器的内部,探秘其核心中间表示(Intermediate Representations, IR):HIR(高级IR)、MIR(中级IR)和 LLVM IR。我们将探讨借用检查(Borrow Checking)在 MIR 上的实现原理,以及优化(Optimizations)如何在 LLVM IR 层面发生,帮助读者理解 Rust “零成本抽象” 的底层逻辑。
一、背景介绍
1.1 为什么 Rust 编译这么慢?
Rust 编译慢是开发者幸福感的主要障碍。与 C (Clang) 或 Go (gc) 相比,rustc 做了多得多的工作:
- 复杂的类型:Trait 解析、泛型单态化(Monomorphization)。
- 借用检查:复杂的静态分析,保证内存安全。
- LLVM 后端:LLVM 负责生成高度优化的代码,但优化本身非常耗时。
`rustc 牺牲了编译速度,换取了运行时的极致性能和内存安全。
1.2 rustc 的多阶段编译管道
rustc 的编译过程是“IR 递降”的过程,每一层 IR 逐步降低抽象层次,接近机器码。

二、原理详解
2.1 AST (Abstract Syntax Tree) - 抽象语法树
AST 是源代码的直接、字面表示。
// 源代码
fn main() {
let x = 1 + 2;
}
// AST (简化)
Item(fn main)
Block
Stmt(let x = ...)
Expr(BinaryOp(+))
Left: Expr(Literal(1))
Right: Expr(Literal(2))
- 特点:保留所有语法细节(如括号、
pub关键字),用于宏展开。 - 查看:`rustc -Z unpretty=ast srcmain.rs` (需要 nightly)
2.2 HIR (High-level IR) - 高级IR
AST 过于关注语法,不适合类型检查。HIR 是 AST 的“去糖”版本,更接近 Rust 的“语义”。
for循环 (AST) ->IntoIterator::into_iter+loop+match(HIR)?运算符 (AST)T) ->match+return Err(HIR)
// 源代码 (AST)
for i in 0..10 { }
//IR (概念上)
{
let mut iter = (0..10).into_iter();
loop {
match iter.next() {
Some(i) => { /* ... */ },
None => break,
}
}
}
- 特点:HIR 是
rustc进行类型检查和 Trait 解析的主要 IR。 - 查看:`rustc - unpretty=hir src/main.rs` (需要 nightly)
2.3 MIR (Mid-level IR) - 中级IRR
MIR 是 rustc 的最大创新之一。它是一种控制流图 (Control Flow Graph, CFG),非常简单、明确。
-
特点:
- 所有操作都被分解为三地址码(
_1 = _2 + _3)。)。 - 没有复杂的表达式,只有
BasicBlock(基本块)和Terminator(终结符,如Goto, `Switch,Call`)。 - 借用检查在此处进行!
- 所有操作都被分解为三地址码(
// 源代码
fn example(x: i32) -> i32 {
let y = x * 2;
if y > 10 {
y - 10
} else {
y
}
}
// MIR (简化)
fn example(_1: i32) -> i32 {
let mut _0: i32; // 返回值
let mut _2: i32; // y
let mut _3: bool; // if 条件
bb0: {
_2 = Mul(_1, 2); // y = x * 2
_3 = Gt(_2, 10); // _3 = (y > 10)
// 终结符:根据 _3 跳转
SwitchInt(_3) -> [false: bb2, true: bb1];
}
bb1: { // if true
_0 = Sub(_2, 10); // _0 = y - 10
Goto(bb3);
}
bb2: { // if false
_0 = _2; // _0 = y
Goto(bb3);
}
bb3: { // 共同出口
Return();
}
}
- 查看:`rust -Z dump-mir=all src/main.rs
(需要 nightly, 会生成在mir_dump/` 目录)
2.4 借用检查 (Borrow Check) 在 MIR 上的实现
rustc 的借用检查器(称为 Polonius,前身为 NLL)在 MIR 上运行。
// 源代码
fn main() {
let mut x = 10;
let y = &x;
let z = &mut x; // ❌ 错误
println!("{}", y);
}
// MIR 上的分析(概念上)
bb0: {
_1 = 10; // x = 10
_2 = &__1; // y = &x (借用 'a 开始)
_3 = &mut _1; // z = &mut x (用 'b 开始)
// 检查点:
// 借用 'a (Read) 和 借用 'b (Write)
// 在同一点上活跃 (Live) 且冲突!
// 编译失败!
_4 = println!(_2); // 借用 'a 在此使用
Return();
}
MIR 的简单结构使得这种数据流分析(Dataflow Analysis)变得可行和高效。
2.5 LLVM IR
MIR 经过优化和转换后,最终被降级为 LLVM IR。LLVM 是一个独立的编译器后端(被 Clang, Swift, Rust 等使用)。
// Rust 源代码
fn add(a: i32, b: i32) -> i32 {
a + b
}
// LLVM IR (未优化)
define i32 @"_ZN4main3add17h...E"(i32 %a, i32 %b) {
; %a 和 %b 是参数
%1 = add i32 %a, %b
ret i32 %1
}
- 特点*:LLVM IR 是静态单赋值(SSA)形式的,与特定 CPU 无关。
- 优化:LLVM 在此 IR上执行重量级的优化,如循环展开、函数内联、矢量化(SIMD)等。
- 查看:`cargo build --release --emit=llvm-ir
(在target/release/deps/` 中)
三、代码实战
3.1 实:查看 MIR
我们将分析一个简单函数的 MIR,以查看借用检查。
src/main.rs
// main.rs
fn main() {
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);
let r2 = &mut s;
r2.push_str(" world");
println!("{}", r2);
}
// 这个代码是合法的,因为 r1 的生命周期在 println! 后就结束了
// (非词法生命周期 - NLL)
编译 (Nightly Rust):
# 安装 nightly
rustup default nightly
# 编译并转储 MIR
rustc -Z dump-mir=nll src/main.rs
# (查看生成的 `mir_dump/main.main.nll.0.mir`)
MIR 分析 (简化):
// fn main()
bb0: {
_1 = String::from("hello"); // s
_2 = &_1; // r1 = &s (借用 'a)
_3 = _2; // 传参给 println!
_4 = println!(_3);
// r1 (借用 'a) 在此结束
_5 = &mut _1; // r2 = &mut s (借用 'b)
_6 = String::push_str(_5, " world");
_7 = _5;
_8 = println!(_7);
// r2 (借用 'b) 在此结束
drop(_1);
Return();
}
分析:借用检查器通过分析 MIR,发现 _2 (借用 'a) 的最后一次使用是在 println!(_3),在此之后,_5 (借用 'b) 开始是安全的。
3.2 实战:查看 LLVM 优化(零成本抽象)
Rust 的 Option<T> 枚举如何实现零成本?
src/main.rs
// (使用 std::ptr::NonNull 来模拟 &mut T)
use std::ptr::NonNull;
fn process(opt: Option<NonNull<u8>>) -> usize {
match opt {
Some(ptr) => ptr.as_ptr() as usize,
None => 0,
}
}
fn main() {
let mut x = 10u8;
let ptr = NonNull::new(&mut x as *mut u8);
println!("{}", process(ptr));
println!("{}", process(None));
}
编译 (Release 模式):
cargo build --release --emit=llvm-ir
# (查看 target/release/deps/my_project-....ll)
LLVM IR (优化后, 简化):
; Rust 的 Option<NonNull<T>> 被优化了
; NonNull<T> (非空指针) 不能为 0
; Rust 利用这个“空指针优化” (Niche Optimization)
; None => 0
; Some(ptr) => ptr (非 0)
define i64 @process(i64 %opt) {
entry:
; 编译器知道 Option<NonNull<T>> 等同于一个 i64 (指针)
; 检查它是否为 0
%0 = icmp eq i64 %opt, 0
; if (%0 == true) then 0 else %opt
%1 = select i1 %0, i64 0, i64 %opt
ret i64 %1
}
分析:Option<NonNull<T>> 的 match 语句被 LLVM 优化为**一次判零(icmp eq ... 0)次条件选择(select)**。Option 枚举的开销完全消失了。这就是 Rust 零成本抽象的体现。
四、结果分析
4.1 编译阶段的开销
| 阶段 | 主要工作 | 开销 |
|---|---|---|
| AST/HIR | 宏展开, 类型检查, Trait 解析 | 中 (受代码复杂度影响) |
| MIR 借用检查, MIR 优化 | 高 (Rust 安全性的核心成本) | |
| LLVM IR | 单态化 (泛型), LLVM 优化 | 极高 (编译慢的主要原因) |
graph TD
A[总编译时间] --> B(Frontend (HIR/MIR));
A --> C(Backend (LLVM));
B --> B1(类型检查);
B --> B2(借用检查);
C --> C1(单态化 (泛G泛型));
C --> C2(LLVM 优化 (-O3));
style B1 fill:#e1f5fe
style B2 fill:#e1f5fe
style C1 fill:#ffebee
style C2 fill:#ffebee
- 借用检查 增加了前端(rustc)的开开销。
- 单态化 和 LLVM 优化 增加了后端(LLVM)的开销。
五、总结讨论
5.1 核心要点
- 编译是管道:
rustc通过AST-> `HIR ->MIR->LLVM IR的多阶段管道来转换代码。 - HIR (高级):用于类型检查和 Trait 解析。
- MIR (中级):是
rustc的核心,用于借用检查和 Rust 特定的优化。 - LLVM IR (后端):用于重量级的、跨语言的性能优化。
- 零成本抽象:如 `Option 的空指针优化,是在 MIR 降级到 LLVM IR 时,通过 LLVM 的优化能力实现的。
5.2 讨论问题
- 既然 LLVM 优化如此耗时,Rust 是否应该考虑其他后端?(提示:
cranelift) - MIR (中级 IR) 的引入,除了借用检查,还给
rustc带来了哪些好处?(提示:更好的错误信息、MIR 优化) - “单态化”(Monomorphization)是 Rust 性能的功臣,但也是编译慢的罪魁祸首,如何权衡?
- 如果你要调试
rustc编译器本身,你会从哪个 IR 阶段开始入手?
参考链接
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)