Skip to content

第16章 LLVM 代码生成:从 MIR 到机器码

"当代码到达 LLVM 时,Rust 的安全保证已经完成了它的使命——剩下的只是把正确的代码变快。"

本章要点

  • Rust 的代码生成采用双层架构rustc_codegen_ssa(后端无关抽象层)+ rustc_codegen_llvm(LLVM 具体实现)
  • MIR → LLVM IR 翻译过程中,Rust 的所有权、借用、lifetime 信息全部丢弃
  • LLVM IR 中的类型极其简单:i32i64ptrfloat 等——Rust 的复杂类型被降级为原始内存布局
  • 泛型在代码生成阶段完成单态化,每个具体类型实例生成独立的机器码
  • LLVM 优化流水线包含多个阶段:PreLink → LTO → PostLink,不同优化级别差异巨大
  • 链接是代码生成的最后一环:将目标文件、元数据、分配器 shim 合并为最终二进制
  • Cranelift 作为替代后端,以编译速度换取运行时性能,适合开发阶段
  • 调试信息(DWARF/CodeView)在整个代码生成过程中被精心维护

16.1 代码生成的全局架构

Rust 编译器的代码生成并非一步完成,而是一条精心设计的流水线。从上一章我们了解到,MIR 是 Rust 编译器中最后一种"Rust 味"的中间表示。从 MIR 开始,编译器进入了一个截然不同的世界——后端代码生成的世界。

16.1.1 双层架构的设计哲学

Rust 编译器的代码生成架构可以用"抽象 + 实现"的经典模式来概括。这个架构分布在两个关键 crate 中:

rustc_codegen_ssa——后端无关的抽象层。这个 crate 定义了一套完整的 trait 体系,规定了任何代码生成后端必须实现的接口。它包含 MIR 到后端 IR 的翻译逻辑、单态化收集的结果处理、以及链接协调等功能。名字中的 "ssa" 表示 Static Single Assignment,因为大多数现代编译器后端都基于 SSA 形式。

rustc_codegen_llvm——LLVM 后端的具体实现。这个 crate 实现了 rustc_codegen_ssa 定义的所有 trait,将抽象操作映射到具体的 LLVM C API 调用。它是 Rust 编译器的默认后端,也是目前最成熟、优化能力最强的后端。

这种双层设计带来的核心好处是:MIR 到后端 IR 的翻译逻辑只需要编写一次。无论使用 LLVM 还是 Cranelift,codegen_statementcodegen_rvaluecodegen_terminator 等核心翻译函数都是共享的——它们通过 trait 方法调用后端特定的 IR 构建操作。

16.1.2 trait 体系全景

rustc_codegen_ssa/src/traits/ 目录下,定义了后端必须实现的全部 trait:

Trait职责
CodegenBackend后端入口,驱动整个代码生成流程
BuilderMethodsIR 指令构建器,每个基本块一个
TypeCodegenMethodsRust 类型 → 后端类型映射
DebugInfoCodegenMethods调试信息生成
PreDefineCodegenMethods函数/全局变量的前向声明
WriteBackendMethods优化、LTO、目标文件写入

这些 trait 通过关联类型实现了类型级别的抽象。BuilderMethods 定义了 type Valuetype BasicBlocktype Function 等关联类型,对于 LLVM 后端它们对应 LLVM 的 Value*BasicBlock*Function*;对于 Cranelift 后端则对应 Cranelift 自己的 IR 类型。

16.1.3 CodegenCxFunctionCx——两层上下文

代码生成过程中有两个关键的上下文对象:

CodegenCx(在 LLVM 后端中定义于 rustc_codegen_llvm/src/context.rs)是模块级上下文。每个 codegen unit 拥有一个独立的 CodegenCx,其中包含:

  • 一个 LLVM Context(llcx)和一个 LLVM Module(llmod
  • TyCtxt 引用(访问类型信息和查询系统)
  • 已声明的函数和全局变量的缓存
  • 调试信息上下文

源码中的定义非常清晰地展示了这种结构:

rust
// rustc_codegen_llvm/src/context.rs
pub(crate) struct FullCx<'ll, 'tcx> {
    pub tcx: TyCtxt<'tcx>,
    pub scx: SimpleCx<'ll>,
    pub use_dll_storage_attrs: bool,
    pub tls_model: llvm::ThreadLocalMode,
    pub codegen_unit: &'tcx CodegenUnit<'tcx>,
    // ... 更多字段
}

FunctionCx(定义于 rustc_codegen_ssa/src/mir/mod.rs)是函数级上下文。每当翻译一个 MIR 函数时,就会创建一个 FunctionCx

rust
// rustc_codegen_ssa/src/mir/mod.rs
pub struct FunctionCx<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> {
    instance: Instance<'tcx>,          // 当前翻译的泛型实例
    mir: &'tcx mir::Body<'tcx>,        // MIR 函数体
    llfn: Bx::Function,               // 后端函数对象
    fn_abi: &'tcx FnAbi<'tcx, Ty<'tcx>>, // 函数 ABI 信息
    cx: &'a Bx::CodegenCx,            // 模块级上下文的引用
    cached_llbbs: IndexVec<mir::BasicBlock, CachedLlbb<Bx::BasicBlock>>,
    locals: locals::Locals<'tcx, Bx::Value>,  // 局部变量的存储位置
    // ...
}

FunctionCx 维护了 MIR 基本块到后端基本块的映射(cached_llbbs),以及每个局部变量的存储位置(locals)。局部变量可能存储在栈上的 alloca(Place),也可能被提升为 SSA 寄存器(Operand)——这是一个重要的优化决策。

16.2 Codegen Unit:并行编译的基本单位

编译器不会把整个 crate 放进一个 LLVM Module。它将所有 mono item(单态化后的函数、静态变量等)划分为多个 Codegen Unit(CGU),每个 CGU 独立编译为一个目标文件。CGU 的划分服务于两个目标:并行编译(不同 CGU 在不同线程上同时编译)和增量编译(未变化的 CGU 可复用上次结果)。默认 debug 构建使用 256 个 CGU,release 构建使用 16 个,可通过 -C codegen-units=N 控制。

16.2.1 CGU 的编译流程

每个 CGU 的编译流程如下(compile_codegen_unit 函数):

rustc_codegen_llvm/src/base.rs 中,compile_codegen_unit 函数展示了这个完整流程:

rust
// rustc_codegen_llvm/src/base.rs(简化)
fn module_codegen(tcx: TyCtxt<'_>, cgu_name: Symbol) -> ModuleCodegen<ModuleLlvm> {
    let cgu = tcx.codegen_unit(cgu_name);
    let llvm_module = ModuleLlvm::new(tcx, cgu_name.as_str());
    {
        let mut cx = CodegenCx::new(tcx, cgu, &llvm_module);

        // 第一遍:预定义所有函数和静态变量的符号
        let mono_items = cgu.items_in_deterministic_order(tcx);
        for &(mono_item, data) in &mono_items {
            mono_item.predefine::<Builder<'_, '_, '_>>(
                &mut cx, data.linkage, data.visibility);
        }

        // 第二遍:翻译每个 mono item 的完整定义
        for &(mono_item, _) in &mono_items {
            mono_item.define::<Builder<'_, '_, '_>>(&mut cx);
        }

        // 完成调试信息
        cx.debuginfo_finalize();
    }
    ModuleCodegen { name: cgu_name.to_string(), module_llvm: llvm_module }
}

预定义和定义分为两遍,因为函数之间可能存在相互引用。预定义阶段只声明符号(类似 C 的前向声明),定义阶段才填充函数体。

16.3 MIR → LLVM IR 翻译

翻译的核心入口是 codegen_mir 函数(rustc_codegen_ssa/src/mir/mod.rs),接收单态化后的 Instance<'tcx>,将其 MIR 翻译为后端 IR。

16.3.1 函数框架的建立

rust
// rustc_codegen_ssa/src/mir/mod.rs(简化)
pub fn codegen_mir<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
    cx: &'a Bx::CodegenCx,
    instance: Instance<'tcx>,
) {
    let llfn = cx.get_fn(instance);
    let mir = tcx.instance_mir(instance.def);
    let fn_abi = cx.fn_abi_of_instance(instance, ty::List::empty());

    // 创建调试信息上下文
    let debug_context = cx.create_function_debug_context(instance, fn_abi, llfn, &mir);

    // 创建入口基本块
    let start_llbb = Bx::append_block(cx, llfn, "start");
    let mut start_bx = Bx::build(cx, start_llbb);

    // 分析哪些局部变量必须在栈上(不能提升为 SSA 值)
    let memory_locals = analyze::non_ssa_locals(&fx, &traversal_order);

    // 为每个局部变量分配存储空间
    // ... alloca 或 operand ...
}

局部变量的存储决策是一个关键优化。non_ssa_locals 分析判断每个局部变量是否需要栈分配。如果变量类型是"立即数"类型、从未被取地址、且没有出现在 place path 中(如 tmp.field),则可以提升为 SSA 寄存器值,避免不必要的 alloca/load/store

16.3.2 语句的翻译

MIR 语句翻译在 rustc_codegen_ssa/src/mir/statement.rs 中。核心是对 statement.kind 的模式匹配:Assign 根据目标位置是 Place(栈上)还是 PendingOperand(SSA 值),分别调用 codegen_rvaluecodegen_rvalue_operandStorageLive/StorageDead 翻译为 LLVM 的 llvm.lifetime.start/llvm.lifetime.end intrinsic,告诉 LLVM 栈变量的活跃范围以复用栈空间。

16.3.3 右值(Rvalue)的翻译

右值的翻译是最复杂的部分,在 rustc_codegen_ssa/src/mir/rvalue.rs 中实现。几个典型的映射关系:

MIR RvalueLLVM IR
Use(operand)load + store(或直接传递值)
BinaryOp(Add, a, b)add i32 %a, %b
BinaryOp(Lt, a, b)icmp slt i32 %a, %b(有符号)或 icmp ult(无符号)
Ref(place)getelementptr 计算地址
Cast(IntToInt, ..)trunczextsextbitcast
Cast(Unsize, ..)构造胖指针(数据指针 + vtable/长度)
Repeat(elem, count)循环或 memset
Aggregate(...)一系列 insertvalue 或直接内存写入

比较运算的翻译(rustc_codegen_ssa/src/base.rs)精确区分了有符号和无符号:

rust
pub(crate) fn bin_op_to_icmp_predicate(op: BinOp, signed: bool) -> IntPredicate {
    match (op, signed) {
        (BinOp::Eq, _) => IntPredicate::IntEQ,
        (BinOp::Lt, true) => IntPredicate::IntSLT,   // 有符号小于
        (BinOp::Lt, false) => IntPredicate::IntULT,  // 无符号小于
        // ...
    }
}

16.3.4 终结符(Terminator)的翻译

每个 MIR 基本块以一个终结符结尾。终结符的翻译在 rustc_codegen_ssa/src/mir/block.rs 中实现:

MIR TerminatorLLVM IR
Goto { target }br label %target
SwitchInt { discr, targets }switch i32 %discr, label %otherwise [...]
Returnret <type> %retval
Call { func, args, dest }call <type> @func(args...) + br label %dest
Drop { place, target }调用 drop_in_place + br label %target
Assert { cond, target, cleanup }br i1 %cond, label %target, label %panic_bb
Unreachableunreachable
UnwindResumeresume 或调用 personality 函数

函数调用的翻译尤其复杂。编译器需要根据 FnAbi 决定如何传递每个参数:直接传值(PassMode::Direct)、通过引用传递(PassMode::Indirect)、还是通过类型强转(PassMode::Cast)。后面的 ABI 章节会详细讨论这些。

16.4 类型映射:Rust 类型 → LLVM 类型

Rust 的类型系统远比 LLVM 的类型系统复杂。代码生成阶段的一个核心任务是将 Rust 类型"降级"为 LLVM 类型。

16.4.1 标量类型映射

基础类型的映射相对直接:

Rust 类型LLVM 类型说明
booli8(不是 i1内存中用 i8 表示,运算时可能用 i1
u8 / i8i8
u16 / i16i16
u32 / i32i32
u64 / i64i64
u128 / i128i128
usize / isizei64(64 位平台)取决于目标平台指针宽度
f32float
f64double
*const T / *mut TptrLLVM opaque pointer
&T / &mut T(瘦引用)ptr

16.4.2 复合类型:struct 的布局

结构体被翻译为 LLVM 的 struct 类型。翻译逻辑在 rustc_codegen_llvm/src/type_of.rs 中的 uncached_llvm_type 函数实现:

rust
// 简化的 struct 翻译逻辑
fn uncached_llvm_type<'a, 'tcx>(
    cx: &CodegenCx<'a, 'tcx>,
    layout: TyAndLayout<'tcx>,
    defer: &mut Option<(&'a Type, TyAndLayout<'tcx>)>,
) -> &'a Type {
    match layout.backend_repr {
        BackendRepr::Scalar(_) => bug!("handled elsewhere"),
        BackendRepr::SimdVector { element, count } => {
            return cx.type_vector(element, count);
        }
        BackendRepr::Memory { .. } | BackendRepr::ScalarPair(..) => {}
    }
    // ...
    match layout.fields {
        FieldsShape::Primitive | FieldsShape::Union(_) => {
            // union:用字节填充到正确大小
            let fill = cx.type_padding_filler(layout.size, layout.align.abi);
            cx.type_struct(&[fill], packed)
        }
        FieldsShape::Array { count, .. } => {
            cx.type_array(layout.field(cx, 0).llvm_type(cx), count)
        }
        FieldsShape::Arbitrary { .. } => {
            // 普通 struct:按字段生成
            let (llfields, packed) = struct_llfields(cx, layout);
            cx.type_struct(&llfields, packed)
        }
    }
}

struct_llfields 函数按字段偏移顺序遍历,在字段之间插入填充字节,并在对齐要求无法满足时标记 packed。例如 struct Example { a: u8, b: u32, c: u16 } 对应 LLVM 类型 { i8, [3 x i8], i32, i16, [2 x i8] },其中 [3 x i8][2 x i8] 是编译器插入的填充字节。

16.4.3 枚举的表示

Rust 的枚举翻译是所有类型映射中最复杂的。编译器根据枚举的变体数量和有效载荷类型,选择最紧凑的表示。

无数据枚举(C-like enum):

rust
enum Color { Red, Green, Blue }
// LLVM 类型:i8(或 i32,取决于变体数量)

带数据枚举

rust
enum Shape {
    Circle(f64),        // 变体 0
    Rectangle(f64, f64), // 变体 1
}
// LLVM 类型大致为:{ i8, [7 x i8], double, double }
// i8 = discriminant(判别值)
// [7 x i8] = 填充
// double, double = 最大变体 Rectangle 的有效载荷

Niche 优化——Rust 编译器最著名的类型布局优化之一:

rust
enum Option<&T> {
    None,
    Some(&T),
}
// LLVM 类型:ptr
// None 用空指针(0x0)表示,不需要额外的 discriminant 字段
// 这就是为什么 Option<&T> 和 &T 大小完全相同

16.4.4 胖指针(Fat Pointer)

Rust 中某些引用包含额外的元数据,在 LLVM 中表示为 ScalarPair

Rust 类型LLVM 表示内容
&[T]{ ptr, i64 }数据指针 + 长度
&str{ ptr, i64 }数据指针 + 字节长度
&dyn Trait{ ptr, ptr }数据指针 + vtable 指针
Box<dyn Trait>{ ptr, ptr }同上

vtable 本身是一个全局常量,布局为:

vtable: {
    ptr,   // drop_in_place 函数指针
    i64,   // size(对象大小)
    i64,   // align(对象对齐)
    ptr,   // trait 方法 1
    ptr,   // trait 方法 2
    ...    // 更多 trait 方法
}

16.4.5 BackendRepr——类型表示的分类

编译器不是对每个类型都从头推导 LLVM 表示。在布局计算阶段,每个类型都会被归类为一种 BackendRepr(后端表示):

BackendRepr含义例子
Scalar(s)单个标量值i32, f64, *const T, bool
ScalarPair(s1, s2)两个标量值&[T](ptr + len), &dyn Trait(ptr + vtable)
SimdVector { element, count }SIMD 向量__m256
Memory { sized }内存中的聚合类型struct, 大 enum

ScalarScalarPair 类型可以直接在寄存器中传递和操作,不需要通过内存。这是函数参数传递和局部变量优化的关键判断依据。

16.5 函数 ABI 与调用约定

16.5.1 单态化(Monomorphization)

Rust 的泛型在代码生成时完成单态化:每个泛型函数与每组具体类型参数的组合,都会生成独立的机器码副本。

rust
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
    a + b
}

fn main() {
    add(1i32, 2i32);   // 生成 add::<i32>
    add(1.0f64, 2.0);  // 生成 add::<f64>
}

在 LLVM IR 中,这会生成两个完全独立的函数:add::<i32> 使用 add i32add::<f64> 使用 fadd doubleFunctionCx::monomorphize 方法通过 instantiate_mir_and_normalize_erasing_regions 在翻译过程中替换泛型参数。

16.5.2 FnAbi 与参数传递模式

FnAbi 决定了每个参数和返回值如何在机器级别传递。每个参数有一个 PassModeDirect(寄存器直接传递标量)、Pair(两个寄存器传递 ScalarPair)、Indirect(通过指针传递大型 struct)、Cast(强转为另一种类型传递,如小 struct 被拍平为 i64)、Ignore(ZST 不传递)。

16.5.3 LLVM 函数属性

编译器根据 Rust 的安全保证为 LLVM 函数参数添加优化属性:&T 被标记为 nonnull readonly(引用非空且不可变),&mut T 被标记为 noalias nonnull(排他借用保证无别名)。还有 dereferenceable(N)(引用指向的内存至少 N 字节有效)和 noundef(值已初始化)。这些属性是 Rust 安全保证转化为 LLVM 优化机会的直接体现。注意,这些优化属性只在 OptLevel != No 时才添加。

16.6 LLVM 优化流水线

LLVM 的优化是 Rust 程序高性能的关键。Rust 编译器通过 LLVMRustOptimize 函数调用 LLVM 的新 Pass Manager 来执行优化。

16.6.1 优化级别映射

Rust 的 -C opt-level 直接映射到 LLVM 优化级别:0 → O0、1 → O1、2 → O2(Release 默认)、3 → O3、s → Os(优化大小)、z → Oz(激进优化大小)。其中 Os 和 Oz 使用 Default 代码生成级别但额外传递 SizeDefault/SizeAggressive 标志。

16.6.2 优化的阶段划分

LLVM 优化分为多个阶段:PreLinkNoLTOPreLinkThinLTOPreLinkFatLTO(预链接)和 ThinLTOFatLTO(后链接)。分阶段是因为如果后面还要做 LTO,预链接阶段的某些优化(如函数内联)应适度克制,把跨模块优化机会留给 LTO。

16.6.3 LLVM 关键优化 Pass

LLVM 的优化 pipeline 包含数十个 pass,以下是对 Rust 代码最重要的几个:

mem2reg——将 alloca 栈变量提升为 SSA 寄存器,消除不必要的 load/store。这是最基础的优化,例如 alloca i32; store 42; load 被简化为直接使用常量 42

SROA(Scalar Replacement of Aggregates)——将 struct 拆分为独立标量字段,使每个字段可以独立优化。例如 alloca { double, double } 被消除,字段直接作为 SSA 值使用。

GVN(Global Value Numbering)——消除冗余计算,两个计算相同值的表达式只保留一个。

函数内联——对 Rust 尤为重要。迭代器链、泛型抽象、闭包都依赖内联消除抽象开销。例如 vec.iter().map(|x| x*2).filter(|x| *x > 10).sum() 在内联后融合为一个高效循环。

自动向量化——将标量循环转换为 SIMD 指令,在 -C opt-level=2 及以上启用。

循环优化——包括展开(Unrolling)、不变量外提(LICM)、强度削减等。在 -Os/-Oz 下循环展开被禁用以控制代码大小。

16.6.4 Sanitizer 集成

Rust 支持多种 LLVM sanitizer(Address、Memory、Thread、CFI 等)。Sanitizer instrumentation 仅在 Pre-Link 阶段插入,LTO 阶段不再重复。编译器通过 SanitizerOptions 结构将配置传递给 LLVMRustOptimize

16.6.5 Debug 构建 vs Release 构建

维度Debug(O0)Release(O2)O3OsOz
函数内联几乎不内联大量内联更激进内联适度内联最少内联
循环展开更激进禁用禁用
自动向量化禁用
mem2reg/SROA
编译速度最快慢 3-5x慢 5-10x慢 3-5x慢 3-5x
运行速度慢 10-100x最优略优于 O2略慢于 O2更慢
二进制大小最大中等最大较小最小

16.7 LTO:跨 crate 边界的全局优化

16.7.1 为什么需要 LTO

默认情况下,每个 CGU 独立编译为 LLVM Module,独立执行优化。这意味着 LLVM 无法跨 CGU 或跨 crate 进行内联和其他优化。对于性能敏感的程序,这可能导致显著的性能损失。

Link-Time Optimization(LTO)打破了这个边界:在链接阶段,将多个 LLVM Module 合并,然后在合并后的大 Module 上运行全局优化。

16.7.2 三种 LTO 模式

toml
# Cargo.toml
[profile.release]
lto = false     # 默认:crate 内 ThinLocal LTO
lto = "thin"    # Thin LTO:跨 crate,保持并行性
lto = true      # Fat LTO:完全合并所有模块

ThinLocal(默认)只在同一 crate 的不同 CGU 之间做 LTO。Thin LTO 跨 crate 进行分析和有选择的导入,但不完全合并模块。Fat LTO 将所有模块合并为一个巨大的 LLVM Module 再优化,提供最大优化机会但编译最慢。实现在 rustc_codegen_llvm/src/back/lto.rs——从每个上游 rlib 归档中提取 bitcode,合并后统一优化。

16.7.3 LTO 与符号可见性

LTO 能够进行死代码消除,但需要知道哪些符号是"外部可见"的。编译器维护 symbols_below_threshold 列表,包含所有导出符号、rust_eh_personality 以及 profiling 相关的弱符号(如 __llvm_profile_raw_version),确保 LTO 不会错误地消除它们。

16.8 从 LLVM IR 到机器码:目标文件生成

16.8.1 TargetMachine 的创建

LLVM 需要 TargetMachine 对象描述目标平台。target_machine_factory 函数(rustc_codegen_llvm/src/back/write.rs)收集所有平台参数并创建它。关键参数包括:目标三元组(如 x86_64-unknown-linux-gnu)、CPU 型号(如 genericapple-m1)、CPU 特性(如 +sse4.2,+avx2)、重定位模型(Static、PIC、PIE)、代码模型(Small、Medium、Large)等。

16.8.2 目标文件的写入

优化完成后,codegen 函数通过 write_output_file 将 LLVM Module 写入目标文件。它创建一个 PassManager,添加分析 pass 和库信息,然后调用 LLVMRustWriteOutputFile 生成输出。输出格式有两种:ObjectFile.o 目标文件)和 AssemblyFile.s 汇编文件,通过 --emit=asm 触发)。

16.9 链接:从目标文件到最终二进制

16.9.1 链接的总体流程

代码生成的最后一步是链接。link_binary 函数(rustc_codegen_ssa/src/back/link.rs)协调整个链接过程:

16.9.2 链接器与 rlib

Rust 支持多种链接器:cc(gcc/clang,默认)、lld(LLVM 链接器,速度快)、link.exe(Windows MSVC)、rust-lld(Rust 自带的 lld)。

Rust 的 rlib 格式是一个 ar 归档,包含:元数据文件(lib.rmeta,供下游 crate 编译时使用)、目标文件(*.o,编译后的机器码)、以及可选的 LLVM bitcode(供 LTO 使用)。链接时,编译器从 rlib 中提取目标文件传递给链接器。

16.9.3 分配器 shim

Rust 程序需要一个全局分配器。编译器生成特殊的 allocator_module,将 __rust_alloc__rust_dealloc 等方法桥接到实际的分配器实现(默认是系统分配器)。

16.10 调试信息生成

16.10.1 DWARF 与 CodeView

Rust 支持两种调试信息格式:DWARF(Linux、macOS 等 Unix 平台)和 CodeView/PDB(Windows MSVC)。调试信息由 rustc_codegen_llvm/src/debuginfo/ 模块负责,核心上下文 CodegenUnitDebugContext 包含 LLVM DIBuilder、文件缓存、类型缓存和命名空间映射。

16.10.2 DWARF 版本与平台适配

编译器根据目标平台选择 DWARF 版本(macOS 和 Android 需要较旧版本)。对于 PDB 平台则设置 CodeView 标志。在 LTO 合并多个 CGU 时,DWARF 版本取各模块的最大值。

16.10.3 源码位置与类型调试信息

每条 MIR 语句都携带 SourceInfo,在翻译时通过 set_debug_loc 映射到 LLVM 的 debug location,使调试器可以在源码行级别设置断点。编译器为每个 Rust 类型生成对应的 DWARF/CodeView 类型描述,type_map 缓存确保每个类型只生成一次。对于递归类型(如链表节点),使用前向声明打破循环依赖。

调试信息的详细程度由 -C debuginfo= 控制:0 不生成、1 只生成行号、2 完整调试信息。

16.11 代码大小优化

16.11.1 -Os-Oz

当使用 -C opt-level=s-C opt-level=z 时,LLVM 会切换到代码大小优化模式。关键变化:

rust
// 在 Os/Oz 模式下禁用增加代码大小的优化
let unroll_loops = opt_level != config::OptLevel::Size
    && opt_level != config::OptLevel::SizeMin;
// vectorize_loop 和 vectorize_slp 在 Oz 下也被禁用

16.11.2 #[inline] 属性的影响

属性行为对代码大小的影响
无属性LLVM 自行决定是否内联中性
#[inline]提示 LLVM 内联,且允许跨 CGU 内联可能增大
#[inline(always)]强制内联通常增大
#[inline(never)]禁止内联通常减小
#[cold]标记为冷路径,降低内联优先级通常减小

#[inline] 的一个重要副作用是:它使函数的 MIR 被序列化到 rlib 中,允许下游 crate 在自己的编译过程中内联该函数。没有 #[inline] 的函数只能在同一 crate 内被内联。

16.12 Cranelift 替代后端

16.12.1 为什么需要替代后端

LLVM 是一个强大的优化编译器,但它有一个显著的缺点:编译速度慢。对于大型项目,即使是 debug 构建(-C opt-level=0),LLVM 也要花费大量时间将 LLVM IR 转换为机器码。

Cranelift 是由 Bytecode Alliance 开发的一个代码生成后端,目标是在保持合理运行时性能的前提下,大幅提升编译速度。它通过 rustc_codegen_cranelift 集成到 Rust 编译器中。

16.12.2 LLVM vs Cranelift 对比

Cranelift 后端同样实现了 rustc_codegen_ssa 定义的 trait 体系,但底层使用 Cranelift 的 IR。其源码位于 compiler/rustc_codegen_cranelift/src/,包含 base.rs(核心翻译)、abi/(ABI处理)、debuginfo/(调试信息)等模块。

维度LLVMCranelift
编译速度慢(debug 构建也慢)快 2-5 倍
运行时性能最优比 LLVM 慢 10-30%
优化级别O0 到 O3,完整优化有限优化
LTO 支持完整支持不支持
SIMD 支持完整有限
平台支持几乎所有平台x86_64、aarch64
成熟度生产级实验性
使用场景release 构建、生产环境开发阶段的快速迭代

16.12.3 使用 Cranelift

bash
rustup component add rustc-codegen-cranelift-preview
RUSTFLAGS="-Z codegen-backend=cranelift" cargo build

推荐在 debug 构建时使用 Cranelift 加速编译,release 构建时切回 LLVM 获取最佳性能。

16.13 实战:Rust 代码 → LLVM IR → 汇编

16.13.1 查看 LLVM IR

bash
# 查看未优化的 LLVM IR
cargo rustc -- --emit=llvm-ir
# 查看优化后的 LLVM IR
cargo rustc --release -- --emit=llvm-ir
# 生成的文件在 target/{debug,release}/deps/*.ll

16.13.2 示例一:简单函数

rust
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

LLVM IR(未优化):

llvm
define i32 @_ZN7example3add17h1234567890abcdefE(i32 %a, i32 %b) {
start:
  %a.dbg.spill = alloca [4 x i8], align 4
  %b.dbg.spill = alloca [4 x i8], align 4
  store i32 %a, ptr %a.dbg.spill, align 4
  store i32 %b, ptr %b.dbg.spill, align 4
  %result = add i32 %a, %b
  ret i32 %result
}

注意未优化的 IR 中有 allocastore——这是为了调试器能够查看参数值。

LLVM IR(优化后):

llvm
define i32 @_ZN7example3add17h1234567890abcdefE(i32 %a, i32 %b) {
  %result = add i32 %a, %b
  ret i32 %result
}

优化后,allocastoremem2reg 消除。

x86-64 汇编:

asm
example::add:
    lea eax, [rdi + rsi]
    ret

16.13.3 示例二:结构体与引用属性

rust
pub struct Point { x: f64, y: f64 }
pub fn distance(p: &Point) -> f64 {
    (p.x * p.x + p.y * p.y).sqrt()
}

优化后的 LLVM IR 中,p 的参数签名为 ptr noalias readonly align 8 dereferenceable(16)Point 变成 { double, double },字段通过 getelementptr 访问。对应的 x86-64 汇编只有 6 条指令:两个 movsd 加载、两个 mulsd 平方、一个 addsd 求和、一个 sqrtsd 开方。

16.13.4 示例三:迭代器链的零成本抽象

rust
pub fn sum_even_doubled(v: &[i32]) -> i32 {
    v.iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * 2)
        .sum()
}

-O2 下,LLVM 会将整个迭代器链内联并融合为一个简单的循环。最终的汇编大致等价于:

rust
pub fn sum_even_doubled_manual(v: &[i32]) -> i32 {
    let mut sum = 0;
    for &x in v {
        if x % 2 == 0 {
            sum += x * 2;
        }
    }
    sum
}

这就是 Rust "零成本抽象"的核心——高级迭代器 API 和手写循环生成完全相同的机器码。这一切归功于 LLVM 的内联和优化能力。

16.14 代码生成的完整流水线

每一步都经过精心设计:单态化收集确保只生成实际使用的代码;CGU 划分平衡并行度和优化粒度;双遍翻译解决相互引用;SSA 提升减少不必要的内存操作;分阶段优化让 PreLink 和 LTO 各司其职;调试信息全程维护。

16.15 小结

本章我们深入剖析了 Rust 编译器代码生成阶段的全貌。从架构设计到实现细节,我们看到了:

双层架构使核心翻译逻辑与后端实现解耦;Rust 的安全属性(noaliasnonnull)作为优化提示传递给 LLVM;从 mem2reg 到 LTO,每层优化各司其职;Cranelift 的引入、CGU 并行编译、Thin LTO 的设计,都是在编译速度和运行时性能之间寻找平衡。

下一章我们将探讨编译器如何通过增量编译来加速——只重新编译发生变化的部分,而不是每次都从头开始。

基于 VitePress 构建