Rust 编译器与运行时揭秘

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

作者 杨艺韬 · 11,000 字 · 发布于 · 更新于

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

本章要点

  • Rust 的代码生成采用双层架构:rustc_codegen_ssa(后端无关抽象层)+ rustc_codegen_llvm(LLVM 具体实现)
  • MIR → LLVM IR 翻译过程中,lifetime 被擦除,所有权与借用规则只以 noalias、readonly 等参数属性的形式留下痕迹
  • LLVM IR 中的类型极其简单:i32、i64、ptr、float 等——Rust 的复杂类型被降级为原始内存布局
  • 泛型在代码生成阶段完成单态化,每个具体类型实例生成独立的机器码
  • LLVM 优化流水线分阶段:预链接(PreLink*)→ LTO(ThinLTO / FatLTO),不同优化级别差异巨大
  • 链接是代码生成的最后一环:将目标文件、元数据、分配器 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:#333,stroke:none
    style L2 fill:#f59e0b,color:#333,stroke:none
    style L3 fill:#f59e0b,color:#333,stroke:none
    style L4 fill:#f59e0b,color:#333,stroke:none
    style L5 fill:#f59e0b,color:#333,stroke:none
    style L6 fill:#f59e0b,color:#333,stroke:none
    style C1 fill:#10b981,color:#333,stroke:none

这种双层设计带来的核心好处是:MIR 到后端 IR 的翻译逻辑只需要编写一次。codegen_statement、codegen_rvalue、codegen_terminator 等核心翻译函数由实现了 BuilderMethods 的后端共享——它们通过 trait 方法调用后端特定的 IR 构建操作。1.96.0 树里实现了这套 builder trait 的是 LLVM 后端和 GCC 后端(compiler/rustc_codegen_gcc/src/builder.rs:513);Cranelift 后端只实现了入口 trait CodegenBackend(compiler/rustc_codegen_cranelift/src/lib.rs:126),MIR 到 Cranelift IR 的翻译是它自己写的(compiler/rustc_codegen_cranelift/src/base.rs 等),图中因此只画了 CodegenBackend 一条连线。

16.1.2 trait 体系全景

在 compiler/rustc_codegen_ssa/src/traits/ 目录下,定义了后端要实现的 trait,主要的几个如下:

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

这些 trait 通过关联类型实现了类型级别的抽象。type Value、type BasicBlock、type Function 等关联类型定义在公共父 trait BackendTypes 里(compiler/rustc_codegen_ssa/src/traits/backend.rs:22-27),BuilderMethods 等 trait 经由父 trait 继承它们;对于 LLVM 后端它们对应 LLVM 的 Value*、BasicBlock*、Function*,对于 GCC 后端则对应 libgccjit 的类型。

16.1.3 CodegenCx 与 FunctionCx——两层上下文

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

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

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

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

// compiler/rustc_codegen_llvm/src/context.rs
// CodegenCx<'ll, 'tcx> 是 GenericCx<'ll, FullCx<'ll, 'tcx>> 的类型别名(第 91 行)
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(定义于 compiler/rustc_codegen_ssa/src/mir/mod.rs)是函数级上下文。每当翻译一个 MIR 函数时,就会创建一个 FunctionCx:

// compiler/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,               // 后端函数对象
    cx: &'a Bx::CodegenCx,            // 模块级上下文的引用
    fn_abi: &'tcx FnAbi<'tcx, Ty<'tcx>>, // 函数 ABI 信息
    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 可复用上次结果)。默认值取决于是否开启增量编译(compiler/rustc_session/src/session.rs:804-817 的 Session::codegen_units,目标规格另有 default_codegen_units 时以它为准):增量模式用 256 个 CGU,非增量用 16 个,可通过 -C codegen-units=N 覆盖。Cargo 的内置 profile 不设 codegen_units(见 Cargo 仓库 rust-lang/cargo 中与 1.96 配套版本的 src/cargo/core/profiles.rs,其中 Profile::default() 为 codegen_units: None,也就不传 -C codegen-units),它决定的是增量开关:dev profile incremental: true、release 为 false——所以 dev 下工作区成员落到 256,release 落到 16。

16.2.1 CGU 的编译流程

每个 CGU 的编译流程如下(前五步在 compile_codegen_unit 函数里完成,LLVM 优化和写目标文件由 compiler/rustc_codegen_ssa/src/back/write.rs 调度的后台工作线程完成):

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:#7c3aed,color:#fff,stroke:none
    style D fill:#9333ea,color:#fff,stroke:none
    style E fill:#c084fc,color:#333,stroke:none
    style F fill:#f59e0b,color:#333,stroke:none
    style G fill:#10b981,color:#333,stroke:none

在 compiler/rustc_codegen_llvm/src/base.rs 中,compile_codegen_unit 内部的 module_codegen 函数展示了前五步:

// compiler/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 = cx.codegen_unit.items_in_deterministic_order(cx.tcx);
        for &(mono_item, data) in &mono_items {
            mono_item.predefine::<Builder<'_, '_, '_>>(
                &mut cx, cgu_name.as_str(), data.linkage, data.visibility);
        }

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

        // ... 入口包装函数、llvm.used 等(省略)

        // 完成调试信息
        if cx.sess().opts.debuginfo != DebugInfo::None {
            cx.debuginfo_finalize();
        }
    }
    ModuleCodegen::new_regular(cgu_name.to_string(), llvm_module)
}

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

16.3 MIR → LLVM IR 翻译

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

16.3.1 函数框架的建立

// compiler/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 tcx = cx.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);

    // ... 构造 FunctionCx `fx`(省略)
    let traversal_order = traversal::mono_reachable_reverse_postorder(mir, tcx, instance);

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

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

局部变量的存储决策是一个关键优化。non_ssa_locals 分析(compiler/rustc_codegen_ssa/src/mir/analyze.rs)判断每个局部变量是否需要栈分配。如果变量的布局在后端是立即数或标量对(is_backend_immediate / is_backend_scalar_pair)、只有一处定义且该定义支配所有使用、从未被取地址,也没有经投影被写入(如 _2.0 = ...;只读的字段投影是允许的),就可以作为 SSA 值处理,避免不必要的 alloca/load/store。

16.3.2 语句的翻译

MIR 语句翻译在 compiler/rustc_codegen_ssa/src/mir/statement.rs 中。核心是对 statement.kind 的模式匹配:Assign 根据目标位置是 Place(栈上)还是 PendingOperand(SSA 值),分别调用 codegen_rvalue 或 codegen_rvalue_operand;StorageLive/StorageDead 对栈上的局部变量翻译为 LLVM 的 llvm.lifetime.start/llvm.lifetime.end intrinsic,告诉 LLVM 栈变量的活跃范围以复用栈空间(opt-level=0 且未开 sanitizer 时不发出,见 compiler/rustc_codegen_llvm/src/builder.rs:1771 的 emit_lifetime_markers 判断)。

16.3.3 右值(Rvalue)的翻译

右值的翻译是最复杂的部分,在 compiler/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 计算地址,无投影时直接用 place 的指针
Cast(IntToInt, ..) trunc、zext 或 sext(同宽时不产生指令)
Cast(Unsize, ..) 构造胖指针(数据指针 + vtable/长度)
Repeat(elem, count) 循环或 memset
Aggregate(...) 逐字段写入内存;能以 SSA 值表示的小聚合走 OperandValue,不落内存

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

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 基本块以一个终结符结尾。终结符的翻译在 compiler/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, destination, target, unwind } call <type> @func(args...) + br label %dest
Drop { place, target } 调用 drop_in_place + br label %target
Assert { cond, expected, msg, target, unwind } br i1 %cond, label %target, label %panic_bb
Unreachable unreachable
UnwindResume resume(MSVC 等使用 funclet 的目标上是 cleanupret)

表里的字段名以 1.96.0 的 TerminatorKind(compiler/rustc_middle/src/mir/syntax.rs)为准。两个容易沿用旧名的地方:Call 的目标 place 叫 destination(不是 dest),而 Assert / Drop / Call 的异常边都叫 unwind: UnwindAction——cleanup 这个旧名早已不在了。

函数调用的翻译尤其复杂。编译器需要根据 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 类型。翻译逻辑在 compiler/rustc_codegen_llvm/src/type_of.rs 中的 uncached_llvm_type 函数实现:

// 简化的 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。例如 #[repr(C)] struct Example { a: u8, b: u32, c: u16 } 对应 LLVM 类型 %Example = type { i8, [3 x i8], i32, i16, [1 x i16] }(rustc 1.96.0 -C opt-level=0 --emit=llvm-ir 实测),其中 [3 x i8] 和 [1 x i16] 是编译器插入的填充——填充数组的元素宽度按前后字段的有效对齐选取(type_padding_filler),所以尾部 2 字节写成 [1 x i16] 而不是 [2 x i8]。注意这里必须写 repr(C):默认的 repr(Rust) 布局会按对齐重排字段,得到的 LLVM 类型完全不同。

16.4.3 枚举的表示

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

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

enum Color { Red, Green, Blue }
// LLVM 类型:i8(变体超过 256 个时换用更宽的整数)

带数据枚举:

enum Shape {
    Circle(f64),        // 变体 0
    Rectangle(f64, f64), // 变体 1
}
// LLVM 类型(rustc 1.96.0 -C opt-level=0 实测):%Shape = type { i64, [2 x i64] }
// i64 = discriminant(判别值):本可用 1 字节,但负载按 8 字节对齐,
//       布局计算按首字段对齐把 tag 扩宽到 8 字节(compiler/rustc_abi/src/layout.rs:942-947;
//       -Zprint-type-sizes 显示 discriminant: 8 bytes)
// [2 x i64] = 16 字节的负载区,只是填充;各变体的字段在访问时按偏移读写

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

// 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 函数指针(类型无需 drop 时为空指针)
    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
SimdScalableVector { element, count, number_of_vectors } 可伸缩向量(SVE 等) svint32_t 类目标类型
Memory { sized } 内存中的聚合类型 struct, 大 enum

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

16.5 函数 ABI 与调用约定

16.5.1 单态化(Monomorphization)

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::<f64>。-C opt-level=0 下它们分别调用 <i32 as Add>::add 和 <f64 as Add>::add,前者展开为带溢出检查的 llvm.sadd.with.overflow.i32,后者就是一条 fadd double;优化构建里这些调用会被内联。FunctionCx::monomorphize 方法通过 instantiate_mir_and_normalize_erasing_regions 在翻译过程中替换泛型参数。

16.5.2 FnAbi 与参数传递模式

FnAbi 决定了每个参数和返回值如何在机器级别传递。每个参数有一个 PassMode,定义在 compiler/rustc_target/src/callconv/mod.rs 第 39 行:

pub enum PassMode {
    Ignore,                                  // ZST
    Direct(ArgAttributes),                   // Scalar / Vector(寄存器)
    Pair(ArgAttributes, ArgAttributes),      // ScalarPair(两个寄存器)
    Cast { pad_i32: bool, cast: Box<CastTarget> },  // 整形拍平,如 struct{u8,u8,u8,u8} → i32
    Indirect { attrs, meta_attrs, on_stack },// 超出寄存器大小时传指针
}

五种模式的选择逻辑并不复杂,但有几个Rust 特有的细节值得关注:

1. Indirect 模式的两种姿态——on_stack: false 时传的是普通 pointer;on_stack: true 时对应 LLVM 的 byval attribute,callee 按栈偏移取值。byval 使用与 Rust 类型等大的字节数组,不靠 LLVM 自己推 struct 布局(compiler/rustc_target/src/callconv/mod.rs line 67 源码注释原话:"which ensures that padding is preserved and that we do not rely on LLVM's struct layout")——因为 LLVM 对 struct 的 padding 推断在某些目标平台不完全符合 Rust 期望的 ABI。

2. Indirect 自动具备的属性(同文件 indirect_pass_mode,line 407–410)——凡是通过隐式指针传递的参数,编译器给指针自动加 NoAlias + CapturesAddress + NonNull + NoUndef。这些属性把"callee 拿到的是一份栈上独占副本"这一 Rust ABI 约定显式告诉 LLVM。

3. meta_attrs 只对 unsized 参数有值——按值传递的 dyn Trait、str、[T] 这类 unsized 参数(需要 unsized_fn_params 特性)会以胖指针的形式间接传递,meta_attrs 存胖指针第二字(vtable/length)的属性。&[T]、&dyn Trait 这样普通的胖指针参数是 ScalarPair,走的是 Pair。Indirect 是唯一支持 unsized 参数的 PassMode。

4. make_direct_deprecated(line 423)——源码里一个带 #[track_caller] 标记的"遗憾"函数,专门用来处理 issue #115666:历史上某些 wasm 目标错把 Aggregate 当 Direct 传,已经泄漏到 ABI 里,没法改回来。这段兼容代码直接说明了"ABI 一旦定好、改起来要付出什么代价"。

16.5.3 LLVM 函数属性:Rust 保证转化为优化机会

属性的加装代码在 compiler/rustc_ty_utils/src/abi.rs 的 arg_attrs_for_rust_scalar 函数里。常听到的简化说法是"&T 标 readonly,&mut T 标 noalias"——这个说法有重要边界条件,简化掉之后会误导优化直觉。真实源码的判定(line 424–437):

let no_alias = match kind {
    PointerKind::SharedRef  { frozen }       => frozen,              // ① 见下
    PointerKind::MutableRef { unpin }        => unpin && noalias_mut_ref,  // ②
    PointerKind::Box { unpin, global }       => unpin && global && noalias_for_box,
};
// ③ 返回值位置永远不加 NoAlias —— LLVM 语义陷阱
if no_alias && !is_return {
    attrs.set(ArgAttribute::NoAlias);
}

// ④ ReadOnly 只给 frozen 共享引用,且也不加在返回值
if matches!(kind, PointerKind::SharedRef { frozen: true }) && !is_return {
    attrs.set(ArgAttribute::ReadOnly);
    attrs.set(ArgAttribute::CapturesReadOnly);
}

四条关键边界,每一条都对应一个容易被教学材料掩盖的 Rust/LLVM 交互真相:

① &T 得到 NoAlias/ReadOnly 的前提是 frozen: true——即 T 类型不含内部可变性(UnsafeCell)。&Mutex<T>、&Cell<T>、&AtomicI32 这些共享引用传入函数时,由于 UnsafeCell 承诺"外部共享的也可能被改",LLVM 不能认为它是 readonly,也不能认为它 no-alias。这把 Rust "&T 不可变"这句口号和实际 ABI 保证的差距精确地暴露出来:口头上 &T 不变、ABI 层面是否"真不变"还要看类型是否 frozen。

② &mut T 得到 NoAlias 还要再过两道关:unpin: true(类型不是 !Unpin,即不包含 PhantomPinned/自引用 future)且 noalias_mut_ref 为真——它来自 -Zmutable-noalias,默认开启。这两条都是历史包袱:源码注释写明,LLVM 12 之前 noalias 有已知 miscompile(#54878),当时整体关闭过,如今只保留这个 -Z 开关;而 unpin 条件针对的是自引用数据(包括 async fn 生成的状态机)与 noalias 语义的冲突(issue #63818,至今仍开着),目前的缓解办法就是把 !Unpin 类型摘出来。

③ 返回值位置永不加 NoAlias——源码行上方的注释引用了 unsafe-code-guidelines issue #385:LLVM 对 return-position noalias 有非常反直觉的语义(基本等于"调用者保证拿回的指针没被别处记住"),实际 Rust 函数做不到这个承诺,索性一律不加。

④ &T 的 ReadOnly 和 CapturesReadOnly——后者属于 LLVM 的 captures(...) 属性族,rustc 把它降为 captures(address, read_provenance)(compiler/rustc_llvm/llvm-wrapper/RustWrapper.cpp:523-526):指针的地址可以被记下来,但经由被记下的指针只能读、不能写。

除此之外,编译器还会根据类型的 PointeeInfo 自动算 dereferenceable(N)(引用指向的内存至少 N 字节有效,N 就是 pointee 的 layout size)和 align N(对齐)。noundef 则来自"Rust 保证初始化过"这一语言级不变式。综合起来,一个 &i32 参数由 rustc 生成的 LLVM 属性是 ptr noalias noundef readonly align 4 captures(address, read_provenance) dereferenceable(4)(rustc 1.96.0 -C opt-level=2 -C no-prepopulate-passes 实测,即 LLVM 优化前;nonnull 不单独出现,因为 compiler/rustc_codegen_llvm/src/abi.rs:77-83 在写出 dereferenceable 时去掉了它;经过 LLVM 优化后,captures 常被进一步推断为 captures(none))——一个简单的类型就给 LLVM 带去了这么多优化信息,这是带泛型、零开销抽象的 Rust 代码能接近手写 C 运行性能的原因之一。

重要提醒:compiler/rustc_codegen_llvm/src/abi.rs:74-96 规定,noalias、nonnull、readonly、noundef、dereferenceable 和 captures 这些优化属性只在 OptLevel != No 时才写出(开了 MemorySanitizer 时例外地保留 noundef);align、inreg、zeroext/signext 则始终保留。所以 -C opt-level=0 下 &i32 参数只剩 ptr align 4。

16.6 LLVM 优化流水线

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

16.6.1 优化级别映射

Rust 的 -C opt-level 直接映射到 LLVM 优化级别:0 → O0、1 → O1、2 → O2、3 → O3(cargo [profile.release] 的默认值是 3,见 Cargo 仓库 rust-lang/cargo 中与 1.96 配套版本的 src/cargo/core/profiles.rs 里的 default_release():opt_level: "3".into())、s → Os(优化大小)、z → Oz(激进优化大小)。其中 s 和 z 并不对应独立的 LLVM 代码生成级别——compiler/rustc_codegen_llvm/src/back/write.rs:140-149 的 to_llvm_opt_settings 把 OptLevel 映射成 (级别, 尺寸) 二元组,Size 和 SizeMin 拿到的级别都是 CodeGenOptLevel::Default(和 -C opt-level=2 同一级),区别全在第二个分量:llvm::CodeGenOptSizeDefault 与 llvm::CodeGenOptSizeAggressive(两个常量定义在 compiler/rustc_codegen_llvm/src/llvm/mod.rs:170-171,值分别是 1 和 2;其余四档统一是 CodeGenOptSizeNone)。

16.6.2 优化的阶段划分

LLVM 优化分为多个阶段:PreLinkNoLTO、PreLinkThinLTO、PreLinkFatLTO(预链接)和 ThinLTO、FatLTO(后链接)。分阶段是因为如果后面还要做 LTO,预链接阶段会把一部分优化推迟,把跨模块优化机会留给 LTO:compiler/rustc_llvm/llvm-wrapper/PassWrapper.cpp 里 PreLinkThinLTO 和 PreLinkFatLTO 都走 LLVM 的 buildThinLTOPreLinkDefaultPipeline,而不是完整的 buildPerModuleDefaultPipeline。

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 指令。compiler/rustc_codegen_ssa/src/back/write.rs 里 ModuleConfig 的构造给出了精确条件:循环向量化只在 opt-level=2/3 打开,SLP 向量化只在 opt-level=3 打开;-Os/-Oz 两者都关闭。

循环优化——包括展开(Unrolling)、不变量外提(LICM)、强度削减等。compiler/rustc_codegen_llvm/src/back/write.rs:596-597 里 unroll_loops = opt_level != config::OptLevel::Size && opt_level != config::OptLevel::SizeMin——即 -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<br/>无优化"]
        D2 --> D3["机器码<br/>每行对应源码"]
    end
    subgraph "Release 构建(opt-level=2/3)"
        R1["MIR"] --> R2["LLVM IR"]
        R2 --> R3["mem2reg + SROA"]
        R3 --> R4["内联 + GVN"]
        R4 --> R5["循环优化 + 向量化"]
        R5 --> R6["机器码<br/>高度优化"]
    end
    style D3 fill:#f59e0b,color:#333,stroke:none
    style R6 fill:#10b981,color:#333,stroke:none
维度 Debug(O0) O2 O3(Cargo release 默认) Os Oz
函数内联 几乎不内联 大量内联 更激进内联 适度内联 最少内联
循环展开 无 有 更激进 禁用 禁用
循环向量化 无 有 有 禁用 禁用
SLP 向量化 无 无 有 禁用 禁用
mem2reg/SROA 无 有 有 有 有
编译速度 最快 较慢 最慢 较慢 较慢
运行速度 最慢 优 通常最优 略逊于 O2 最保守
二进制大小 大 中等 通常大于 O2 较小 最小

(表中循环展开与向量化三行直接对应三个布尔量的取值条件,注意它们分处两个同名文件:unroll_loops 在 compiler/rustc_codegen_llvm/src/back/write.rs:596,vectorize_loop / vectorize_slp 在 compiler/rustc_codegen_ssa/src/back/write.rs:221-225 由 ModuleConfig 构造时定下;速度与体积一栏只给定性方向,具体倍数完全取决于代码本身。)

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 模式

# Cargo.toml(三选一,同一个表里 lto 只能写一次)
[profile.release]
lto = false     # 默认:crate 内 ThinLocal LTO(opt-level > 0 且 CGU 多于 1 个时)
# lto = "thin"  # Thin LTO:跨 crate,保持并行性
# lto = true    # Fat LTO:完全合并所有模块

ThinLocal(默认)只在同一 crate 的不同 CGU 之间做 LTO,判定逻辑在 compiler/rustc_session/src/session.rs 的 Session::lto:未指定 -C lto 时,CGU 只有 1 个或 opt-level=0 都返回 Lto::No,否则返回 Lto::ThinLocal;要连它也关掉,写 lto = "off"。Thin LTO 跨 crate 进行分析和有选择的导入,但不完全合并模块。Fat LTO 将所有模块合并为一个巨大的 LLVM Module 再优化,提供最大优化机会但编译最慢。实现在 compiler/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 函数(compiler/rustc_codegen_llvm/src/back/write.rs)收集所有平台参数并创建它。关键参数包括:目标三元组(如 x86_64-unknown-linux-gnu)、CPU 型号(如 generic、apple-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 函数(compiler/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:#333,stroke:none
    style BIN fill:#10b981,color:#333,stroke:none

16.9.2 链接器与 rlib

Rust 支持多种链接器:cc(gcc/clang 驱动)、lld(LLVM 链接器,速度快)、link.exe(Windows MSVC)、rust-lld(随工具链分发的 lld)。链接器的选择写在 target spec 里:compiler/rustc_target/src/spec/base/linux_gnu.rs:9-12 中的 base.linker_flavor = LinkerFlavor::Gnu(Cc::Yes, Lld::Yes)。注意它不是无条件生效的——那两行裹在 if option_env!("CFG_DEFAULT_LINKER_SELF_CONTAINED_LLD_CC").is_some() 里,是一个构建 rustc 时才决定的开关;官方分发的工具链打开了它,所以自 1.90.0 起 x86_64-unknown-linux-gnu 默认经由 cc 驱动调用 rust-lld(PR #140525),而自己从源码构建、没设这个环境变量的 rustc 仍然落回 TargetOptions::default() 的 LinkerFlavor::Gnu(Cc::Yes, Lld::No)(compiler/rustc_target/src/spec/mod.rs:2802)。

Rust 的 rlib 格式是一个 ar 归档,包含:元数据文件(lib.rmeta,供下游 crate 编译时使用)和目标文件(*.rcgu.o,编译后的机器码);供 LTO 使用的 LLVM bitcode 默认嵌在这些目标文件的 .llvmbc 段里(-C embed-bitcode=no 时不嵌),而不是单独的归档成员(rustc 1.96.0 实测:ar t 只列出 lib.rmeta 与 .rcgu.o,objdump -h 可见 .llvmbc)。链接时,编译器从 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)。调试信息由 compiler/rustc_codegen_llvm/src/debuginfo/ 模块负责,核心上下文 CodegenUnitDebugContext 包含 LLVM DIBuilder、文件缓存、类型缓存和命名空间映射。

16.10.2 DWARF 版本与平台适配

编译器根据目标平台选择 DWARF 版本:TargetOptions 默认是 4(compiler/rustc_target/src/spec/mod.rs:2839),Android 和 FreeBSD/NetBSD/OpenBSD/DragonFly 的 base 规格降到 2,AIX 为 3(均见 compiler/rustc_target/src/spec/base/ 下对应文件)。对于 PDB 平台则设置 CodeView 标志。在 LTO 合并多个 CGU 时,DWARF 版本取各模块的最大值。

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

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

调试信息的详细程度由 -C debuginfo= 控制。compiler/rustc_session/src/options.rs 的 parse_debuginfo 给出全部取值:0/none、line-directives-only、line-tables-only、1/limited、2/full——注意 1 对应 DebugInfo::Limited 而非“只有行表”,后者要显式写 line-tables-only。

16.11 代码大小优化

16.11.1 -Os 和 -Oz

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

// 在 Os/Oz 模式下禁用增加代码大小的优化
let unroll_loops = opt_level != config::OptLevel::Size
    && opt_level != config::OptLevel::SizeMin;
// vectorize_loop / vectorize_slp 来自 ModuleConfig,Os/Oz 下均为 false

16.11.2 #[inline] 属性的影响

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

#[inline] 的一个重要副作用是:它使函数的 MIR 被序列化到 rlib 中,允许下游 crate 在自己的编译过程中内联该函数。没有 #[inline] 的函数一般只能在同一 crate 内被内联,但有一个例外:非增量的优化构建里,rustc 会自动把足够小、不含调用的叶子函数当作可跨 crate 内联(compiler/rustc_mir_transform/src/cross_crate_inline.rs,阈值由 -Zcross-crate-inline-threshold 控制,默认 100 条语句)。

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 CodegenBackend,并复用其中的链接等公共逻辑;MIR 到 Cranelift IR 的翻译不走 BuilderMethods,是它自己实现的。其源码位于 compiler/rustc_codegen_cranelift/src/,包含 base.rs(核心翻译)、abi/(ABI处理)、debuginfo/(调试信息)等模块。

维度 LLVM Cranelift
编译速度 慢(debug 构建也慢) 明显更快(其设计目标)
运行时性能 最优 低于 LLVM
优化级别 O0 到 O3,完整优化 有限优化
LTO 支持 完整支持 不支持
SIMD 支持 完整 有限
平台支持 几乎所有平台 x86_64 / AArch64 / riscv64 / s390x(见后端 Readme 的平台矩阵)
成熟度 生产级 nightly component,实验性
使用场景 release 构建、生产环境 开发阶段的快速迭代

16.12.3 使用 Cranelift

后端只随 nightly 分发。compiler/rustc_codegen_cranelift/Readme.md 给出的安装与启用方式是:

rustup component add rustc-codegen-cranelift-preview --toolchain nightly
CARGO_PROFILE_DEV_CODEGEN_BACKEND=cranelift cargo +nightly build -Zcodegen-backend

也可以在项目根目录下 .cargo 目录里的 config.toml 里写 [unstable] codegen-backend = true 加 [profile.dev] codegen-backend = "cranelift"。推荐在 debug 构建时使用 Cranelift 加速编译,release 构建时切回 LLVM 获取最佳性能。

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

16.13.1 查看 LLVM IR

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

两点提醒:Cargo 的 dev profile 带溢出检查和完整调试信息,所以"未优化"的 IR 里会看到 llvm.sadd.with.overflow 和 .dbg.spill;在 lib crate 里用 --release 看小函数时,它们可能因上一节说的自动跨 crate 内联而根本不在本 crate 生成。下面的输出都是 rustc 1.96.0(rustc -vV 显示 LLVM version: 22.1.2)在 x86_64-unknown-linux-gnu 上直接编译单个文件(--crate-type=lib --crate-name=example -C codegen-units=1)得到的,符号用 stable 默认的 legacy 修饰;为了让优化后的小函数留在本 crate,优化版额外加了 RUSTC_BOOTSTRAP=1 -Zcross-crate-inline-threshold=never -Zunstable-options -C symbol-mangling-version=legacy(RUSTC_BOOTSTRAP 会让默认修饰切到 v0,所以显式指定 legacy)。符号里的哈希随 crate 元数据变化,自己编译时会不同。

16.13.2 示例一:简单函数

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

LLVM IR(未优化,-C opt-level=0 -g,省略 !dbg 元数据与 #dbg_declare 记录):

define i32 @_ZN7example3add17hd8ef676bba71e0d4E(i32 %a, i32 %b) unnamed_addr #0 {
start:
  %b.dbg.spill = alloca [4 x i8], align 4
  %a.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
  %0 = call { i32, i1 } @llvm.sadd.with.overflow.i32(i32 %a, i32 %b)
  %_3.0 = extractvalue { i32, i1 } %0, 0
  %_3.1 = extractvalue { i32, i1 } %0, 1
  br i1 %_3.1, label %panic, label %bb1

bb1:                                              ; preds = %start
  ret i32 %_3.0

panic:                                            ; preds = %start
; call core::panicking::panic_const::panic_const_add_overflow
  call void @_RNvNtNtCs27Vx93FoQ6z_4core9panicking11panic_const24panic_const_add_overflow(ptr align 8 @alloc_8f1a6a9835f9f23540f8f2652baab04b) #8
  unreachable
}

注意未优化的 IR 中有 alloca 和 store——这是为了调试器能够查看参数值(不加 -g 就没有这两个 .dbg.spill)。debug 构建默认开启溢出检查,所以 a + b 变成了 llvm.sadd.with.overflow 加一个跳到 panic 的分支;被调用的 panic_const_add_overflow 来自预编译的标准库(其符号是 v0 修饰),本 crate 里只有它的 declare。

LLVM IR(优化后,-C opt-level=2):

define noundef i32 @_ZN7example3add17hd8ef676bba71e0d4E(i32 noundef %a, i32 noundef %b) unnamed_addr #2 {
start:
  %_0 = add i32 %b, %a
  ret i32 %_0
}

优化构建不做溢出检查(release profile 默认关闭),也不带调试信息,剩下一条 add;参数上多了只在优化时写出的 noundef。

x86-64 汇编(-C opt-level=2 --emit=asm,Intel 语法):

_ZN7example3add17hd8ef676bba71e0d4E:
	lea	eax, [rdi + rsi]
	ret

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

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

优化后(-C opt-level=2)的 LLVM IR 中,p 的参数签名为 ptr noalias noundef readonly align 8 captures(none) dereferenceable(16)。函数体里不出现结构体类型,字段按字节偏移访问:p.x 直接 load double, ptr %p,p.y 先 getelementptr inbounds nuw i8, ptr %p, i64 8 再加载。对应的 x86-64 汇编只剩少数几条 SSE 指令:两次 movsd 加载、两次 mulsd 平方、一次 addsd 求和、一次 sqrtsd 开方(外加一条清零 xmm0 的 xorps)。

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

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

在 -C opt-level=2 下,LLVM 会将整个迭代器链内联并融合为一个循环。最终的代码在语义上等价于:

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
}

用 rustc 1.96.0 -C opt-level=2 实测两个版本的汇编:两者都被向量化成每轮处理 8 个 i32 的 SSE 循环(pand 取奇偶、pcmpeqd 比较、paddd 累加),再接一段标量收尾循环;向量循环体的运算指令一一对应,差别只在寻址和计数方式(迭代器版用字节偏移,手写版用元素下标)以及进入/退出循环的簿记。这就是 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:#7c3aed,color:#fff,stroke:none
    style PAR fill:#f59e0b,color:#333,stroke:none
    style LTO_MERGE fill:#dc2626,color:#fff,stroke:none
    style BINARY fill:#10b981,color:#333,stroke:none

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

16.15 实测:LLVM 与 Cranelift 两个后端 + SSA 共享层约 78,300 行

把本章贯穿讨论的代码生成相关 crate 实测(口径:各目录下全部 .rs 文件的物理行,find <目录> -name '*.rs' | xargs cat | wc -l;树里还有一个 GCC 后端 compiler/rustc_codegen_gcc,本章没有展开,未计入)——

crate 行 角色
compiler/rustc_codegen_llvm 29116 LLVM 后端实现(§16.1.1 双层架构的 LLVM 端)
compiler/rustc_codegen_ssa 27042 共享 SSA 层——MIR→IR 翻译框架 + linker + 跨后端 trait(§16.1.2 trait 体系全景)
compiler/rustc_codegen_cranelift 22148 §16.12 提到的 Cranelift 替代后端(debug 构建快速编译用;含 build_system/、example/ 等目录,其中 src/ 为 16709 行)
三个目录合计 78,306 —

rustc_codegen_llvm 内部最大文件——

文件 行 角色
compiler/rustc_codegen_llvm/src/intrinsic.rs 3062 Rust intrinsic 在 LLVM 后端的翻译;从第 1640 行起的 generic_simd_intrinsic 占了近一半(size_of 在 const eval 中求值、unreachable 由 rustc_codegen_ssa 处理,都不在这里)
compiler/rustc_codegen_llvm/src/llvm/ffi.rs 2573 LLVM C API 的 Rust FFI 绑定
compiler/rustc_codegen_llvm/src/builder.rs 1982 实现 BuilderMethods trait(§16.3 一切翻译的最终落脚点)
compiler/rustc_codegen_llvm/src/debuginfo/metadata.rs 1819 §16.10 调试信息生成的 DWARF metadata
compiler/rustc_codegen_llvm/src/asm.rs 1468 inline asm 处理
compiler/rustc_codegen_llvm/src/back/write.rs 1394 写出 .o 目标文件
compiler/rustc_codegen_llvm/src/va_arg.rs 1195 C variadic args 跨平台处理

rustc_codegen_ssa 内部最大文件——

文件 行 角色
compiler/rustc_codegen_ssa/src/back/link.rs 3624 §16.9 链接主流程——所有 platform 的链接器调用编排
compiler/rustc_codegen_ssa/src/back/write.rs 2329 输出写出(区别于 llvm 的同名文件)
compiler/rustc_codegen_ssa/src/back/linker.rs 2169 链接器抽象(gnu / lld / msvc / wasm-ld 等)
compiler/rustc_codegen_ssa/src/mir/block.rs 2165 §16.3.4 终结符翻译 + 基本块编排
compiler/rustc_codegen_ssa/src/errors.rs 1278 错误诊断
compiler/rustc_codegen_ssa/src/mir/operand.rs 1158 §16.3.3 右值 / OperandRef
compiler/rustc_codegen_ssa/src/base.rs 1136 顶层入口

两条值得记住的物理事实——

  1. intrinsic.rs 3062 行 = rustc_codegen_llvm(29116 行)的 10.5%——单一文件负责把 Rust intrinsic 翻译成 LLVM 指令或 LLVM intrinsic——是 §6.11 实测的 InstanceKind::Intrinsic 变体在 codegen 阶段的对应——标准库里一行 core::intrinsics::xxx 调用,背后是后端里逐个 intrinsic 的翻译代码
  2. 链接器编排 compiler/rustc_codegen_ssa/src/back/link.rs 3624 + compiler/rustc_codegen_ssa/src/back/linker.rs 2169 = 5793 行——是 ssa 共享层里最大的两个文件(合占 27042 行的 21%;作为对照,compiler/rustc_codegen_ssa/src/mir/ 目录共 8013 行)——呼应 §16.9 讲的链接流程:这 5793 行处理 platform-specific 的 linker flag、rlib 解包、导出符号过滤、native lib 链接顺序等——Rust 跨平台编译承诺背后的真实工程量

和其他章节的数字怎么比——本节三个数都是整 crate 的物理行,只能与同样按整 crate 统计的数字并列,例如借用检查 compiler/rustc_borrowck 34458 行。其余各章的实测数字多是作者挑选的若干文件(有的还包括 library/ 下的标准库代码),统计范围各不相同,不能与本节相加,也不能拿来算占比。

16.16 小结

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

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

物理事实:LLVM 后端、SSA 共享层、Cranelift 后端三个目录的 .rs 共 78,306 行(rustc_codegen_llvm 29116 + rustc_codegen_ssa 27042 + rustc_codegen_cranelift 22148,GCC 后端未计入);intrinsic.rs 3062 行负责 Rust intrinsic 在 LLVM 后端的翻译,是 ch06 §6.11 InstanceKind::Intrinsic 在 codegen 阶段的对应;链接器编排 link.rs 3624 + linker.rs 2169 = 5793 行占共享 SSA 层 21%——Rust 跨平台编译承诺背后的真实工程量。