《Rust 编译器原理》完整目录
- 前言
- 第1章 编译管线全景:从源码到机器码的完整旅程
- 第2章 所有权系统:编译期内存管理的核心机制
- 第3章 借用检查器:编译器如何证明内存安全
- 第4章 生命周期:编译器如何推断引用的有效范围
- 第5章 内存布局:编译器如何排列数据
- 第6章 单态化:泛型的编译期展开
- 第7章 Trait 静态分发:零成本抽象的编译器实现
- 第8章 Trait Object 与虚表:运行时多态的内存布局
- 第9章 async/await:状态机的编译器变换
- 第10章 Pin、Waker 与 Future:异步运行时的三大支柱
- 第11章 闭包:匿名函数的编译器实现
- 第12章 unsafe:安全抽象的逃生舱
- 第13章 FFI:与 C 世界的桥梁
- 第14章 宏系统:编译期的元编程引擎
- 第15章 MIR 优化:编译器的中间表示与优化管线
- 第16章 LLVM 代码生成:从 MIR 到机器码(当前)
- 第17章 增量编译:让重编译只做必要的事
- 第18章 设计哲学与架构决策
第16章 LLVM 代码生成:从 MIR 到机器码
"当代码到达 LLVM 时,Rust 的安全保证已经完成了它的使命------剩下的只是把正确的代码变快。"
:::tip 本章要点
- Rust 的代码生成采用双层架构 :
rustc_codegen_ssa(后端无关抽象层)+rustc_codegen_llvm(LLVM 具体实现) - MIR → LLVM IR 翻译过程中,Rust 的所有权、借用、lifetime 信息全部丢弃
- LLVM IR 中的类型极其简单:
i32、i64、ptr、float等------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_statement、codegen_rvalue、codegen_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 Value、type BasicBlock、type Function 等关联类型,对于 LLVM 后端它们对应 LLVM 的 Value*、BasicBlock*、Function*;对于 Cranelift 后端则对应 Cranelift 自己的 IR 类型。
16.1.3 CodegenCx 与 FunctionCx------两层上下文
代码生成过程中有两个关键的上下文对象:
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_rvalue 或 codegen_rvalue_operand;StorageLive/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, ..) |
trunc、zext、sext 或 bitcast |
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 |
Scalar 和 ScalarPair 类型可以直接在寄存器中传递和操作,不需要通过内存。这是函数参数传递和局部变量优化的关键判断依据。
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 i32,add::<f64> 使用 fadd double。FunctionCx::monomorphize 方法通过 instantiate_mir_and_normalize_erasing_regions 在翻译过程中替换泛型参数。
16.5.2 FnAbi 与参数传递模式
FnAbi 决定了每个参数和返回值如何在机器级别传递。每个参数有一个 PassMode:Direct(寄存器直接传递标量)、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 优化分为多个阶段:PreLinkNoLTO、PreLinkThinLTO、PreLinkFatLTO(预链接)和 ThinLTO、FatLTO(后链接)。分阶段是因为如果后面还要做 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 构建
无优化"] 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 型号(如 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 函数(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/(调试信息)等模块。
| 维度 | 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 中有 alloca 和 store------这是为了调试器能够查看参数值。
LLVM IR(优化后):
llvm
define i32 @_ZN7example3add17h1234567890abcdefE(i32 %a, i32 %b) {
%result = add i32 %a, %b
ret i32 %result
}
优化后,alloca 和 store 被 mem2reg 消除。
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 的安全属性(noalias、nonnull)作为优化提示传递给 LLVM;从 mem2reg 到 LTO,每层优化各司其职;Cranelift 的引入、CGU 并行编译、Thin LTO 的设计,都是在编译速度和运行时性能之间寻找平衡。
下一章我们将探讨编译器如何通过增量编译来加速------只重新编译发生变化的部分,而不是每次都从头开始。