Rust编译器原理-第16章 LLVM 代码生成:从 MIR 到机器码

《Rust 编译器原理》完整目录

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

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

:::tip 本章要点

  • 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 编译器的默认后端,也是目前最成熟、优化能力最强的后端。

graph TB subgraph "rustc_codegen_ssa(后端无关层)" T1["CodegenBackend trait"] T2["BuilderMethods trait"] T3["TypeCodegenMethods trait"] T4["DebugInfoCodegenMethods trait"] T5["MIR → 后端 IR 翻译逻辑"] T6["链接协调逻辑"] end subgraph "rustc_codegen_llvm(LLVM 实现)" L1["LlvmCodegenBackend"] L2["Builder(LLVM IR Builder)"] L3["CodegenCx(LLVM 上下文)"] L4["debuginfo 模块"] L5["back::write(优化 + 输出)"] L6["back::lto(LTO 支持)"] end subgraph "rustc_codegen_cranelift(替代实现)" C1["Cranelift 后端"] end T1 --> L1 T2 --> L2 T3 --> L3 T4 --> L4 T1 --> C1 style T1 fill:#6366f1,color:#fff,stroke:none style T2 fill:#6366f1,color:#fff,stroke:none style T3 fill:#6366f1,color:#fff,stroke:none style T4 fill:#6366f1,color:#fff,stroke:none style T5 fill:#6366f1,color:#fff,stroke:none style T6 fill:#6366f1,color:#fff,stroke:none style L1 fill:#f59e0b,color:#fff,stroke:none style L2 fill:#f59e0b,color:#fff,stroke:none style L3 fill:#f59e0b,color:#fff,stroke:none style L4 fill:#f59e0b,color:#fff,stroke:none style L5 fill:#f59e0b,color:#fff,stroke:none style L6 fill:#f59e0b,color:#fff,stroke:none style C1 fill:#10b981,color:#fff,stroke:none

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

16.1.2 trait 体系全景

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

Trait 职责
CodegenBackend 后端入口,驱动整个代码生成流程
BuilderMethods IR 指令构建器,每个基本块一个
TypeCodegenMethods Rust 类型 → 后端类型映射
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 函数):

graph LR A["创建 LLVM Module"] --> B["创建 CodegenCx"] B --> C["预定义所有符号"] C --> D["逐个翻译 mono item"] D --> E["完成调试信息"] E --> F["LLVM 优化"] F --> G["写入目标文件"] style A fill:#6366f1,color:#fff,stroke:none style B fill:#6366f1,color:#fff,stroke:none style C fill:#8b5cf6,color:#fff,stroke:none style D fill:#a855f7,color:#fff,stroke:none style E fill:#c084fc,color:#fff,stroke:none style F fill:#f59e0b,color:#fff,stroke:none style G fill:#10b981,color:#fff,stroke:none

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 Rvalue LLVM 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 Terminator LLVM IR
Goto { target } br label %target
SwitchInt { discr, targets } switch i32 %discr, label %otherwise [...]
Return ret <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
Unreachable unreachable
UnwindResume resume 或调用 personality 函数

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

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

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

16.4.1 标量类型映射

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

Rust 类型 LLVM 类型 说明
bool i8(不是 i1 内存中用 i8 表示,运算时可能用 i1
u8 / i8 i8
u16 / i16 i16
u32 / i32 i32
u64 / i64 i64
u128 / i128 i128
usize / isize i64(64 位平台) 取决于目标平台指针宽度
f32 float
f64 double
*const T / *mut T ptr LLVM 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 本身是一个全局常量,布局为:

rust 复制代码
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 构建

graph LR subgraph "Debug 构建(opt-level=0)" D1["MIR"] --> D2["LLVM IR
无优化"] D2 --> D3["机器码
每行对应源码"] end subgraph "Release 构建(opt-level=2)" R1["MIR"] --> R2["LLVM IR"] R2 --> R3["mem2reg + SROA"] R3 --> R4["内联 + GVN"] R4 --> R5["循环优化 + 向量化"] R5 --> R6["机器码
高度优化"] end style D3 fill:#f59e0b,color:#fff,stroke:none style R6 fill:#10b981,color:#fff,stroke:none
维度 Debug(O0) Release(O2) O3 Os Oz
函数内联 几乎不内联 大量内联 更激进内联 适度内联 最少内联
循环展开 更激进 禁用 禁用
自动向量化 禁用
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)协调整个链接过程:

graph TB subgraph "编译产出" OBJ1["CGU1.o"] OBJ2["CGU2.o"] OBJ3["CGU3.o"] META["metadata.o"] ALLOC["allocator_shim.o"] end subgraph "外部依赖" RLIB["上游 crate(.rlib)"] NATIVE["系统库(-lm, -lpthread)"] CRT["CRT 启动文件"] end subgraph "链接器" LINKER["ld / lld / link.exe"] end subgraph "产出" BIN["最终二进制"] end OBJ1 --> LINKER OBJ2 --> LINKER OBJ3 --> LINKER META --> LINKER ALLOC --> LINKER RLIB --> LINKER NATIVE --> LINKER CRT --> LINKER LINKER --> BIN style LINKER fill:#f59e0b,color:#fff,stroke:none style BIN fill:#10b981,color:#fff,stroke:none

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/(调试信息)等模块。

维度 LLVM Cranelift
编译速度 慢(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 代码生成的完整流水线

graph TB MIR["MIR(已通过借用检查)"] --> MONO["单态化收集"] MONO --> PART["CGU 划分"] PART --> PAR["并行翻译 + LLVM 优化"] PAR --> LTO_CHECK{"LTO?"} LTO_CHECK -->|否| LINK["链接"] LTO_CHECK -->|是| LTO_MERGE["LTO 合并优化"] LTO_MERGE --> LINK LINK --> BINARY["最终二进制"] style MIR fill:#6366f1,color:#fff,stroke:none style MONO fill:#8b5cf6,color:#fff,stroke:none style PAR fill:#f59e0b,color:#fff,stroke:none style LTO_MERGE fill:#ef4444,color:#fff,stroke:none style BINARY fill:#10b981,color:#fff,stroke:none

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

16.15 小结

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

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

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

相关推荐
杨艺韬2 小时前
Rust编译器原理-第5章 内存布局:编译器如何排列数据
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第3章 借用检查器:编译器如何证明内存安全
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第9章 async/await:状态机的编译器变换
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第8章 Trait Object 与虚表:运行时多态的内存布局
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第13章 FFI:与 C 世界的桥梁
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第4章 生命周期:编译器如何推断引用的有效范围
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第7章 Trait 静态分发:零成本抽象的编译器实现
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第18章 设计哲学与架构决策
rust·编译器
杨艺韬2 小时前
Rust编译器原理-第12章 unsafe:安全抽象的逃生舱
rust·编译器