Skip to content

第15章 MIR 优化:编译器的中间表示与优化管线

"编译器的艺术在于将程序员的意图精确地转化为机器指令,同时消除一切不必要的开销。MIR 就是 Rust 编译器完成这一使命的核心战场。"

本章要点

  • MIR(Mid-level Intermediate Representation) 是 Rust 编译器在 HIR 与 LLVM IR 之间的中间表示,是借用检查、优化、代码生成三大阶段共享的核心数据结构
  • MIR 由 BasicBlock、Statement、Terminator、Place、Operand、Rvalue 六大核心类型构成
  • MIR 经历 Built → Analysis → Runtime 三个方言阶段,每个阶段有不同的语义约束
  • 优化管线包含 30+ 个优化 pass,从 SimplifyCfg 到 Inline、GVN、CopyProp、DestinationPropagation
  • 借用检查运行在 Analysis MIR 上,Drop 展开将条件性 Drop 转化为确定性 Drop
  • 通过 --emit=mir-Zunpretty=mir 可以查看各阶段的 MIR 输出

15.1 为什么需要 MIR:三层 IR 架构的设计哲学

要理解 MIR 的存在意义,我们需要回到一个根本性的问题:为什么 Rust 编译器需要三层中间表示?

15.1.1 从源码到机器码的距离

Rust 源代码与最终的机器指令之间存在巨大的语义鸿沟。源代码中充满了复杂的控制流结构(matchif letfor 循环)、所有权转移、生命周期约束和泛型参数。而机器指令只理解寄存器、内存地址和跳转。如果试图一步完成这个转换,编译器会变得极其复杂且难以维护。

Rust 编译器采用的三层 IR 架构正是为了分而治之:

HIR(High-level IR) 保留了源码的大部分结构信息,包括完整的类型注解、trait 约束、模式匹配的层次结构。它适合进行类型检查和 trait 求解,但对于数据流分析来说过于复杂。

LLVM IR 是一种接近机器码的低级表示,擅长寄存器分配、指令选择等底层优化,但它不理解 Rust 的所有权语义、生命周期和 Drop 机制。

MIR 恰好填补了这个空白。它足够低级,可以表示为控制流图(CFG)以便进行数据流分析;又足够高级,保留了 Rust 特有的语义信息,如借用、移动和 Drop。

15.1.2 MIR 承载的三大使命

MIR 不仅仅是一个"中转站",它同时承载了三个关键使命:

  1. 借用检查:MIR 的控制流图结构使得编译器能够精确地分析每个值的生命周期和借用关系。正如我们在前面章节中讨论的 NLL(Non-Lexical Lifetimes),正是在 MIR 上进行的数据流分析赋予了它超越词法作用域的精度。

  2. 领域级优化:Rust 编译器可以利用自身对所有权、类型系统和语义的理解,在 MIR 层面执行 LLVM 无法完成的优化。例如,编译器知道某个泛型函数被单态化后的具体实现,可以进行精确的内联决策。

  3. 代码生成的统一入口:无论最终的代码生成后端是 LLVM、Cranelift 还是 GCC,它们都从同一份优化后的 MIR 开始工作。

15.1.3 MIR 的历史背景

MIR 并非 Rust 一开始就有的设计。在早期版本中,Rust 编译器直接从 HIR 生成 LLVM IR。这导致了两个严重的问题:第一,借用检查只能基于词法作用域进行,产生了大量令人困惑的错误信息;第二,许多 Rust 特有的优化机会被浪费了,因为 LLVM 不理解 Rust 的语义。

2016 年引入的 MIR 彻底改变了这一局面。它使得 NLL 成为可能,使得编译器能够进行精确的 Drop 分析,并为后来的一系列优化打下了基础。

15.2 MIR 的核心数据结构

MIR 的设计理念是将函数体表示为一个控制流图(Control Flow Graph, CFG),其中节点是基本块(BasicBlock),边是控制流转移。让我们深入分析每个核心数据结构。

15.2.1 Body:函数的完整表示

一个函数在 MIR 中的完整表示是 Body 结构体,定义在 compiler/rustc_middle/src/mir/mod.rs 中:

rust
pub struct Body<'tcx> {
    /// 基本块列表,通过 BasicBlock 索引访问
    pub basic_blocks: BasicBlocks<'tcx>,

    /// 当前所处的 MIR 阶段
    pub phase: MirPhase,

    /// 局部变量声明列表
    /// 第一个是返回值位置,接着是参数,然后是用户变量和临时变量
    pub local_decls: IndexVec<Local, LocalDecl<'tcx>>,

    /// 函数参数的数量
    pub arg_count: usize,

    /// 调试信息
    pub var_debug_info: Vec<VarDebugInfo<'tcx>>,

    /// 如果是协程,包含额外信息
    pub coroutine: Option<Box<CoroutineInfo<'tcx>>>,

    // ... 其他字段
}

这个结构体中,几个关键的设计决策值得关注:

局部变量的统一编址:返回值、参数、用户变量和编译器生成的临时变量全部统一存储在 local_decls 中。_0 固定为返回值位置,_1_N(N = arg_count)是函数参数,之后的编号是用户定义的变量和编译器生成的临时变量。

阶段标记phase 字段标记了当前 MIR 处于哪个编译阶段。这不仅是元数据——不同阶段的 MIR 具有不同的语义约束。

15.2.2 MirPhase:三方言架构

MIR 的最精妙的设计之一是其"方言"(dialect)系统。MirPhase 枚举定义了三个方言,每个方言代表 MIR 在编译过程中的一个阶段:

rust
pub enum MirPhase {
    /// MIR 构建阶段,直接从 HIR/THIR 降低而来
    Built,

    /// 分析阶段 MIR,用于借用检查
    Analysis(AnalysisPhase),

    /// 运行时阶段 MIR,用于优化和代码生成
    Runtime(RuntimePhase),
}

pub enum AnalysisPhase {
    Initial = 0,
    /// 此阶段后,FalseUnwind、FalseEdge、FakeRead 等被消除
    PostCleanup = 1,
}

pub enum RuntimePhase {
    /// Drop 展开后的初始状态
    Initial = 0,
    /// Box 解引用展开后
    PostCleanup = 1,
    /// 完成所有优化后
    Optimized = 2,
}

这三个方言之间存在关键的语义差异:

Built MIR 直接从源码降低而来,包含了许多辅助构造,如 FalseEdge(仅用于借用检查的虚假控制流边)和 FakeRead(不执行实际读取但影响 NLL 分析的伪读操作)。

Analysis MIR 是借用检查的工作对象。在这个阶段,Drop 终结器表示条件性 drop——只有当数据流分析确定被 drop 的位置已初始化时,drop 才会执行。

Runtime MIR 是 CTFE(编译时函数求值)、优化和代码生成的工作对象。在这个阶段,Drop 是无条件的——到达 Drop 终结器时,如果类型有 drop glue,就一定会执行。

15.2.3 BasicBlock 与 BasicBlockData

控制流图的每个节点是一个基本块。基本块是一个线性的语句序列,以一个终结器(terminator)结束:

rust
/// 基本块的数据
pub struct BasicBlockData<'tcx> {
    /// 本块中的语句列表
    pub statements: Vec<Statement<'tcx>>,

    /// 本块的终结器——决定控制流如何离开这个块
    pub terminator: Option<Terminator<'tcx>>,

    /// 是否为 cleanup 块(用于 unwind 路径)
    pub is_cleanup: bool,
}

基本块的核心不变量是:控制流只能从基本块的第一条语句进入,只能从终结器离开。这个不变量使得数据流分析可以高效地遍历控制流图。

15.2.4 Statement:基本块内的操作

语句(Statement)是基本块内不改变控制流的操作。每个语句包含源代码信息和具体的操作类型:

rust
pub struct Statement<'tcx> {
    pub source_info: SourceInfo,
    pub kind: StatementKind<'tcx>,
}

pub enum StatementKind<'tcx> {
    /// 赋值:_1 = rvalue
    Assign(Box<(Place<'tcx>, Rvalue<'tcx>)>),

    /// 伪读:仅影响借用检查,运行时为 nop
    FakeRead(Box<(FakeReadCause, Place<'tcx>)>),

    /// 设置枚举的判别式
    SetDiscriminant { place: Box<Place<'tcx>>, variant_index: VariantIdx },

    /// 标记局部变量的存储生命周期
    StorageLive(Local),
    StorageDead(Local),

    /// Stacked Borrows 模型中的 retag 操作
    Retag(RetagKind, Box<Place<'tcx>>),

    /// 覆盖率插桩信息
    Coverage(CoverageKind),

    /// 不会 panic 的内建函数调用
    Intrinsic(Box<NonDivergingIntrinsic<'tcx>>),

    /// 编译时求值计数器
    ConstEvalCounter,

    /// 空操作,用于原地删除语句而不影响索引
    Nop,

    // ...
}

其中,Assign 是最常见也是最重要的语句类型。值得注意的是 Nop 的设计——当优化 pass 需要删除一条语句时,不会真的从 Vec 中移除它(那会破坏所有基于索引的 Location 引用),而是将其替换为 Nop。这个设计体现了 MIR 对增量修改的友好性。

15.2.5 Terminator:控制流的分叉点

终结器决定了基本块结束后控制流的去向。它是 MIR 控制流图中的关键组件:

rust
pub enum TerminatorKind<'tcx> {
    /// 无条件跳转
    Goto { target: BasicBlock },

    /// 基于整数值的多路分支(对应 match/if)
    SwitchInt {
        discr: Operand<'tcx>,
        targets: SwitchTargets,
    },

    /// 函数返回
    Return,

    /// 不可达代码(执行到此处是 UB)
    Unreachable,

    /// 函数调用
    Call {
        func: Operand<'tcx>,
        args: Box<[Spanned<Operand<'tcx>>]>,
        destination: Place<'tcx>,
        target: Option<BasicBlock>,   // 正常返回后跳转到哪里
        unwind: UnwindAction,          // panic 时的处理
        fn_span: Span,
        // ...
    },

    /// Drop 操作
    Drop {
        place: Place<'tcx>,
        target: BasicBlock,
        unwind: UnwindAction,
        replace: bool,
        // ...
    },

    /// 断言(边界检查、溢出检查等)
    Assert {
        cond: Operand<'tcx>,
        expected: bool,
        msg: Box<AssertMessage<'tcx>>,
        target: BasicBlock,
        unwind: UnwindAction,
    },

    /// 协程的 yield 点
    Yield { value: Operand<'tcx>, resume: BasicBlock, /* ... */ },

    /// Unwinding 继续
    UnwindResume,

    // ...
}

注意 Call 终结器的设计:函数调用可能永不返回(target: None),也可能 panic(unwind 字段处理栈展开)。这种将函数调用建模为终结器而非语句的决策,使得编译器能够精确地追踪控制流在 panic 场景下的行为。

15.2.6 Place、Operand 与 Rvalue:值的三要素

MIR 中对值的处理围绕三个核心概念展开:

Place(位置) 表示"内存中的一个位置",大致对应 Rust 中的左值(lvalue):

rust
pub struct Place<'tcx> {
    pub local: Local,                              // 基础局部变量
    pub projection: &'tcx List<PlaceElem<'tcx>>,   // 投影链
}

pub enum ProjectionElem<V, T> {
    Deref,                    // 解引用:*x
    Field(FieldIdx, T),       // 字段访问:x.f
    Index(V),                 // 索引:x[i]
    ConstantIndex { /* */ },  // 编译时常量索引
    Subslice { /* */ },       // 子切片
    Downcast(Option<Symbol>, VariantIdx),  // 枚举变体向下转型
    // ...
}

一个 Place 由一个局部变量加上零或多个投影组成。例如,_1.0.field 表示对局部变量 _1 先取第 0 个元素(元组索引),再取 field 字段。

Operand(操作数) 表示一个"值"——可以是从位置复制或移动得来的,也可以是一个常量:

rust
pub enum Operand<'tcx> {
    /// 从位置复制值(要求类型实现 Copy)
    Copy(Place<'tcx>),

    /// 从位置移动值(之后该位置变为未初始化)
    Move(Place<'tcx>),

    /// 编译时常量
    Constant(Box<ConstOperand<'tcx>>),
}

CopyMove 的区分是 Rust 所有权语义在 MIR 层面的直接体现。在 Drop 展开之前,只有 Copy 类型可以使用 Copy 操作数。

Rvalue(右值) 表示一个可以计算出值的表达式:

rust
pub enum Rvalue<'tcx> {
    /// 直接使用操作数
    Use(Operand<'tcx>),

    /// 创建数组:[x; N]
    Repeat(Operand<'tcx>, ty::Const<'tcx>),

    /// 创建引用
    Ref(Region<'tcx>, BorrowKind, Place<'tcx>),

    /// 二元运算
    BinaryOp(BinOp, Box<(Operand<'tcx>, Operand<'tcx>)>),

    /// 一元运算
    UnaryOp(UnOp, Operand<'tcx>),

    /// 读取判别式
    Discriminant(Place<'tcx>),

    /// 构造聚合类型(元组、结构体、枚举等)
    Aggregate(Box<AggregateKind<'tcx>>, IndexVec<FieldIdx, Operand<'tcx>>),

    /// 类型转换
    Cast(CastKind, Operand<'tcx>, Ty<'tcx>),

    // ...
}

这三者的关系构成了 MIR 中数据处理的基本模型:

15.2.7 一个完整的 MIR 示例

让我们通过一个具体的 Rust 函数来看 MIR 的完整结构:

rust
fn example(x: i32) -> i32 {
    let y = x + 1;
    if y > 10 {
        y * 2
    } else {
        y
    }
}

编译后的 MIR(简化版)大致如下:

fn example(_1: i32) -> i32 {
    let mut _0: i32;          // 返回值
    let _2: i32;              // y
    let mut _3: bool;         // 比较结果
    let mut _4: i32;          // 临时变量

    bb0: {
        _2 = Add(_1, const 1_i32);        // let y = x + 1
        _3 = Gt(_2, const 10_i32);        // y > 10
        switchInt(move _3) -> [0: bb2, otherwise: bb1];
    }

    bb1: {
        _0 = Mul(_2, const 2_i32);        // y * 2
        goto -> bb3;
    }

    bb2: {
        _0 = _2;                           // y
        goto -> bb3;
    }

    bb3: {
        return;
    }
}

这个例子展示了 MIR 如何将 Rust 的 if-else 表达式分解为基本块和 switchInt 终结器。注意 _0 是返回值位置,两个分支都向 _0 赋值后跳转到 bb3 返回。

15.3 MIR 的完整编译管线

理解了 MIR 的数据结构后,让我们来看它是如何在编译器中流转和变换的。Rust 编译器的 MIR 管线定义在 compiler/rustc_mir_transform/src/lib.rs 中,是一个精心设计的多阶段过程。

15.3.1 从 HIR 到 MIR:构建阶段

MIR 的旅程从 mir_built 查询开始。编译器从 THIR(Typed HIR)降低到最初的 MIR,然后立即运行一批初始的 lint 和简化:

rust
fn mir_built(tcx: TyCtxt<'_>, def: LocalDefId) -> &Steal<Body<'_>> {
    let mut body = tcx.build_mir_inner_impl(def);

    pm::run_passes(
        tcx,
        &mut body,
        &[
            // MIR 级别的 lint 检查
            &Lint(check_inline::CheckForceInline),
            &Lint(check_call_recursion::CheckCallRecursion),
            &Lint(check_packed_ref::CheckPackedRef),
            &Lint(check_const_item_mutation::CheckConstItemMutation),
            &Lint(function_item_references::FunctionItemReferences),
            // 首次控制流简化
            &simplify::SimplifyCfg::Initial,
            &Lint(sanity_check::SanityCheck),
        ],
        None,
        pm::Optimizations::Allowed,
    );
    tcx.alloc_steal_mir(body)
}

这里有一个重要的细节:Steal 类型。MIR 在不同的编译阶段之间通过"窃取"机制传递,确保每个阶段的 MIR 只被消费一次,避免了昂贵的深拷贝。

15.3.2 常量提升与分析准备

接下来,mir_promoted 查询执行常量提升(Promote Temps),将适合在编译时求值的表达式提取到独立的 MIR body 中:

rust
fn mir_promoted(tcx: TyCtxt<'_>, def: LocalDefId)
    -> (&Steal<Body<'_>>, &Steal<IndexVec<Promoted, Body<'_>>>)
{
    let promote_pass = promote_consts::PromoteTemps::default();
    pm::run_passes(
        tcx,
        &mut body,
        &[
            &promote_pass,
            &simplify::SimplifyCfg::PromoteConsts,
            &coverage::InstrumentCoverage,
        ],
        Some(MirPhase::Analysis(AnalysisPhase::Initial)),
        pm::Optimizations::Allowed,
    );
    // ...
}

15.3.3 借用检查与分析清理

mir_drops_elaborated_and_const_checked 是一个关键的查询节点。它首先触发借用检查,然后运行一系列降低和清理 pass:

rust
fn mir_drops_elaborated_and_const_checked(tcx: TyCtxt<'_>, def: LocalDefId)
    -> &Steal<Body<'_>>
{
    // 先运行借用检查
    let tainted_by_errors = if !tcx.is_synthetic_mir(def) {
        tcx.mir_borrowck(tcx.typeck_root_def_id_local(def)).err()
    } else {
        None
    };

    // ...
    run_analysis_to_runtime_passes(tcx, &mut body);
    // ...
}

run_analysis_to_runtime_passes 包含三个子阶段:

分析清理(Analysis Cleanup)

rust
fn run_analysis_cleanup_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
    let passes: &[&dyn MirPass<'tcx>] = &[
        &impossible_predicates::ImpossiblePredicates,  // 移除不可能的谓词
        &cleanup_post_borrowck::CleanupPostBorrowck,   // 清除借用检查用的辅助构造
        &remove_noop_landing_pads::RemoveNoopLandingPads,
        &simplify::SimplifyCfg::PostAnalysis,
        &deref_separator::Derefer,                     // 分离解引用操作
    ];
    // 完成后进入 Analysis(PostCleanup) 阶段
}

运行时降低(Runtime Lowering)——这是 MIR 从 Analysis 方言转换为 Runtime 方言的关键步骤:

rust
fn run_runtime_lowering_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
    let passes: &[&dyn MirPass<'tcx>] = &[
        &add_call_guards::CriticalCallEdges,
        &post_analysis_normalize::PostAnalysisNormalize,
        &add_subtyping_projections::Subtyper,
        &elaborate_drops::ElaborateDrops,        // Drop 展开(关键!)
        &Lint(check_call_recursion::CheckDropRecursion),
        &abort_unwinding_calls::AbortUnwindingCalls,
        &add_moves_for_packed_drops::AddMovesForPackedDrops,
        &add_retag::AddRetag,
        &elaborate_box_derefs::ElaborateBoxDerefs,
        &coroutine::StateTransform,              // 协程状态机变换
        &Lint(known_panics_lint::KnownPanicsLint),
    ];
    // 完成后进入 Runtime(Initial) 阶段
}

运行时清理(Runtime Cleanup)

rust
fn run_runtime_cleanup_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
    let passes: &[&dyn MirPass<'tcx>] = &[
        &lower_intrinsics::LowerIntrinsics,
        &remove_place_mention::RemovePlaceMention,
        &simplify::SimplifyCfg::PreOptimizations,
    ];
    // 完成后进入 Runtime(PostCleanup) 阶段
}

15.3.4 优化阶段:核心管线

最后,run_optimization_passes 执行所有的 MIR 优化。这是整个管线中最丰富、也是本章重点讨论的部分:

rust
pub(crate) fn run_optimization_passes<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
    pm::run_passes(
        tcx,
        body,
        &[
            // === 第一阶段:UB 检查插入 ===
            &check_alignment::CheckAlignment,
            &check_null::CheckNull,
            &check_enums::CheckEnums,

            // === 第二阶段:内联前的准备 ===
            &lower_slice_len::LowerSliceLenCalls,
            &instsimplify::InstSimplify::BeforeInline,

            // === 第三阶段:内联 ===
            &inline::ForceInline,           // 强制内联(#[rustc_force_inline])
            &inline::Inline,                // 普通内联

            // === 第四阶段:内联后清理 ===
            &remove_storage_markers::RemoveStorageMarkers,
            &remove_zsts::RemoveZsts,
            &remove_unneeded_drops::RemoveUnneededDrops,
            &unreachable_enum_branching::UnreachableEnumBranching,
            &unreachable_prop::UnreachablePropagation,
            &o1(simplify::SimplifyCfg::AfterUnreachableEnumBranching),
            &multiple_return_terminators::MultipleReturnTerminators,
            &instsimplify::InstSimplify::AfterSimplifyCfg,

            // === 第五阶段:核心数据流优化 ===
            &o1(simplify_branches::SimplifyConstCondition::AfterInstSimplify),
            &ref_prop::ReferencePropagation,
            &sroa::ScalarReplacementOfAggregates,
            &simplify::SimplifyLocals::BeforeConstProp,
            &dead_store_elimination::DeadStoreElimination::Initial,
            &gvn::GVN,
            &simplify::SimplifyLocals::AfterGVN,

            // === 第六阶段:高级优化 ===
            &ssa_range_prop::SsaRangePropagation,
            &match_branches::MatchBranchSimplification,
            &dataflow_const_prop::DataflowConstProp,
            &single_use_consts::SingleUseConsts,
            &o1(simplify_branches::SimplifyConstCondition::AfterConstProp),
            &jump_threading::JumpThreading,
            &early_otherwise_branch::EarlyOtherwiseBranch,
            &simplify_comparison_integral::SimplifyComparisonIntegral,

            // === 第七阶段:最终清理与传播 ===
            &o1(simplify_branches::SimplifyConstCondition::Final),
            &o1(remove_noop_landing_pads::RemoveNoopLandingPads),
            &o1(simplify::SimplifyCfg::Final),
            &strip_debuginfo::StripDebugInfo,
            &copy_prop::CopyProp,
            &dead_store_elimination::DeadStoreElimination::Final,
            &dest_prop::DestinationPropagation,
            &simplify::SimplifyLocals::Final,
            &multiple_return_terminators::MultipleReturnTerminators,
            &large_enums::EnumSizeOpt { discrepancy: 128 },

            // === 第八阶段:代码生成准备 ===
            &add_call_guards::CriticalCallEdges,
            &prettify::ReorderBasicBlocks,
            &prettify::ReorderLocals,
            &dump_mir::Marker("PreCodegen"),
        ],
        Some(MirPhase::Runtime(RuntimePhase::Optimized)),
        optimizations,
    );
}

这个 pass 列表是经过精心排序的,每个 pass 的位置都有特定的理由。让我们用一个图来概览整个优化管线:

15.4 控制流简化:SimplifyCfg

SimplifyCfg 是运行最频繁的 MIR pass 之一——它在整个管线中被调用超过 8 次,每次标记不同的子阶段(Initial、PromoteConsts、PostAnalysis、PreOptimizations、Final 等)。

15.4.1 SimplifyCfg 的核心操作

compiler/rustc_mir_transform/src/simplify.rs 的注释中可以看到,SimplifyCfg 执行两个核心操作:

  1. 合并基本块:如果一个基本块 B 只有一个前驱 A,且 A 的终结器是无条件跳转到 B,那么将 B 的语句合并到 A 中。

  2. 删除不可达块:移除从入口块无法到达的死基本块。

rust
pub(super) fn simplify_cfg<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
    if CfgSimplifier::new(tcx, body).simplify() {
        body.basic_blocks.invalidate_cfg_cache();
    }
    remove_dead_blocks(body);
    body.basic_blocks.as_mut_preserves_cfg().shrink_to_fit();
}

15.4.2 为什么 SimplifyCfg 如此重要

SimplifyCfg 的重要性体现在两个方面:

第一,它是正确性的保障。源码注释中明确指出:"此 pass 必须在任何分析 pass 之前运行,因为它会移除死块,而其中一些可能是类型不合法的。" 这是因为 typeck 允许在不可达位置返回任意类型——一旦这些块被保留到后续分析阶段,就可能导致 ICE(Internal Compiler Error)。

第二,它为后续优化创造条件。内联后可能产生大量只有单条出边的中间块,不合并这些块会增加后续 pass 的工作量。同样,不可达块传播(UnreachablePropagation)可能使一些块变得不可达,SimplifyCfg 负责清理它们。

15.4.3 实际效果示例

考虑以下代码经过 match 降低后的 MIR:

bb0: {
    switchInt(_1) -> [0: bb1, 1: bb2, otherwise: bb3];
}
bb1: {
    goto -> bb4;
}
bb2: {
    goto -> bb4;
}
bb3: {
    unreachable;
}
bb4: {
    _0 = _2;
    return;
}

SimplifyCfg 会将 bb1bb2 直接跳转到 bb4(虽然合并取决于入边数),并删除不可达的 bb3。最终可能简化为:

bb0: {
    switchInt(_1) -> [0: bb1, 1: bb1, otherwise: bb2];
}
bb1: {
    _0 = _2;
    return;
}
bb2: {
    unreachable;
}

15.5 函数内联:Inline

函数内联是 MIR 优化管线中影响最大的单个 pass。它将被调用函数的 MIR 直接嵌入调用点,消除了函数调用的开销,并为后续的数据流优化创造了巨大的分析窗口。

15.5.1 内联的架构设计

Rust 编译器的 MIR 内联系统(compiler/rustc_mir_transform/src/inline.rs)采用了一个优雅的 trait 抽象,统一处理两种内联模式:

rust
pub struct Inline;       // 普通内联,基于成本模型
pub struct ForceInline;  // 强制内联,用于 #[rustc_force_inline]

trait Inliner<'tcx> {
    fn should_inline_for_callee(&self, def_id: DefId) -> bool;
    fn check_callee_mir_body(
        &self, callsite: &CallSite<'tcx>,
        callee_body: &Body<'tcx>,
        callee_attrs: &CodegenFnAttrs,
    ) -> Result<(), &'static str>;
    fn on_inline_success(&mut self, /* ... */);
    fn on_inline_failure(&self, callsite: &CallSite<'tcx>, reason: &'static str);
}

普通内联(NormalInliner)在 mir_opt_level >= 2 时启用,且要求非增量编译模式。强制内联(ForceInliner)始终启用且不可禁用。

15.5.2 成本模型:是否值得内联

内联决策的核心是成本模型,定义在 compiler/rustc_mir_transform/src/cost_checker.rs 中。编译器为 MIR 中的每种构造分配一个"惩罚"值:

rust
const INSTR_COST: usize = 5;          // 普通指令(赋值等)
const CALL_PENALTY: usize = 25;       // 函数调用
const LANDINGPAD_PENALTY: usize = 50; // landing pad(unwind 处理)
const RESUME_PENALTY: usize = 45;     // resume(继续 unwinding)
const LARGE_SWITCH_PENALTY: usize = 20; // 大型 switch
const CONST_SWITCH_BONUS: usize = 10; // 常量 switch(可以优化掉)的奖励

CostChecker 遍历被调用函数的 MIR,累计总惩罚并减去奖励,得到最终成本。它还有一个函数级别的奖励机制——如果被调用函数只包含一个函数调用(即它是一个"转发器"),则额外奖励 CALL_PENALTY 分,因为内联它不会增加整体调用次数。

15.5.3 内联阈值

计算出成本后,与阈值比较:

rust
let mut threshold = if self.caller_is_inline_forwarder || self.past_depth_limit() {
    tcx.sess.opts.unstable_opts.inline_mir_forwarder_threshold.unwrap_or(30)
} else if tcx.cross_crate_inlinable(callsite.callee.def_id()) {
    tcx.sess.opts.unstable_opts.inline_mir_hint_threshold.unwrap_or(100)
} else {
    tcx.sess.opts.unstable_opts.inline_mir_threshold.unwrap_or(50)
};

// 小函数(3 个块以内)获得 25% 的阈值提升
if callee_body.basic_blocks.len() <= 3 {
    threshold += threshold / 4;
}

三个阈值对应三种场景:

场景默认阈值说明
普通函数50未标记 #[inline] 的本地函数
跨 crate 可内联100标记了 #[inline] 或满足跨 crate 内联条件
转发器/深度限制30调用者本身是转发器,或内联深度已超限

15.5.4 内联深度控制

为防止无限递归和指数级代码膨胀,内联器维护了两个深度限制:

rust
const HISTORY_DEPTH_LIMIT: usize = 20;    // 调用栈深度限制
const TOP_DOWN_DEPTH_LIMIT: usize = 5;    // 顶层多调用点内联限制

history 记录了从当前函数一路内联进来的所有 DefId,用于检测多态递归。top_down_counter 则追踪从顶层开始内联了多少个包含多个调用的函数——这是防止代码膨胀的关键机制。

15.5.5 MIR 内联 vs LLVM 内联

为什么 Rust 要在 MIR 层面做内联,而不是完全交给 LLVM?

  1. 类型信息优势:MIR 内联发生在单态化之后,编译器知道泛型函数的具体类型参数。这意味着它可以内联 trait 方法的具体实现,而 LLVM 只能看到一个间接调用。

  2. 跨 crate 内联:Rust 可以将 MIR 序列化到 rlib 中,使得跨 crate 的内联成为可能,而不需要等到 LTO(Link Time Optimization)阶段。

  3. 优化级联效应:MIR 内联后,后续的常量传播、死存储消除等 pass 可以在更大的分析窗口上工作,产生比 LLVM 单独做更好的优化效果。

  4. 编译时间:MIR 内联可以在早期就消除小函数调用,减少传递给 LLVM 的代码量,从而加速后端编译。

15.6 全局值编号:GVN

GVN(Global Value Numbering)是 MIR 优化管线中最强大的数据流优化之一。它的目标是检测 MIR 中的冗余计算,并用已经计算好的结果来替换它们。

15.6.1 GVN 的工作原理

compiler/rustc_mir_transform/src/gvn.rs 的文档可以看到,GVN 为每个 SSA 局部变量的赋值计算一个"符号值"(Value),并将其内部化为一个 VnIndex。如果两个不同的赋值产生了相同的 VnIndex,说明它们计算的是同一个值。

其操作语义可以概括为:

_a = some_computation     // 获得 VnIndex i
// ... 一些 MIR ...
_b = same_computation     // 也获得 VnIndex i

可以被替换为:

_a = some_computation     // 获得 VnIndex i
// ... 一些 MIR ...
_b = _a                   // 直接复用已有的值

15.6.2 引用处理的精妙设计

GVN 处理引用时有一个精妙的设计:它为每个 Ref/RawPtr rvalue 分配一个不同的"来源"(provenance)索引,确保不会错误地合并不应合并的借用:

_x = &_a;
_a = 0;
_y = &_a;    // 不能替换为 _y = _x!

但对于不可变引用指向的 freeze 类型,GVN 认为所有通过该引用的解引用都产生相同的值,这使得它能优化类似 *x + *x 的模式。

15.6.3 GVN 的位置

GVN 被安排在 ReferencePropagationSROA(标量替换聚合体)之后运行。这是因为 SROA 将结构体和元组拆分为独立的标量变量,为 GVN 创造了更多发现冗余值的机会。GVN 之后紧接着的 SimplifyLocals::AfterGVN 负责清除被 GVN 使其无用的局部变量。

15.7 常量传播:从简单到数据流

Rust 编译器中的常量传播分为两个层次,对应两个不同的 pass。

15.7.1 GVN 中的常量折叠

GVN pass 除了进行值编号外,还会进行常量折叠。当它发现一个 rvalue 的所有操作数都是常量时,会直接计算出结果。例如:

rust
fn const_example() -> i32 {
    let x = 3;
    let y = 5;
    x + y
}

优化前的 MIR:

bb0: {
    _1 = const 3_i32;
    _2 = const 5_i32;
    _0 = Add(move _1, move _2);
    return;
}

GVN 发现 _1_2 都是常量,直接计算 3 + 5 = 8

bb0: {
    _0 = const 8_i32;
    return;
}

15.7.2 DataflowConstProp:基于数据流的常量传播

更强大的 DataflowConstPropcompiler/rustc_mir_transform/src/dataflow_const_prop.rs)使用数据流分析框架进行常量传播。它能处理 GVN 无法处理的跨基本块场景,特别是当控制流的不同分支汇聚后某些变量的值仍然可以确定的情况。

DataflowConstProp 在 mir_opt_level >= 3 时才启用,并且对分析的规模有限制:

rust
// 这些常量是启发式选择的,未经严格优化
const BLOCK_LIMIT: usize = 100;   // 基本块数量上限
const PLACE_LIMIT: usize = 100;   // 追踪的 Place 数量上限

mir_opt_level >= 4 时,这些限制被解除,但这可能导致编译时间大幅增加。

15.7.3 常量条件分支简化

常量传播的一个重要后续优化是 SimplifyConstCondition,它在管线中出现三次:

  • AfterInstSimplify:在指令简化之后
  • AfterConstProp:在常量传播之后
  • Final:最终清理

switchInt 的判别式被传播为常量时,这个 pass 将多路分支替换为无条件跳转:

// 优化前
_3 = const true;
switchInt(move _3) -> [0: bb2, otherwise: bb1];

// 优化后
goto -> bb1;

这会使 bb2 变为不可达,后续的 SimplifyCfg 将其移除。

15.8 死存储消除:DeadStoreElimination

死存储消除(DSE)删除那些写入值之后再也不会被读取的赋值语句。它在管线中运行两次——InitialFinal——形成了一个与 DestinationPropagation 配合的优化循环。

15.8.1 基于活跃性分析的消除

DSE 的核心依赖于 MaybeTransitiveLiveLocals 数据流分析。这个分析从每个基本块的出口向入口反向传播,计算在每个程序点哪些局部变量是"活跃的"(即后续会被读取)。

rust
fn eliminate<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) -> bool {
    let borrowed_locals = borrowed_locals(body);
    let debuginfo_locals = debuginfo_locals(body);

    let mut live = MaybeTransitiveLiveLocals::new(&borrowed_locals, &debuginfo_locals)
        .iterate_to_fixpoint(tcx, body, None)
        .into_results_cursor(body);

    // 对于每个赋值语句,检查目标是否在该点之后是活跃的
    for (statement_index, statement) in bb_data.statements.iter().enumerate().rev() {
        if let Some(destination) = /* 可被消除的语句 */ {
            live.seek_before_primary_effect(loc);
            if !live.get().contains(destination.local) {
                // 目标变量不活跃——这个赋值是死存储
                patch.push((loc, drop_debuginfo));
            }
        }
    }
}

15.8.2 Copy 到 Move 的提升

DSE 还有一个巧妙的附加优化:当函数调用的参数以 Copy 方式传递,但该参数在调用后不再被使用时,DSE 会将 Copy 提升为 Move。这为后续的代码生成提供了"就地传递"(in-place passing)的机会。

15.8.3 幂等性保证

DSE 文档中强调了一个重要的不变量:此 pass 是幂等的。运行两次 DSE 不会产生额外的变化。更进一步,在 DSE 和 DestinationPropagation 之间交替运行,DSE 仍然保持幂等。这个性质使得整个优化管线的行为可预测。

15.9 拷贝传播与目标传播

15.9.1 CopyProp:统一拷贝等价类

拷贝传播(compiler/rustc_mir_transform/src/copy_prop.rs)的目标是识别和统一那些彼此拷贝的局部变量。它处理以下模式:

_a = rvalue
_b = move _a     // 或 copy _a
_c = move _a     // 或 copy _a
_d = move _c     // 或 copy _c

CopyProp 使用 SSA(Static Single Assignment)分析来构建"拷贝等价类"——所有通过拷贝链相连的局部变量形成一个等价类,最终全部被替换为该类的代表元素:

rust
pub(super) struct CopyProp;

impl<'tcx> crate::MirPass<'tcx> for CopyProp {
    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
        let ssa = SsaLocals::new(tcx, body, typing_env);

        // 构建拷贝等价类
        for (local, &head) in ssa.copy_classes().iter_enumerated() {
            if local != head {
                // local 和 head 在同一个等价类中
                unified.insert(head);
                unified.insert(local);
            }
        }

        // 执行替换:所有等价类成员替换为代表元素
        Replacer { tcx, copy_classes: ssa.copy_classes(), unified }
            .visit_body_preserves_cfg(body);

        // 清除不再使用的变量定义
        crate::simplify::remove_unused_definitions(body);
    }
}

15.9.2 DestinationPropagation:反向传播赋值目标

目标传播(compiler/rustc_mir_transform/src/dest_prop.rs)是 MIR 优化中最精巧的 pass 之一,也是管线中最后运行的数据传播优化。它类似于 C++ 中的 NRVO(Named Return Value Optimization),但不限于返回值。

其核心思想是:对于赋值语句 dest = src,如果 destsrc生存期不重叠,则可以将所有对 src 的引用替换为 dest,然后删除这条赋值。

健全性检查包括:

  1. srcdest 必须是常量位置(不含间接引用或索引投影)
  2. 两者必须具有完全相同的类型(不是子类型,是同一类型)
  3. 两者的活跃范围必须不相交
  4. 两者都不能被借用(被取地址的局部变量被排除在外)

目标传播直接解决了 MIR 构建过程中引入的大量冗余拷贝问题。源码注释中指出,LLVM 本身不擅长消除这类冗余(参见 rust-lang/rust#32966),因此在 MIR 层面解决它对最终代码质量有显著影响。

15.10 Drop 展开:从条件到确定

Drop 展开(compiler/rustc_mir_transform/src/elaborate_drops.rs)是 MIR 从 Analysis 方言转换为 Runtime 方言的核心步骤之一。它解决了一个根本性问题:在任意控制流下,如何正确地确定哪些值需要被 drop?

15.10.1 问题的本质

在 MIR 构建阶段,编译器在每个可能发生 drop 的位置插入 Drop 终结器。但此时,这些 drop 是条件性的——一个变量可能在某些控制流路径上已被移走,在另一些路径上仍然有效。

考虑以下代码:

rust
fn example(condition: bool) {
    let s = String::from("hello");
    if condition {
        drop(s);  // 路径 A:s 被显式 drop
    }
    // 路径 B:s 可能还需要在函数结束时被 drop
    // 但如果走了路径 A,s 已经被 drop 了
}

15.10.2 Drop 标志机制

Drop 展开通过插入drop 标志(drop flags)来解决这个问题。对于每个可能被 drop 的变量,编译器引入一个布尔值的局部变量来追踪其初始化状态:

rust
pub(super) struct ElaborateDrops;

impl<'tcx> crate::MirPass<'tcx> for ElaborateDrops {
    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
        let move_data = MoveData::gather_moves(body, tcx,
            |ty| ty.needs_drop(tcx, typing_env)  // 只追踪需要 drop 的类型
        );

        let mut inits = MaybeInitializedPlaces::new(tcx, body, &env.move_data)
            .iterate_to_fixpoint(tcx, body, Some("elaborate_drops"));

        let uninits = MaybeUninitializedPlaces::new(tcx, body, &env.move_data)
            .iterate_to_fixpoint(tcx, body, Some("elaborate_drops"));

        // 使用初始化/未初始化信息来决定每个 Drop 终结器是否应该执行
    }
}

展开后的 MIR 中,原本的条件性 Drop 被替换为:检查 drop 标志 → 如果已初始化则调用 drop_in_place → 继续执行。

15.10.3 与借用检查的关系

这里有一个关键的时序关系:借用检查运行在 Drop 展开之前

这意味着借用检查器看到的是条件性的 Drop,它需要考虑所有可能的控制流路径来确定生命周期约束。而 Drop 展开后,这些条件已经被解析为确定性的控制流,适合代码生成。

正如我们在第 2-4 章中讨论的 NLL,借用检查器正是利用 MIR 的控制流图来计算每个借用的精确生命周期。Drop 的位置直接影响了这些生命周期的终点。

15.11 其他重要优化 Pass

15.11.1 ScalarReplacementOfAggregates(SROA)

SROA 将聚合类型(结构体、元组)的局部变量拆分为独立的标量变量。这使得后续的 GVN、CopyProp 等 pass 可以单独操作每个字段。

例如,一个 (i32, i32) 类型的局部变量 _1 可能被拆分为两个 i32 变量 _1_0_1_1,然后对 _1.0 的读写变为对 _1_0 的读写。

15.11.2 ReferencePropagation

引用传播消除了不必要的引用创建和解引用对。当一个引用只用于立即解引用时,这个间接层可以被消除。这个 pass 紧接在 InstSimplify 之后运行,因为 InstSimplify 可能产生这样的模式。

15.11.3 JumpThreading

跳转线程化是一种控制流优化,它在判别式已知的情况下"跳过"不必要的分支点。例如:

bb0: {
    _2 = discriminant(_1);
    switchInt(_2) -> [0: bb1, 1: bb2, otherwise: bb3];
}
bb1: {
    // ... 一些代码 ...
    switchInt(_2) -> [0: bb4, otherwise: bb5];  // _2 已知为 0
}

JumpThreading 发现进入 bb1_2 = 0,因此第二个 switch 可以直接跳转到 bb4

15.11.4 UnreachableEnumBranching

当枚举类型被实例化后,编译器可能知道某些变体不可能出现(例如空变体、不同 crate 中不存在的变体)。这个 pass 将不可能的分支标记为 unreachable,使后续的 SimplifyCfg 可以清除它们。

15.11.5 EnumSizeOpt

EnumSizeOpt 是一个有趣的空间优化。当枚举的不同变体之间大小差异超过 128 字节(discrepancy: 128)时,它会将大变体的数据放到堆上,用一个指针替代。这可以显著减少栈空间使用和 memcpy 开销。

15.12 MIR 的查看与调试

理解如何查看和调试 MIR 对于深入理解编译器行为至关重要。Rust 编译器提供了丰富的 MIR 输出工具。

15.12.1 基本的 MIR 输出

bash
# 输出优化前的 MIR
cargo +nightly rustc -- --emit=mir

# 使用 -Zunpretty=mir 输出格式化的 MIR
cargo +nightly rustc -- -Zunpretty=mir

# 输出优化后的 MIR
cargo +nightly rustc -- -Zunpretty=mir-cfg

15.12.2 控制 MIR 优化级别

bash
# 关闭所有 MIR 优化
cargo +nightly rustc -- -Zmir-opt-level=0

# 基础优化(CopyProp 等)
cargo +nightly rustc -- -Zmir-opt-level=1

# 完整优化(包括内联、GVN 等)
cargo +nightly rustc -- -Zmir-opt-level=2

# 激进优化(包括 DataflowConstProp)
cargo +nightly rustc -- -Zmir-opt-level=3

15.12.3 Dump 特定 Pass 的 MIR

Rust 编译器可以在每个 pass 前后 dump MIR 到文件:

bash
# dump 所有 pass 的 MIR(生成大量文件)
RUSTFLAGS="-Zdump-mir=all" cargo build

# 只 dump 特定 pass
RUSTFLAGS="-Zdump-mir=Inline" cargo build

# 只 dump 特定函数
RUSTFLAGS="-Zdump-mir=my_function" cargo build

dump 的文件会被写入 mir_dump/ 目录,文件名包含 pass 名称和阶段信息,便于对比优化前后的变化。

15.12.4 阅读 MIR 输出

MIR 输出中需要注意以下约定:

  • _0:返回值位置
  • _1, _2, ...:前 N 个是参数(N = arg_count),之后是局部变量
  • bb0, bb1, ...:基本块编号
  • const 42_i32:类型标注的常量
  • move _1 vs copy _1:移动语义 vs 复制语义
  • StorageLive(_x) / StorageDead(_x):变量存储生命周期标记

15.12.5 实战:观察内联效果

rust
// src/main.rs
#[inline]
fn add_one(x: i32) -> i32 {
    x + 1
}

pub fn compute(a: i32) -> i32 {
    let b = add_one(a);
    let c = add_one(b);
    c
}

-Zdump-mir=Inline 编译后,对比内联前后的 compute 函数 MIR:

内联前:

bb0: {
    _2 = add_one(move _1) -> [return: bb1, unwind: bb3];
}
bb1: {
    _0 = add_one(move _2) -> [return: bb2, unwind: bb3];
}
bb2: {
    return;
}

内联后:

bb0: {
    _2 = Add(_1, const 1_i32);
    _0 = Add(_2, const 1_i32);
    return;
}

两次函数调用被消除,变成了直接的算术运算。后续的 GVN 还可能进一步优化。

15.13 设计权衡:MIR 优化 vs LLVM 优化

一个自然的问题是:既然 LLVM 已经拥有成熟的优化管线,为什么 Rust 还要在 MIR 层面做优化?这涉及一系列设计权衡。

15.13.1 MIR 优化的优势

领域知识:MIR 保留了 Rust 的类型系统和所有权语义。编译器知道 Vec<T> 的析构函数做什么,知道 Copy 类型可以安全地复制,知道泛型参数被实例化后的具体类型。LLVM 看到的只是字节、指针和函数调用。

代码量减少:MIR 层面的优化(特别是内联和死代码消除)可以显著减少传递给 LLVM 的 IR 数量。这直接减少了 LLVM 的编译时间,对大型项目尤其重要。

独立于后端:MIR 优化对所有代码生成后端(LLVM、Cranelift、GCC)都有效。在切换后端时,这些优化的收益不会丢失。

15.13.2 LLVM 优化的优势

成熟度:LLVM 的优化管线经过了数十年的发展和数百万行代码的实战检验,包含了大量精妙的底层优化。

硬件感知:LLVM 了解目标架构的指令集、流水线特性和缓存层次,可以做出 MIR 层面无法做到的低级优化决策(如向量化、指令调度)。

优化间的协同:LLVM 的优化 pass 之间有深度的协同关系,经过精心调优。

15.13.3 两层互补的哲学

实际上,MIR 优化和 LLVM 优化并非竞争关系,而是互补关系:

  • MIR 负责高级语义优化:利用 Rust 的类型系统和所有权信息进行内联、常量传播、死存储消除等
  • LLVM 负责低级机器优化:向量化、指令选择、寄存器分配、循环优化等

这种分层设计使得每一层都可以专注于自己最擅长的事情,同时避免了在底层重复实现高级语义分析。

15.13.4 编译时间的平衡

一个持续的工程挑战是平衡优化质量和编译时间。MIR 优化管线中多处体现了这种权衡:

  • DataflowConstPropmir_opt_level >= 3 时才启用,因为其数据流分析的时间复杂度较高
  • 内联深度被限制为 20 层递归和 5 层顶层扩展
  • 多个 pass 使用 WithMinOptLevel 包装,在低优化级别时被跳过
  • EnumSizeOpt 的差异阈值设为 128 字节,避免对小差异进行不必要的堆分配

15.14 总结与展望

MIR 是 Rust 编译器架构中最具创新性的设计之一。它不仅是一个中间表示,更是连接类型系统、所有权语义和机器码生成的桥梁。

本章要点回顾

  1. MIR 的三层方言(Built → Analysis → Runtime)使得同一数据结构可以服务于不同的编译阶段,每个阶段有清晰的语义约束。

  2. 核心数据结构(Body、BasicBlock、Statement、Terminator、Place、Operand、Rvalue)形成了一个表达力强且分析友好的控制流图表示。

  3. 优化管线包含 30+ 个 pass,按照精心设计的顺序执行:先内联以扩大分析窗口,再进行核心数据流优化(GVN、SROA),最后进行传播类优化(CopyProp、DestProp)清理冗余。

  4. 内联决策基于成本模型,默认阈值 50(普通函数)/ 100(跨 crate #[inline] 函数),考虑了指令成本、调用惩罚和深度限制。

  5. Drop 展开将条件性 Drop 转化为确定性 Drop,是 Analysis MIR 到 Runtime MIR 转换的关键步骤。

  6. MIR 与 LLVM 的优化是互补的:MIR 利用 Rust 的语义信息进行高级优化,LLVM 负责底层的机器级优化。

未来发展方向

MIR 优化管线仍在积极发展中。一些值得关注的方向包括:

  • 更精确的内联策略:考虑调用频率(profile-guided)和调用图全局信息
  • 更强的别名分析:利用 Rust 的借用规则为 MIR 优化提供比 LLVM 的 TBAA 更精确的别名信息
  • 增量编译与 MIR 优化的协同:在增量编译模式下更好地利用缓存的 MIR 优化结果
  • DestinationPropagation 的增强:支持带投影的位置合并(如 _5.foo_6

下一章,我们将跨过 MIR 与 LLVM IR 的边界,看 Rust 的类型信息和优化后的 MIR 如何被翻译为 LLVM IR——这是从 Rust 语义到机器语义的最后一步转换。

基于 VitePress 构建