目录

📝 文章摘要

一、背景介绍

1.1 为什么 Rust 编译这么慢?

1.2 rustc 的多阶段编译管道

二、原理详解

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 上的实现

2.5 LLVM IR

三、代码实战

3.1 实:查看 MIR

3.2 实战:查看 LLVM 优化(零成本抽象)

四、结果分析

4.1 编译阶段的开销

五、总结讨论

5.1 核心要点

5.2 讨论问题

参考链接


📝 文章摘要

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 做了多得多的工作:

  1. 复杂的类型:Trait 解析、泛型单态化(Monomorphization)。
  2. 借用检查:复杂的静态分析,保证内存安全。
  3. 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. 所有操作都被分解为三地址码_1 = _2 + _3)。)。
    2. 没有复杂的表达式,只有 BasicBlock(基本块)和 Terminator(终结符,如 Goto, `SwitchCall`)。
    3. 借用检查在此处进行!
// 源代码
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 讨论问题

  1. 既然 LLVM 优化如此耗时,Rust 是否应该考虑其他后端?(提示:cranelift
  2. MIR (中级 IR) 的引入,除了借用检查,还给 rustc 带来了哪些好处?(提示:更好的错误信息、MIR 优化)
  3. “单态化”(Monomorphization)是 Rust 性能的功臣,但也是编译慢的罪魁祸首,如何权衡?
  4. 如果你要调试 rustc 编译器本身,你会从哪个 IR 阶段开始入手?

参考链接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐