这次我们不聊模型部署,也不聊 Web 框架,而是拆一个 Rust 编译器内部比较硬核的话题:一份关于“把 MIR 从 phi 节点改成 block arguments”的 LLVM Code Generation RFC 标题及讨论方向。先说明定位,这份材料不是给你下载后双击运行的工具,而是一个编译器 IR 层面的设计变更。看懂它,需要你至少知道 MIR 是什么、SSA 是什么、LLVM IR 里的 phi 节点大概长什么样;看不清这些概念,后面所有关于 Codegen 的讨论都会飘在空中。
这个 RFC 方向最值得关注的几个点如下。第一,它动了 rustc 中端表示 MIR 的基础数据结构,不再依赖传统 SSA 的 phi 节点,而是转向 block arguments 这种在 MLIR、Cranelift 等 IR 里已经验证过的表达方式。第二,它会影响 rustc 到 LLVM Code Generation 之间的衔接,比如 MIR 优化、DebugInfo、CFG 遍历、以及未来对其它后端的支持。第三,验证这个方向不是靠读文档,而是要本地构建一个带变更的 rustc 分支,用--emit=llvm-ir、-Z dump-mir这些命令去观察生成结果。这事门槛不低,但一旦跑通,你对编译器 IR 设计的理解会上升一个档次。
这篇文章会带你做四件事:先把这个 RFC 方向涉及的背景概念讲清楚;再对比 phi 节点和 block arguments 在表达控制流合并值时的差异;然后给出一套可以在本地验证 MIR 与 LLVM IR 输出的环境准备和实验方法;最后聊聊如果真要参与这类设计,有哪些坑和最佳实践。适合的读者是:对 Rust 编译器内部感兴趣的同学、想往 LLVM 后端方向走的编译器工程师、以及在读源码时总被 MIR dump 和 phi 节点绕晕的人。普通业务开发可以先收藏,但大概率不是你现在需要的“快速上手”类文章。
1. 核心能力速览
在开始长篇分析之前,先把这份 RFC 材料相关的关键信息整理成一张速览表。这里要特别说明:由于当前材料中并没有给出完整 RFC 正文、具体 PR 编号或合并状态,下面的表格是基于标题、关键词和编译原理常识做的推断。你需要把“待确认”项当作手动核对的起点,而不是结论。
| 项目项 | 说明 |
|---|---|
| 讨论主题 | Change MIR to use block arguments instead of phis |
| 技术领域 | Rust 编译器 rustc、MIR、SSA、LLVM Code Generation |
| 核心变更 | 将 MIR 中用于表达控制流合并值的 phi 节点,替换为 block arguments(块参数) |
| 直接相关方 | rustc MIR 构建、MIR 优化 pass、LLVM 后端、其它后端(如 Cranelift) |
| 主要动机 | 简化 SSA 处理、降低 CFG 遍历和序列化成本、改善与 LLVM IR 的衔接 |
| 当前状态 | 讨论阶段,具体进度需要看 rust-lang 相关仓库的最新动态 |
| 验证方式 | 构建带变更的 rustc 分支,对比 MIR dump、LLVM IR 输出、编译时间与运行性能 |
| 推荐平台 | Linux / macOS 优先;Windows 可通过 MSVC/GNU 工具链加独立 LLVM 验证 |
| 硬件门槛 | 需要构建 rustc,内存和磁盘充足;具体数值随仓库版本与优化级别变化 |
| 是否一键启动 | 不适用,这是源码级变更,不是应用工具 |
| 是否提供 API | 不适用,可以借用 rustc 命令行接口和 LLVM 工具链做验证 |
| 适合读者 | rustc contributor、编译器学习者、LLVM 后端开发、系统语言爱好者 |
这张表的核心结论是:这不是一个开箱即用的项目,而是一个值得跟踪和验证的编译器演进方向。下面从背景开始展开。
2. 适用场景与使用边界
这个 RFC 方向适合谁?主要是三类人。
第一类是 rustc 编译器贡献者,尤其是关注 MIR 优化、SSA 构建、控制流图表示的开发者。如果你平时就在看 rustc 源码里Rvalue、Statement、BasicBlock这些结构的定义,那这个方向对你来说是真正能写进 code review 或 comment 里的东西。第二类是 LLVM 后端开发者,因为 MIR 里怎么表达值合并,直接会影响最终生成 LLVM IR 前需要做什么转换,以及这些转换是否容易验证。第三类是编译器设计学习者和系统编程爱好者,把 phi 换成 block arguments 这个思路本身就是绝佳的 IR 设计案例,读懂它,你再看 MLIR 的^bb0(%arg0: i32)或者 Cranelift 的 block params,会有一种“原来如此”的感觉。
但这个方向也有明确的使用边界。它不适合普通业务项目直接使用。只要 RFC 没有合入某个稳定 rustc 版本,你就不应该把“依赖这个变更”写进生产项目里。另一个边界是验证成本:你现在写一个普通 Rust 项目,用的是稳定版 rustc,MIR 和 LLVM IR 的变化不会体现出来。除非你真的准备去 fork rustc 并构建一个自定义分支,否则这个方向对你的日常开发不会带来立竿见影的效果。
合规和安全边界也要说清楚。虽然这个主题不涉及图像、语音、数据隐私等高风险场景,但如果你要在公司或受控环境中构建测试编译器,建议不要直接把公司私有代码作为大规模 benchmark 输入。编译器实验通常涉及生成 IR、运行性能对比,这些动作本身没有风险,但如果你把专有代码塞进某份公开 RFC 的实验数据里,就可能在授权和保密上出问题。关于使用边界,我的建议是:当作开源技术研究来对待,用公开的测试用例或自写的小函数验证即可。
3. 背景理解:MIR 为什么需要处理 phi 节点
要理解这份 RFC 为什么存在,先要理解 rustc 的管线位置。Rust 代码经过解析、HIR 之后,会生成 MIR(Mid-level Intermediate Representation)。MIR 是借用检查、部分优化和代码生成的基础。rustc 把 MIR 再做一次下降,才能生成 LLVM IR,交给 LLVM 优化并生成机器码。所以 MIR 是连接高层语义和底层代码生成之间的一个中间档。
SSA(Static Single Assignment)是编译器 IR 里最常用的约束形式:每个变量只被赋值一次。这个约束对数据流分析特别友好,因为当你看到某个值的使用点时,它的定义点可以快速被找到。但在有控制流的图里,一个变量可能来自不同的前驱块,比如 if-else 两个分支都往同一个后续块传值,后续块里这个值如果没有 phi 节点,就说不清它到底来自哪个分支。于是传统 SSA 引入了 phi 节点:在基本块入口处根据控制流来源选择一个值。
LLVM IR 里最常见的 phi 写法是这样的:
; LLVM IR 中的 phi 节点示例 define i32 @foo(i1 %cond, i32 %a, i32 %b) { entry: br i1 %cond, label %then, label %else then: br label %merge else: br label %merge merge: %result = phi i32 [ %a, %then ], [ %b, %else ] ret i32 %result }这段代码的意思是:从then块跳进merge块时,%result等于%a;从else块跳进merge块时,%result等于%b。phi 节点本质上就是在给 SSA 形式“补漏洞”,让同一个变量名在不同控制流路径下得到正确值。
那么 MIR 里处理 phi 有什么痛点?从编译器工程角度看,至少有这几个方向会让维护者感到不舒服。
第一,phi 节点破坏了“值定义点单一”的直觉。虽然 SSA 的规则保证了每个变量名只有一个定义,但 phi 节点的语义是“看你是从哪个前驱来的”,它不是一个普通运算,而是一个与控制流绑定的选择操作。这让许多优化 pass 在处理 phi 时需要额外分类,比如把一个 “phi 相关” 的传播逻辑单独写一遍。第二,phi 节点在 CFG 遍历和数据流分析中比较复杂。数据流分析要反复计算 phi 的控制依赖,甚至要处理“无前驱”的基本块入口值,这种边角情况对正确性要求很高。第三,如果编译器还需要对 MIR 做序列化,比如增量编译缓存或调试信息,phi 节点往往要额外记录前驱来源信息,内存和解析成本都不小。第四,LLVM Code Generation 阶段本身就会生成 phi 节点;如果 rustc 的 MIR 层不使用 phi,而改用 block arguments,理论上可以更直接地把 MIR 的控制流参数信息映射到 LLVM 的 phi 或其他结构,减少一层中间转换的认知负担。
另外要说一句:block arguments 并不是一个新概念。公开资料里,MLIR 的基本块是带参数的,Cranelift 的 IR 也用 block params,Swift 的 SIL 和某些研究性 IR 同样采用了类似设计。所以在 LLVM 系之外,block arguments 已经被多个编译器项目验证过。这也是这个 RFC 方向能引起讨论的原因之一:它并不是凭空发明,而是想把一个在其它 IR 里已经证明好用的表达方式迁移到 rustc 的 MIR 里。
4. RFC 核心设计:从 phi 换成 block arguments
既然题目是“Change MIR to use block arguments instead of phis”,我们就要理解 block arguments 在语义上是怎么替换 phi 的。我用一个简化示来说明。假设我们要表达一个循环累加,传统 SSA 用 phi 的写法和 block arguments 的写法会不一样。
传统 phi 风格(示意,类似 LLVM IR):
define i32 @sum_loop(i32 %n) { entry: br label %loop loop: %i = phi i32 [ 0, %entry ], [ %i.next, %loop ] %acc = phi i32 [ 0, %entry ], [ %acc.next, %loop ] %done = icmp eq i32 %i, %n br i1 %done, label %exit, label %body body: %i.next = add i32 %i, 1 %acc.next = add i32 %acc, %i br label %loop exit: ret i32 %acc }block arguments 风格(示意,类似 MLIR / Cranelift 表示):
func.func @sum_loop(%n: i32) -> i32 { br ^loop(%i: i32 = 0, %acc: i32 = 0) ^loop(%i: i32, %acc: i32): %done = arith.cmpi eq, %i, %n : i32 cf.cond_br %done, ^exit, ^body ^body: %i.next = arith.addi %i, %c1 : i32 %acc.next = arith.addi %acc, %i : i32 br ^loop(%i.next, %acc.next) ^exit: return %acc : i32 }注意第二种写法里,跳转指令后面直接带实参,比如br ^loop(%i.next, %acc.next)。进入^loop时,%i和%acc就自动接收这些实参值。你不需要在^loop里再写一个 phi 节点,因为块的参数本身就是 “从哪个前驱来,就接收哪个前驱传入的值”。
这种表达方式的好处之一是:CFG 的参数化变得显式了。编译器看到br ^exit时,可以一眼看出它传入了哪些值;看到^exit(%result: i32)时,也可以直接知道这个块需要哪些入口参数。相比之下,phi 节点把信息散落在每个块的顶部,而且依赖前驱块的跳转来源来推导值,分析路径更长。
不过这里必须提醒一个关键点:如果最终目标还是生成 LLVM IR,那么 LLVM IR 本身不支持块的显式参数。LLVM 采用的仍然是我们熟知的 phi 节点。因此,这个 RFC 到底是在 MIR 层完全去掉 phi,然后在 LLVM Codegen 阶段再把 block arguments 翻译回 phi,还是说只在一些优化 pass 内部使用 block args、最终输出仍用 phi,需要看最终 RFC 正文的设计细节。材料里目前只有标题和 keyword,我不能替它补完具体方案。但一个合理的判断是:这个变更大概率发生在 rustc 内部的 MIR 表示层,目的是让 MIR 的构建、遍历、优化和序列化更简单,而不一定改变 LLVM IR 最终的形态。
5. 本地验证环境准备与工具链安装
如果你想真正验证这个 RFC 方向,而不是只看表层分析,那就需要自己在本地搭一套观察环境。这里分两条路线。
第一条路线是不修改 rustc,先观察目前稳定版或 nightly 版生成的 MIR 和 LLVM IR 是什么样。这能让你建立基线认知。第二条路线是 fork rustc 源码,把 RFC 对应的补丁合入本地分支,重新构建 rustc,然后再跑同样的测试程序,对比 MIR 和 LLVM IR 的差异。第二条路线成本高,但它才是验证 RFC 效果的正道。
先做基线准备。安装 Rust 工具链时,最常见的做法是使用 rustup。Windows 上也可以通过 rustup-init 安装,不需要先手动装一个完整 Rust 编译器。安装完成后,你可以用rustup toolchain install nightly安装 nightly 工具链,因为-Z dump-mir这类内部观察 flag 一般在 nightly 才能用。
# 安装 rustup 后,安装 nightly 工具链 rustup toolchain install nightly rustup default nightly rustc --version现在来看 LLVM 工具链。如果你想在 Windows 上快速获得一套可用的 LLVM 工具链,优先考虑 LLVM 官方发布的预编译压缩包。把压缩包解压后,把其中的bin目录加入PATH,就能使用llvm-dis、llc、opt、llvm-config这些命令。这种方式是免安装的,适合做实验,不需要走 MSI 安装器的流程。当然,具体压缩包版本和下载方式会随 LLVM release 变化,建议直接到 LLVM 官方 release 页面找对应平台的版本。
# Windows PowerShell 中把 LLVM 解压目录加入当前会话 PATH $env:PATH = "C:\llvm\bin;" + $env:PATH llvm-config --version llc --version如果是源码级验证,接下来要克隆 rustc 仓库并构建自定义分支。rustc 源码构建不是一个小操作,我建议先准备充足的磁盘空间和内存,另外把构建目标限制在当前平台的默认 target 上,不要一上来就交叉编译。
下面的命令是通用模板,实际路径和分支名需要按 RFC 对应的仓库和 PR 情况替换:
# 克隆 rustc 主仓库 git clone https://github.com/rust-lang/rust.git cd rust # 创建实验分支,这里的分支名只是示例 git checkout -b block-args-experiment # 构建 stage1 编译器,具体构建配置以仓库 README 为准 # 首次构建需要较长的时间,请耐心等待 ./x.py build --stage 1构建完成后,build/x86_64-unknown-linux-gnu/stage1/bin/rustc就是你的实验用编译器。Windows 下对应的路径会带上 MSVC 的 target 目录,具体以实际构建输出为准。关于 x.py 的细节,我只给到通用模型,因为 rustc 构建系统经常调整参数,最稳妥的做法是照着克隆下来的仓库里的 README 和config.toml.example来配置。
6. 功能测试与效果验证:对比 MIR 和 LLVM IR 输出
环境准备好了之后,就可以设计实验了。这个 RFC 的核心目标是改变 MIR 对控制流合并值的表示方式,所以验证逻辑也非常明确:用同一份 Rust 测试程序,在未修改的 rustc 和带 RFC 变更的 rustc 上分别生成 MIR dump 和 LLVM IR,然后对比差异。
先准备一个小测试程序。这个程序要包含分支合流和循环,因为 phi 节点最常出现在这两种位置:
// test_phi.rs fn sum_loop(n: i64) -> i64 { let mut i = 0; let mut acc = 0; while i < n { acc += i; i += 1; } acc } fn choose(flag: bool, a: i64, b: i64) -> i64 { if flag { a + 1 } else { b - 1 } } fn main() { let n = std::env::args() .nth(1) .and_then(|s| s.parse().ok()) .unwrap_or(10); println!("{} {}", sum_loop(n), choose(true, 1, 2)); }然后分别观察 MIR 和 LLVM IR。
观察 MIR dump 的一种方式是用 nightly rustc 的-Z dump-mir。这个 flag 会把 MIR 中间文件输出到本地,文件内容会随编译器版本变化,但结构一般很接近。
# 生成 MIR dump 文件,具体 flag 以 nightly 版本帮助信息为准 rustc -Z dump-mir -Z unpretty=mir test_phi.rs运行后你会看到当前 rustc 的 MIR 输出。如果这个 RFC 已经合入,那么相关块的入口处可能就不再以 phi 形式表达合并值,而是出现块参数。当前未合入的版本里,你看到的是现有 MIR 的表达方式。这就是最直接的差异证据。
观察 LLVM IR 用--emit=llvm-ir:
# 生成 LLVM IR 文件 rustc --emit=llvm-ir test_phi.rs # 在生成的 IR 中寻找 phi 节点 grep -n " phi " test_phi.ll如果当前 LLVM IR 中出现大量 phi 节点,这是正常现象,因为 LLVM IR 本身就是 SSA 加 phi 的形式。如果 RFC 合入后,rustc 在 MIR 层用 block arguments,但 LLVM 后端又需要把块参数重新转换回 phi,那么最终生成的 LLVM IR 里仍然会有 phi。这种情况下,真正的变化要回到 MIR 层去看,而不是只盯着 LLVM IR。
这一步的关键判断标准是:MIR dump 中是否出现了块参数,以及这些块参数是否能被正确映射到 LLVM 后端。如果你的实验分支里 LLVM Codegen 直接把 block arguments 翻译成 phi,且编译通过,输出 IR 语义等价,那就可以说这个方向在基础链路上成立。
接着可以对比优化行为。使用同一个函数在不同 rustc 版本下,观察开启优化前后 LLVM IR 的变化:
rustc -O --emit=llvm-ir test_phi.rs grep -n " phi " test_phi.ll对比开启-O前后 phi 节点数量,能帮你理解 MIR 层改变是否间接影响了优化后 IR 的形态。更进一步的验证可以使用 LLVM 工具链里的opt和llvm-dis对 IR 做分析和阅读,比如把 IR 转成人类可读文本:
llvm-dis test_phi.bc -o test_phi.ll不过要强调,任何实验都必须建立在相同基线之上。同一台机器、同一个 rustc 构建配置、同一份源码,才能对比出差异。不能拿 release 构建的 rustc 和 debug 构建的 rustc 对比,也不能一边开-C opt-level=3、另一边不开优化。
7. 工具链接口与批量验证
这个 RFC 不提供 HTTP API,也没有一键启动脚本。但从命令行工具链角度看,rustc 和 LLVM 工具链本身提供了足够的“接口”供自动化验证。你可以把rustc --emit=llvm-ir看作一个代码生成接口,把llvm-dis看作 IR 剖析接口,把opt看作优化接口。这些接口可以串联成一条批量验证流水线。
比如,你写一个简单的脚本,对多个 Rust 测试文件批量生成 LLVM IR,并把 phi 节点统计出来。以下是通用脚本模板,路径和参数要按你的环境调整:
#!/usr/bin/env bash # 批量生成 LLVM IR 并统计 phi 节点数量的脚本模板 set -euo pipefail RUSTC="$HOME/rust/build/x86_64-unknown-linux-gnu/stage1/bin/rustc" OUT_DIR="./ir_out" mkdir -p "$OUT_DIR" for src in tests/*.rs; do name="$(basename "$src" .rs)" $RUSTC --emit=llvm-ir -O "$src" -o "$OUT_DIR/$name.ll" count="$(grep -c " phi " "$OUT_DIR/$name.ll" || true)" echo "$name: $count phi nodes" done脚本里使用了grep -c统计phi关键字,这不严谨,但对于快速筛查足够了。你还可以配合comm或diff对比两个 rustc 分支生成的 IR 差异:
# 比较两个实验分支生成的 LLVM IR diff -u baseline.ll block_args.llRust 编译器内部还有一些调试输出接口可以用。比如RUSTC_LOG环境变量可以控制编译器内部日志,-Z dump-mir可以输出 MIR,-Z self-profile可以输出编译流水线中各个 pass 的时间开销。对这类 IR 设计验证来说,-Z self-profile特别有价值,因为你可以观察 phi 到 block arguments 的转换是否带来了可衡量的 pass 耗时差异。以下是一个通用用法:
# 生成 self-profile 数据,文件名可按需调整 rustc -Z self-profile -Z self-profile-events=default test_phi.rsself-profile 输出的数据可以用工具查看,具体工具名和命令我不在这里硬写,因为 rustc 团队经常调整推荐工具,最好以当前 nightly 工具链的文档为准。总之,命令行“接口”是充分可用的,关键是你要有明确的实验脚本和对比基线。
8. 资源占用与性能观察
编译器内部设计变更,最终要落到资源占用和性能上。但这里有一个必须遵守的原则:不能拍脑袋给数字。构建 rustc 的资源需求、实验分支的编译时间、生成 IR 的差别、运行性能差异,都必须在你的实际机器和实际仓库版本上测量。
先看构建 rustc 的资源占用。rustc 仓库本身庞大,构建过程中要下载依赖、编译各种 crate,最后还要链接。初步经验是:完整构建需要的磁盘空间可能在几十 GB 量级,内存建议充足,链接阶段内存占用会明显上升。但这只是一个定性经验,具体数值取决于你是否开启 debug 信息、是否只构建 stage1、以及你的 target 平台。建议用一个简单方法观察:构建时打开系统资源监视器,记录峰值内存和磁盘占用。
再看编译测试程序时的性能观察。这里可以分两个维度:编译时间和运行时间。编译时间可以用cargo build --timings来观察 crate 构建时序,也可以直接用-Z self-profile分析 rustc 内部 pass 耗时。如果 RFC 的目标之一是简化 MIR 处理流程,那么你应当去对比MIR_built、codegen_llvm等 pass 在基线分支和实验分支上的耗时差异。运行时间则直接用time命令或 Windows PowerShell 里的Measure-Command。
# Linux/macOS 下观察运行耗时 time ./target/release/test_phi 1000# Windows PowerShell 下观察运行耗时 Measure-Command { .\target\release\test_phi.exe 1000 }另外一个值得观察的点是 phi 节点数量与优化时间的关系。你可以在基线分支下统计一个 crate 生成的 LLVM IR 中 phi 节点数量,然后尝试在 MIR 层做等价改写(如果 RFC 补丁已经可用),再统计 phi 数量。如果 phi 数量显著下降,说明 LLVM 后端需要处理的 SSA 边界情况会变少。但这不一定是净收益,因为把 block arguments 重新翻译回 phi 本身也需要成本。所以最终判断必须以整条编译链路结果为标准。
降低资源占用的常见方法:使用 stage1 构建,不需要完整 stage2;只构建当前 host target;使用 sccache 缓存编译产物;定期清理build目录中不需要的中间产物。如果你发现构建 rustc 的频率很高,可以考虑保留一个基线分支和一个实验分支,两个分支共享同一个build缓存,缩短切换成本。不过 rustc 的构建系统很复杂,具体缓存参数需要参考当前仓库的构建文档。
9. 常见问题与排查方法
我把这个主题下常见的坑和排查思路整理成一张表。由于很多情况是版本相关的,我不给死命令,而是给排查方向。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
x.py build在下载依赖阶段报错 | 网络不稳定或依赖源不可达 | 查看构建日志,确认卡在哪一步 | 确保网络稳定,必要时配置国内镜像源;不要使用不合规网络手段 |
| 构建 rustc 时缺 C++ 工具链或 CMake | 系统环境缺少 LLVM 编译依赖 | 检查 clang/cmake 版本,查看系统日志 | 安装对应平台的基础编译工具链,确保编译器版本满足要求 |
rustc -Z dump-mir提示 unknown flag | 使用的工具链不是 nightly | 执行rustc --version,确认版本 | rustup toolchain install nightly && rustup default nightly |
llvm-config命令找不到 | LLVM 工具链未加入 PATH | 执行where llvm-config或which llvm-config | 在 PATH 中加入 LLVM 解压目录,或使用绝对路径 |
| 生成的 LLVM IR 里看不到 phi | 优化级别太高,phi 被优化掉 | 先不加-O生成 IR | 分别跑--emit=llvm-ir和-O --emit=llvm-ir对照 |
| 实验分支构建失败 | 本地补丁与当前 rustc 版本冲突 | 查看编译错误信息,定位是哪个 crate | 切换分支基线或更新补丁到当前版本 |
| 对比结果不稳定 | 两个分支优化级别或配置不一致 | 检查config.toml和命令行参数 | 使用完全相同参数和同一份测试程序 |
| 运行测试程序时进程内存飙升 | 构建配置或实验数据量过大 | 用资源监视器查看峰值内存 | 减小测试输入规模,关闭 debug 信息 |
如果你遇到的是“对比出来的结果没有差异”,这也可能是正常现象。特别是当你只在 LLVM IR 层面观察时,block arguments 可能已经被 LLVM 后端重新转换成 phi 节点。此时要把重心转向 MIR dump,实在不行就去看 rustc 的 codegen 相关源码,确认转换逻辑在哪个 pass 中发生。
10. 最佳实践与参与 RFC 讨论的建议
如果你看完材料后,不只是想“知道有这回事”,而是想真正参与验证或讨论,我有几个建议。
第一,先建立一套可重复的验证脚本。把基线 rustc、实验 rustc、测试程序、IR 输出目录、结果对比命令全部固化下来。不要每次手动敲命令,那样无法保证一致性。第二,任何结论都要同时附上实验环境和参数:机器型号、rustc commit hash、LLVM 版本、优化级别、测试输入规模。没有这些信息,你的数据对别人来说没有参考价值。第三,优先使用公开测试用例或你自己写的小函数做验证。不要拿未授权的内部代码用于开源 RFC 的实验。这也是最基本的合规边界。
第四,如果你想参与 RFC 讨论,不要只在标题层面表态。只看标题就说“赞成”或“反对”没有意义。更好的方式是:基于本地实验,指出 block arguments 在 MIR 层的引入对哪个 pass 产生了什么影响,具体哪些场景有收益,哪些场景有退化风险。RFC 讨论最有价值的内容是带着数据和复现路径的意见。第五,注意补丁提交流程。rustc 项目对代码格式、测试覆盖和 commit message 要求很高。如果你做实验时修改了代码,提交之前要跑相关测试,确保没有破坏现有 MIR 相关功能。再强调一次,这类变更合入主线前,一切以维护团队的评审意见为准,不要急着自己上线到生产项目。
最后是效率建议:构建 rustc 很耗时,不要频繁从头构建。你可以保留两个源码目录,一个作为基线,一个作为实验分支,两者共享工具链缓存和依赖缓存,只在真正需要时才切换构建。实验过程中也要区分“现象”和“结论”。你在本地看到某个函数生成的 LLVM IR 中 phi 减少,这是现象;只有当实验覆盖足够多样本,同时排除了优化级别和代码结构干扰后,才能下一句可靠结论。
11. 总结与下一步
这个 RFC 方向最值得关注的点,是它试图把 rustc 的 MIR 从传统 SSA phi 表达,迁移到 block arguments 这种在现代编译器 IR 中越来越常见的表达方式。它并不是一个独立的应用工具,而是一个会影响 MIR 构建、MIR 优化、序列化和 LLVM Code Generation 的基础设计变更。如果你对编译器 IR 设计感兴趣,这个方向是一个很好的研究样本。
你最先应该验证的内容,不是写一个大 crate,而是找一个最简单的分支合流函数,分别用基线 rustc 和实验分支 rustc 生成 MIR dump 和 LLVM IR,仔细观察 block arguments 是否出现、phi 节点是否减少、最终 LLVM IR 语义是否保持一致。这一步能让你快速理解整个设计的关键链路。
最容易被低估的坑是构建成本和对比维度。很多人会花一个下午构建 rustc,却忘了记录基线环境,最后无法解释变化来自代码变更还是环境差异。记住一个原则:每次实验前先固定“什么样的代码在什么样的编译器版本下生成什么样的输出”。
接下来可以深入的方向包括:研究 block arguments 对增量编译缓存格式的影响、检查 DebugInfo 是否需要额外变换、观察 Cranelift 后端是否因为 MIR 层的变化而更容易对接、以及评估 MLIR 风格的 CFG 表示是否适合 rustc 的优化管线。这些都是在这个 RFC 基础上值得继续啃的硬骨头。建议把这份材料和相关的 LLVM 工具链本地验证方法一起收藏,后续 rustc 有任何新进展,你可以在已有实验环境上快速跟进。