Rust编译器原理-第6章 单态化:泛型的编译期展开

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

第6章 单态化:泛型的编译期展开

"零成本抽象不是没有成本,而是把成本从运行时搬到了编译时。单态化就是这笔账单的具体明细。"

:::tip 本章要点

  • 单态化(Monomorphization)是 Rust 编译器将泛型代码为每个具体类型生成独立副本的核心机制
  • rustc 中的单态化围绕 Instance<'tcx>InstanceKind<'tcx> 两个核心数据结构展开
  • 收集算法分两阶段:先从 HIR 发现根节点,再通过 MIR 递归发现所有单态化实例
  • 代码膨胀是单态化的本质代价:N 种类型 x M 个泛型函数 = 最多 N x M 份独立机器码
  • -Zshare-generics 允许跨 crate 复用已有的单态化实例,减少重复编译
  • Rust 的单态化与 C++ 模板实例化同源,但与 Java 类型擦除、Haskell/Swift 字典传递截然不同 :::

6.1 什么是单态化

在 Rust 中,泛型函数并不直接编译为可执行代码。编译器在编译期找到所有调用点,确定每次调用使用的具体类型参数,然后为每个独特的类型参数组合生成一份完全独立的函数实现。这个过程就是单态化------将多态(polymorphic)的泛型代码,转化为具体类型的单态(monomorphic)代码。

rust 复制代码
// 你写的泛型函数------一份源码
fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

fn main() {
    max(1_i32, 2_i32);     // T = i32
    max(3.14_f64, 2.71);   // T = f64
    max("hello", "world"); // T = &str
}

编译器看到三种不同的类型参数,生成三份独立函数。在 LLVM IR 层面,i32 使用 icmp sgt(有符号整数比较),f64 使用 fcmp ogt(浮点数比较),&strmemcmp 字节序比较。运行时没有任何类型判断、没有虚函数调用、没有装箱拆箱------这就是零成本抽象的本质。

cargo rustc -- --emit=llvm-ir 可以直接观察产物:

llvm 复制代码
; max::<i32> - 使用整数比较指令
define internal i32 @_ZN7example3max17h8a3b...E(i32 %a, i32 %b) {
  %cmp = icmp sgt i32 %a, %b
  %result = select i1 %cmp, i32 %a, i32 %b
  ret i32 %result
}

; max::<f64> - 使用浮点比较指令
define internal double @_ZN7example3max17hf4c1...E(double %a, double %b) {
  %cmp = fcmp ogt double %a, %b
  %result = select i1 %cmp, double %a, double %b
  ret double %result
}

函数名中的 h8a3b...hf4c1... 是编译器为不同实例生成的唯一哈希值。

6.2 为什么 Rust 选择单态化

编译型语言的泛型实现有三条截然不同的技术路线。Rust 选择了代码膨胀最严重但运行时性能最优的一条。

flowchart TB subgraph 类型擦除["类型擦除(Java)"] direction TB JE1["List<String>"] --> JE2["一份字节码
List<Object>"] JE3["List<Integer>"] --> JE2 JE2 --> JE5["虚方法分发
invokevirtual"] end subgraph 字典传递["字典传递(Haskell/Swift)"] direction TB DP1["max :: Ord Int"] --> DP2["一份代码
max + OrdDict"] DP3["max :: Ord Float"] --> DP2 DP2 --> DP5["字典查找
间接调用"] end subgraph 单态化["单态化(Rust/C++)"] direction TB MO1["max::<i32>"] --> MO2["i32 专用指令
icmp sgt"] MO3["max::<f64>"] --> MO4["f64 专用指令
fcmp ogt"] end style JE2 fill:#f59e0b,color:#fff,stroke:none style JE5 fill:#ef4444,color:#fff,stroke:none style DP2 fill:#f59e0b,color:#fff,stroke:none style DP5 fill:#ef4444,color:#fff,stroke:none style MO2 fill:#10b981,color:#fff,stroke:none style MO4 fill:#10b981,color:#fff,stroke:none

Java 类型擦除 :编译器只生成一份代码,泛型参数擦除为上界(通常是 Object)。代价:基础类型必须装箱(int -> Integer),运行时类型信息丢失(List<String>List<Integer> 运行时是同一个类),方法调用通过虚分发无法内联。

Haskell/Swift 字典传递:编译器生成一份代码,为每个 trait/protocol 约束生成一个字典(witness table),运行时通过字典查找方法地址。代价:每次方法调用多一次间接跳转,无法内联泛型调用。

Rust/C++ 单态化:为每种具体类型生成独立代码副本。运行时零开销,完全支持内联,编译器可以做类型特化优化。代价:编译时间和二进制体积。

维度 单态化(Rust/C++) 类型擦除(Java) 字典传递(Haskell/Swift)
二进制体积 大(N 类型 = N 份代码) 小(一份代码) 小(一份代码 + 字典)
编译时间 中等
运行时性能 最优 较差(虚调用+装箱) 中等(字典间接调用)
内联可能性 完全可以 需要 JIT 困难
基础类型支持 直接支持 需要装箱 直接支持

Rust 为系统编程而生,运行时性能是第一优先级。单态化是唯一能做到"泛型调用和手写具体类型代码完全零差异"的方案。

6.3 rustc 核心数据结构:Instance 与 InstanceKind

单态化的核心概念由 compiler/rustc_middle/src/ty/instance.rs 中的两个数据结构表达。

Instance:单态化实例

rust 复制代码
// 来自 compiler/rustc_middle/src/ty/instance.rs
pub struct Instance<'tcx> {
    pub def: InstanceKind<'tcx>,   // 什么类型的可调用项
    pub args: GenericArgsRef<'tcx>, // 具体泛型参数
}

源码注释中有一段关键的设计说明:

Monomorphization happens on-the-fly and no monomorphized MIR is ever created. Instead, this type simply couples a potentially generic InstanceKind with some args, and codegen and const eval will do all required instantiations as they run.

rustc 不会真的创建单态化后的 MIR 副本Instance 只是一个"承诺"------告诉代码生成器"用这个泛型定义配合这组类型参数去实例化"。真正的实例化在 codegen 阶段按需进行。

InstanceKind:实例的种类

InstanceKind 不仅仅是"用户定义的函数",还包括编译器自动生成的各种 shim:

rust 复制代码
pub enum InstanceKind<'tcx> {
    Item(DefId),                              // 用户定义的 fn、闭包、协程
    Intrinsic(DefId),                         // 编译器内建函数
    VTableShim(DefId),                        // VTable 垫片:处理 unsized self
    ReifyShim(DefId, Option<ReifyReason>),     // 函数指针具化垫片
    FnPtrShim(DefId, Ty<'tcx>),               // fn 指针的 FnTrait 实现
    Virtual(DefId, usize),                    // 动态分发:vtable 调用
    ClosureOnceShim { call_once: DefId, .. },  // FnOnce::call_once 闭包垫片
    DropGlue(DefId, Option<Ty<'tcx>>),         // drop_in_place::<T>
    CloneShim(DefId, Ty<'tcx>),                // 编译器生成的 Clone 实现
    AsyncDropGlue(DefId, Ty<'tcx>),            // 异步 drop 垫片
    // ...其他变体
}

这个枚举的丰富程度揭示了一个重要事实:编译器自动生成的代码同样需要大量单态化 ------每种类型的 drop_in_place、每种闭包的 FnOnce::call_once 适配、每种 dyn Trait 的 vtable 垫片。这些隐藏的实例往往是代码膨胀的重要贡献者。

关键变体解读:

  • DropGlue(_, Some(T)) :每种需要 drop 的类型都有一份。Vec<String> 的 drop glue 要先 drop 每个 String 再释放内存,而 Vec<i32> 只需释放内存------完全不同的代码
  • Virtual(DefId, usize) :动态分发调用,不被单态化为独立函数。它是单态化的边界,标记了从静态分发过渡到动态分发的点
  • ReifyShim :将函数从"直接调用"转换为"函数指针"时的垫片。带 #[track_caller] 的函数转为指针时需要固定调用者位置信息

Instance 的关键方法

rust 复制代码
impl<'tcx> Instance<'tcx> {
    /// 查找上游 crate 中是否已有可复用的单态化实例
    pub fn upstream_monomorphization(&self, tcx: TyCtxt<'tcx>) -> Option<CrateNum> {
        if self.def_id().is_local() { return None; }
        if !tcx.sess.opts.share_generics() { return None; }
        self.args.non_erasable_generics().next()?;
        match self.def {
            InstanceKind::Item(def) => tcx
                .upstream_monomorphizations_for(def)
                .and_then(|monos| monos.get(&self.args).cloned()),
            InstanceKind::DropGlue(_, Some(_)) => tcx.upstream_drop_glue_for(self.args),
            _ => None,
        }
    }
}

这是 -Zshare-generics 功能的核心入口,我们在 6.7 节详述。

6.4 单态化收集算法

收集算法是整个单态化流程中最复杂的部分。它的任务是找出 crate 中所有需要生成机器码的实例。实现在 compiler/rustc_monomorphize/src/collector.rs 中。

flowchart TD A["阶段1: 发现根节点
collect_roots"] --> B["遍历 HIR"] B --> C1["公开非泛型函数"] B --> C2["静态变量"] B --> C3["入口函数 main"] B --> C4["#[no_mangle] 函数"] C1 & C2 & C3 & C4 --> D["根节点集合"] D --> E["阶段2: 图遍历
collect_items_rec"] E --> F["检查 MIR 中的使用"] F --> G1["函数调用"] F --> G2["函数引用(取地址)"] F --> G3["Drop glue"] F --> G4["Unsizing 转换(vtable)"] G1 & G2 & G3 & G4 --> H["新发现的实例"] H --> |"递归"|E H --> I["完整的 Mono Item 图"] style A fill:#3b82f6,color:#fff,stroke:none style D fill:#8b5cf6,color:#fff,stroke:none style E fill:#f59e0b,color:#fff,stroke:none style I fill:#10b981,color:#fff,stroke:none

阶段1:发现根节点

入口函数 collect_crate_mono_items 先收集根节点------不需要被别人调用就必须存在的实例:

rust 复制代码
pub(crate) fn collect_crate_mono_items<'tcx>(
    tcx: TyCtxt<'tcx>,
    strategy: MonoItemCollectionStrategy,
) -> (Vec<MonoItem<'tcx>>, UsageMap<'tcx>) {
    // 阶段1:收集根节点
    let roots = tcx.sess.time(
        "monomorphization_collector_root_collections",
        || collect_roots(tcx, strategy),
    );
    // 阶段2:并行图遍历
    let state = SharedState { visited: Lock::new(UnordSet::default()), /* ... */ };
    tcx.sess.time("monomorphization_collector_graph_walk", || {
        par_for_each_in(roots, |root| {
            collect_items_root(tcx, dummy_spanned(*root), &state, recursion_limit);
        });
    });
    // 排序确保确定性输出
    // ...
}

根节点包括:非泛型的公开函数和方法、静态变量、入口函数、#[no_mangle] 函数。在 Eager 策略(增量编译)下还会包括所有非泛型 ADT 的 drop glue。

阶段2:MIR 遍历发现使用关系

核心遍历器 MirUsedCollector 实现了 MirVisitor trait:

rust 复制代码
struct MirUsedCollector<'a, 'tcx> {
    tcx: TyCtxt<'tcx>,
    body: &'a mir::Body<'tcx>,
    used_items: &'a mut MonoItems<'tcx>,
    instance: Instance<'tcx>,  // 当前正在分析的实例
}

impl<'a, 'tcx> MirUsedCollector<'a, 'tcx> {
    fn monomorphize<T: TypeFoldable<TyCtxt<'tcx>>>(&self, value: T) -> T {
        // 将泛型 MIR 中的类型参数替换为当前实例的具体类型
        self.instance.instantiate_mir_and_normalize_erasing_regions(
            self.tcx, ty::TypingEnv::fully_monomorphized(),
            ty::EarlyBinder::bind(value),
        )
    }
}

遍历器捕获四种使用关系:

  1. 函数调用foo::<i32>() 调用 bar::<i32>(),后者成为新实例
  2. 函数引用let f = print_val::<i32>; 不需要调用就创建实例
  3. Drop glue :MIR 中的 drop 语句引入 drop_in_place::<T> 实例
  4. Unsizing 转换 :将具体类型转为 dyn Trait 时,需要实例化 trait 所有方法来构建 vtable------即使这些方法从未被直接调用

第四点尤其值得注意。源码中处理 unsizing 的逻辑:

rust 复制代码
// 当遇到 Unsize 转换时
if target_ty.is_trait() && !source_ty.is_trait() {
    create_mono_items_for_vtable_methods(
        self.tcx, target_ty, source_ty, span, self.used_items,
    );
}

仅仅将一个类型转换为 dyn Trait,就会导致该类型的所有 trait 方法被实例化。

递归深度限制

泛型递归实例化可能无限展开(foo::<T> -> foo::<Vec<T>> -> foo::<Vec<Vec<T>>> -> ...)。编译器通过 recursion_limit(默认 128)防止这种情况,可用 #![recursion_limit = "256"] 调整。

UsageMap:实例间的依赖关系

收集完成后构建 UsageMap------记录每个实例使用了哪些其他实例。这个图在后续的 CGU 分区中至关重要。

rust 复制代码
pub(crate) struct UsageMap<'tcx> {
    pub used_map: UnordMap<MonoItem<'tcx>, Vec<MonoItem<'tcx>>>,  // 正向依赖
    user_map: UnordMap<MonoItem<'tcx>, Vec<MonoItem<'tcx>>>,       // 反向依赖
}

6.5 代码膨胀:零成本抽象的代价

每个独特的 (泛型函数, 类型参数组合) 对都会生成一份独立机器码。当泛型函数多、类型参数多时,组合爆炸不可避免。

flowchart TB subgraph 泛型函数["泛型函数(M 个)"] F1["Vec::push"] F2["Vec::pop"] F3["HashMap::insert"] F4["serialize"] end subgraph 类型["具体类型(N 种)"] T1["i32"] T2["String"] T3["MyStruct"] end subgraph 实例["单态化实例(最多 N x M 个)"] I1["push::<i32>"] I2["push::<String>"] I3["push::<MyStruct>"] I4["pop::<i32>"] I5["pop::<String>"] I6["insert::<String,i32>"] I7["serialize::<MyStruct>"] end F1 --> I1 & I2 & I3 F2 --> I4 & I5 F3 --> I6 F4 --> I7 style I1 fill:#ef4444,color:#fff,stroke:none style I2 fill:#ef4444,color:#fff,stroke:none style I3 fill:#ef4444,color:#fff,stroke:none style I4 fill:#ef4444,color:#fff,stroke:none style I5 fill:#ef4444,color:#fff,stroke:none style I6 fill:#ef4444,color:#fff,stroke:none style I7 fill:#ef4444,color:#fff,stroke:none

cargo-bloat 量化膨胀:

bash 复制代码
cargo bloat --release -n 20
# 典型输出:
#  5.2%  serde_json::de::Deserializer<R>::parse_value (×8)
#  3.1%  <Vec<T> as serde::Deserialize>::deserialize (×15)

(x15) 表示 Vec<T> 的反序列化方法被 15 种不同的 T 实例化了 15 份。

隐藏的膨胀源

用户代码中的泛型只是膨胀的一部分。编译器自动生成的代码同样膨胀:

  • Drop glueVec<String>Vec<i32>drop_in_place 是完全不同的代码
  • Clone 实现#[derive(Clone)] 为每种类型生成独立的 clone 方法
  • 格式化代码println!("{:?}", my_vec) 展开为 <Vec<T> as Debug>::fmt 的特定实例

实际影响度量

bash 复制代码
cargo install cargo-llvm-lines
cargo llvm-lines --release | head -20
# 输出:
#   Lines  Copies  Function name
#   23000      15  <Vec<T> as serde::Deserialize>::deserialize
#   18500      12  hashbrown::raw::RawTable<T>::find

Lines x Copies 的总和越大,对编译时间和二进制大小的贡献越大。

6.6 缓解膨胀的策略

策略一:LLVM 自动优化

  • 函数合并(MergeFunctions Pass):两个实例的 LLVM IR 完全相同时,合并为一个,另一个变成跳转 thunk
  • 内联消除 :小函数内联后消失,不再作为独立函数存在------反而减少体积
  • 死代码消除:内联后不再被引用的独立副本被链接器丢弃

策略二:Shared Generics(-Zshare-generics)

当 crate A 已经为 Vec::<String>::push 生成了代码,crate B 可以直接链接到 crate A 的版本,无需重新编译。实现依赖前述的 upstream_monomorphization 方法------编译器为每个 crate 维护"已导出的单态化实例"表,下游编译时查询复用。

限制:只能共享完全相同的 (函数DefId, 泛型参数) 对;带 #[inline] 的函数会跳过共享。

策略三:Polymorphization(已暂停)

曾是 nightly 的实验性功能(-Zpolymorphize),思路是:如果函数体不依赖某个类型参数,不同值可以共享代码。例如 fn log_count<T>(items: &[T]) 只用了 items.len(),理论上 log_count::<i32>log_count::<String> 可以共享。但实现复杂度过高,截至当前版本 rustc 源码中相关代码已被移除。

策略四:程序员手动优化

核心思路:将泛型函数中不依赖类型参数的部分提取到非泛型函数中

rust 复制代码
// 不好:整个函数体都被单态化
fn process<T: Display>(items: &[T]) {
    setup_database();        // 与 T 无关,但每种 T 都生成一份
    for item in items { println!("{}", item); }
    cleanup();               // 与 T 无关
}

// 好:只有依赖 T 的部分被单态化
fn process<T: Display>(items: &[T]) {
    setup_and_prepare(items.len());  // 非泛型,只编译一份
    for item in items { println!("{}", item); }
    finish(items.len());             // 非泛型,只编译一份
}

fn setup_and_prepare(count: usize) { setup_database(); /* ... */ }
fn finish(count: usize) { cleanup(); /* ... */ }

标准库大量使用这个技巧------Vec 的内存分配逻辑提取为非泛型辅助函数(只关心字节数和对齐),hashbrown 的探测算法不随 K、V 类型变化。

6.7 对编译时间的影响

单态化是 Rust 编译慢的核心原因之一:

  1. 收集阶段:遍历所有可达实例的 MIR 并做类型替换
  2. 代码生成线性放大:N 种实例化 = N 倍 LLVM 编译和优化时间
  3. 链接时间增长:更多目标代码,更长的链接和去重时间
  4. 增量编译不稳定性:新增一个泛型调用可能导致某个 CGU 需要重新编译

partitioning.rs 的注释解释了编译器的应对策略------将每个源码模块分为两个 CGU:

There are two codegen units for every source-level module:

  • One for "stable", that is non-generic, code
  • One for more "volatile" code, i.e., monomorphized instances

这样新增泛型实例只影响"不稳定"CGU,不波及"稳定"CGU 中的非泛型代码。

InstantiationMode 控制实例的放置策略------GloballyShared(整个程序一份,减小体积)vs LocalCopy(每个 CGU 一份,允许内联)。源码中的决策逻辑:

rust 复制代码
// 兜底规则:优化构建用 LocalCopy,非优化构建用 GloballyShared
match tcx.sess.opts.optimize {
    OptLevel::No => InstantiationMode::GloballyShared { may_conflict: true },
    _ => InstantiationMode::LocalCopy,
}

这体现了编译器面临的核心张力:优化 希望多副本(更多内联机会),增量编译希望少副本(更少重编译)。

6.8 跨 crate 单态化

泛型函数有一个独特的特性:定义和实例化可能发生在完全不同的 crate 中。这引出了一系列复杂的工程问题。

当 crate A 定义了 fn process<T>(x: T),crate B 调用 process(42_i32) 时:

  1. crate A 编译时,process 的 MIR 序列化到元数据中
  2. crate B 编译时,从元数据读取 MIR,发现 process::<i32> 实例
  3. 实例化在 crate B 中完成 ------crate A 从未见过 i32 这个参数

这就是为什么泛型函数的完整 MIR 必须包含在 crate 元数据中。也解释了为什么修改底层库中的泛型函数实现会导致所有上游 crate 重新编译。

should_codegen_locally 函数是跨 crate 的核心决策点:

rust 复制代码
fn should_codegen_locally<'tcx>(tcx: TyCtxt<'tcx>, instance: Instance<'tcx>) -> bool {
    let Some(def_id) = instance.def.def_id_if_not_guaranteed_local_codegen() else {
        return true;  // shim 类型总是本地生成
    };
    if tcx.is_foreign_item(def_id) { return false; }  // FFI 项总是链接
    // 检查上游是否已有可复用的实例
    if instance.upstream_monomorphization(tcx).is_some() {
        return false;  // 上游已有,直接链接
    }
    true
}

6.9 单态化 vs 动态分发:何时该用哪个

flowchart TD START["需要多态行为"] --> Q1{"在性能热路径上?"} Q1 -->|"是"| Q2{"类型数量少于 5 种?"} Q1 -->|"否"| Q3{"需要异构集合?"} Q2 -->|"是"| A1["泛型(单态化)
fn foo<T: Trait>(x: T)"] Q2 -->|"否"| Q4{"函数体很小?"} Q4 -->|"是"| A1 Q4 -->|"否"| A2["dyn Trait
fn foo(x: &dyn Trait)"] Q3 -->|"是"| A3["必须 dyn Trait
Vec<Box<dyn Trait>>"] Q3 -->|"否"| A2 style A1 fill:#10b981,color:#fff,stroke:none style A2 fill:#3b82f6,color:#fff,stroke:none style A3 fill:#8b5cf6,color:#fff,stroke:none
场景 推荐策略 理由
调用类型少(< 5 种) 泛型 膨胀有限,性能最优
调用类型多且函数体大 dyn Trait 避免 N 份大函数膨胀
性能关键路径 泛型 允许内联和类型特化
插件系统 / 可扩展架构 dyn Trait 编译时不知道有哪些类型
异构集合 dyn Trait 泛型无法实现
库的公开 API 泛型 给调用者最大灵活性
内部实现细节 dyn Trait 减少编译时间

最佳实践是混合策略------公开 API 用泛型保持灵活性,内部实现将不依赖类型参数的部分用非泛型函数或 dyn Trait 实现。

6.10 与 C++ 模板实例化和 Swift Witness Table 的对比

Rust 的单态化和 C++ 模板在核心原理上高度相似------都为每种具体类型生成独立代码。但在工程细节上有重要区别。

Rust vs C++ 模板

类型检查时机 :C++ 模板在实例化时 才做类型检查。如果模板体中使用了 T::foo(),只有实例化时才知道 T 有没有 foo 方法------这导致了臭名昭著的模板错误信息(几十行的 nested template 类型展开)。Rust 泛型在定义时就通过 trait bound 完成检查,不合法的调用在调用处报错,错误信息清晰。

特化支持:C++ 支持模板全特化和偏特化,允许为特定类型提供完全不同的实现:

cpp 复制代码
template<typename T> void process(T x) { /* 通用版本 */ }
template<> void process<int>(int x) { /* int 专用版本 */ }

Rust 的 specialization 仍是不稳定特性(#![feature(specialization)]),且存在健全性问题。在稳定 Rust 中,不能为泛型函数的特定类型参数提供不同实现。

实例化控制 :C++ 有 extern template 显式阻止某个编译单元中的实例化,并在另一个 .cpp 中显式实例化一次。Rust 没有等价机制,-Zshare-generics 在链接层面实现类似效果,但程序员无法精确控制实例化在哪个 crate 中发生。

编译模型 :C++ 模板定义在头文件中,每个包含头文件的编译单元独立实例化。N 个 .cpp 都用 vector<int> 就编译 N 次,链接器通过 COMDAT 或弱符号去重,丢弃 N-1 份------浪费了 N-1 份编译时间。Rust 在 crate 级别做去重,一个 crate 内 Vec::<i32>::push 只在一个 CGU 中实例化(或以 LocalCopy 有意复制供内联),跨 crate 通过 -Zshare-generics 避免重复。整体效率优于 C++ 的"编译 N 次,链接时丢弃"模式。

Rust vs Swift Witness Table

Swift 的泛型实现了一个有趣的混合方案。默认使用 witness table (值见证表)实现泛型------每个 protocol conformance 生成一个表,泛型函数通过表查找方法地址,类似 Haskell 的字典传递。但 Swift 编译器的 SIL optimizer 会在可能的情况下自动将泛型调用特化为具体类型,实质上执行了"按需单态化"。

这种混合策略的特点:

  • 未优化的 Swift 代码使用字典传递,性能低于 Rust 的始终单态化
  • 优化后的 Swift 代码在常见情况下接近 Rust 性能(优化器做了单态化)
  • Swift 默认二进制更小(未特化的函数只有一份),但优化后也会膨胀
  • Swift 可以在 ABI 层面保持稳定(因为默认的调用约定不依赖单态化),这是 Rust 目前做不到的------Rust 的泛型函数没有稳定的 ABI

6.11 总结

单态化是 Rust 类型系统和性能承诺之间的桥梁。它将编译期的泛型抽象转化为运行时的零成本具体代码,但代价是编译时间和二进制体积的膨胀。

从编译器实现的角度,单态化涉及三个核心组件:

  1. 数据表示Instance<'tcx> 将函数定义(InstanceKind)与具体类型参数绑定。InstanceKind 的十几个变体涵盖了用户代码和编译器生成代码的所有情况------从普通函数到 drop glue、vtable shim、闭包适配器
  2. 收集算法:两阶段图遍历。第一阶段从 HIR 发现根节点(非泛型公开项、静态变量、入口函数),第二阶段从根节点出发遍历 MIR,递归发现所有可达的单态化实例。收集器并行运行,最终输出确定性排序的 mono item 集合
  3. 放置策略InstantiationMode 决定每个实例是 GloballyShared(整个程序一份,减小体积)还是 LocalCopy(每个 CGU 一份,允许内联)。这个决策平衡了 LLVM 优化效果和增量编译效率

在工程实践中,理解单态化的核心价值在于:

  • 诊断能力 :当项目编译慢或二进制过大时,知道用 cargo-bloatcargo-llvm-lines 定位膨胀源
  • 设计决策 :在泛型和 dyn Trait 之间做出明智选择------性能热路径用泛型,非关键路径用动态分发
  • 代码组织:将泛型函数中不依赖类型参数的逻辑提取为非泛型辅助函数,显著减少膨胀
  • 依赖选择:意识到重泛型库(serde、diesel 等)对编译时间的放大效应

单态化没有银弹------编译时间和运行时性能是系统编程语言的本质权衡。但理解了它的全部机制,你就能在每个具体场景中做出最优的权衡。

理解了单态化,我们就掌握了 Rust 泛型系统在编译器中的核心实现。下一章,我们将进入 trait 系统的另一面------动态分发,看看 vtable 如何构建,dyn Trait 的运行时表示是什么样的,以及编译器如何在零成本的静态分发和灵活的动态分发之间架起桥梁。

相关推荐
杨艺韬4 小时前
Rust编译器原理-第11章 闭包:匿名函数的编译器实现
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第15章 MIR 优化:编译器的中间表示与优化管线
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第14章 宏系统:编译期的元编程引擎
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第16章 LLVM 代码生成:从 MIR 到机器码
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第5章 内存布局:编译器如何排列数据
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第3章 借用检查器:编译器如何证明内存安全
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第9章 async/await:状态机的编译器变换
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第8章 Trait Object 与虚表:运行时多态的内存布局
rust·编译器
杨艺韬4 小时前
Rust编译器原理-第13章 FFI:与 C 世界的桥梁
rust·编译器