以下文章来自微信公众号“嘻话 Tech”,作者俞一峻,已获授权。

cargo-slicer 项目手记 · 2026年3月


开场白:一把工具,三个月

我们花了三个月,用 Rust 写了一个叫 cargo-slicer 的工具。它的核心思路并不复杂:通过 BFS(广度优先搜索)找出从程序入口点不可达的函数,然后在 LLVM 编译它们之前,把它们的 MIR(中间表示)主体替换成一条 Unreachable 指令。

结果是可观的:

  • Zed 编辑器:编译时间从 16 分 52 秒降到 11 分 59 秒(-29%)

  • rustc 本身:2 分 16 秒降到 1 分 52 秒(-17%)

工具能用。算法验证了。然后我们把它拿到 Rust 社区,准备提交 MCP(编译器特性提案)。

接下来发生的事,才是这篇文章真正想讲的。


第一刀:lcnr 的手术刀

lcnr 是 Rust 编译器团队的核心成员,负责 trait solver。他在百忙中看了我们的 is_safe_to_skip() 函数——这个函数决定哪些函数可以被"截断"。

他的第一个问题很直接:

"我不理解 '绝对不要截断嵌套函数' 这条规则。”

我们的原始逻辑是:嵌套函数(定义在另一个函数内部的函数)在父函数被内联后,其调用引用会"消失",所以 BFS 看不到它们,不安全。

听起来很有道理。但 lcnr 指出了一个根本性的问题:我们的分析是在 mir_built 阶段进行的,那是内联发生之前。在那个阶段,父函数→嵌套函数的调用边是完全可见的。这条限制是基于一个错误假设建立起来的。

删掉它。

一条规则删除,rustc 基准测试减少了 240 亿条指令(4292B → 4268B,-0.56%)。

他的第二个建议更优雅:

// 我们原来的代码(字符串匹配):
if def_path.contains("::drop") { return false; }

// lcnr 建议的版本(lang item 查询):
if let Some(drop_trait) = tcx.lang_items().drop_trait() {
    if tcx.parent(trait_item_id) == drop_trait {
        return false;
    }
}

字符串匹配不仅脆弱,还会误伤任何路径里带 "drop" 字样的函数。编译器自己有 lang item 注册表,用它。

这就是 Rust 社区的风格:不是"你的方向错了",而是"这里有一把更好的刀"。


第二刀:Amanieu 的另一条路

Amanieu 是 Rust 标准库的leader和Rust编译器的维护者之一。他看完我们的设计后,提出了一个完全不同的思路:

"为什么不用延迟编译?先给库 crate 生成只含元数据的 .rlib 占位符,让 Cargo 立刻开始流水线化编译下游 crate,然后在链接前再回头做完整的代码生成。"

这个想法在理论上很美:不需要分析调用图,不需要替换 MIR,只靠 Cargo 的流水线机制。

我们实现了它(CARGO_SLICER_DEFERRED=1),然后测了测。

结果:

模式

时间

vs 基线

基线(无切片)

2分05秒

--

单次 CGU 过滤(我们的现有方案)

3分22秒

+37%

延迟两阶段编译(Amanieu 的建议)

3分57秒

+90%

更慢了。

原因清楚了:对于只有两个 crate 的小项目,两次编译的开销远超流水线带来的收益。更要命的是,--emit=metadata 产生的 .rmeta 不包含优化后的 MIR,所以二进制 crate 编译时无法做跨 crate 的单态化——根本行不通。

但这个探索不是失败。它明确了一件事:延迟编译真正有价值的场景,是在 rustc 内部实现,而不是作为 RUSTC_WRAPPER。这个结论后来成为我们提交 MCP 的重要动机之一。


第三刀:workingjubilee 的灵魂拷问

@workingjubilee 是 Rust 编译器贡献者,他的问题切中了最深层的设计缺陷:

"rustc 的驱动层在借用检查完成之后运行 after_analysis 钩子。如果你在 optimized_mir 里返回一个从未经过借用检查的存根 body,编译器的借用检查保证就不再适用于这个函数了。"

这个问题是对的。在 cargo-slicer 的外部实现里,我们确实是在替换 MIR body——用一个单 Unreachable 终结符的存根代替原始 body。存根从未被借用检查器检查过。

我们的答辩是:这些函数是 BFS 证明不可达的,它们永远不会被执行,所以借用检查结论对程序正确性无关。

这在逻辑上成立,但在编译器设计上是个坏味道。

然后 @oli-obk(Oliver Scherer,rustc 编译器团队)补了一刀,方向完全不同:

"其实,把 cargo-slicer 的功能迁移进 rustc 本身,工作量没那么大——核心算法直接用 TyCtxt 的原生 API 就能表达。"

这两个评论放在一起,就是答案:不要替换 MIR body,直接从代码生成单元(CGU)里删掉那些函数collect_and_partition_mono_items 决定哪些函数进入 LLVM 的视野。在那里做过滤,LLVM 永远看不到被删除的函数,借用检查器的问题也不复存在。


奥卡姆的剪刀

我们的外部工具 cargo-slicer 有多少代码?

整个项目:约 67,000 行

包括:

  • RUSTC_WRAPPER 路由和分发逻辑

  • 守护进程(fork-server)架构

  • 进程间通信(IPC)

  • 每个 nightly 版本的 ABI 版本管理

  • 磁盘 I/O:.slicer-cache/ 文件的读写

  • syn 解析器后端(跨 crate 预分析)

  • ctags 后端

  • 延迟编译模式

  • 各种兼容性 shim

这包括三个月来尝试过的成功与不那么成功,以及失败的各种中间代码以及几十种算法优化尝试。作为一个软件工程师,这些包含了多少血泪和汗水凝成的宝贵经验啊。

对应的,新的 in-tree 实现 dead_fn_elim.rs约 422 行

改动涉及 4 个文件:

文件

改动

行数

options.rs

新增一个 -Z 选项

1

dead_fn_elim.rs

新文件(含跨 crate BFS)

~410

lib.rs

(mir_transform)

pub mod dead_fn_elim;

1

lib.rs

(driver_impl)

两个调用点

~10

这就是奥卡姆剃刀定律在软件工程里的具体形态:如无必要,勿增实体

外部工具的 6 万多行代码,很大一部分是在对抗 RUSTC_WRAPPER 架构本身的限制——IPC 开销、ABI 版本化、跨进程缓存、磁盘 I/O。当这些限制被消除(移入编译器),核心算法本身只需要 422 行。关键是,如果不直接修改Rust 编译器,Rust编译器驱动的方案需要每天跟社区新出的Nightly编译器代码进行符号对齐,对 CI 造成压力。而用户一旦更新了Rust编译器版本,也必须刷新驱动版本。


倾听的价值

每一次社区反馈,我们都做了实质性的修改,虚怀若谷,从善如流:

  • lcnr:删掉了一条基于错误假设的规则,把字符串匹配换成了 lang item 查询。代码更少,更精确,性能更好。

  • Amanieu:实现了延迟编译,测了,结论是"外部工具做不了",但"in-tree 值得"。这个结论推动了 MCP 的形成。

  • workingjubilee + oli-obk:把 MIR body 替换改成了 CGU 级别删除。从根本上解决了借用检查保证的问题,同时把代码从 6 万行精简到 422 行。

每一次更新带来工程上的收益:奥卡姆的每一刀都让代码更小、更正确、更可维护。


Occam's Scissors,而非 Occam's Razor

奥卡姆剃刀是"不要引入不必要的复杂度",爱因斯坦的KISS原则是“保持足够的简单,如果不是更简单”。

但在工程里,我们更需要的是一把剪刀——主动去剪掉那些已经存在但不必要的复杂度

这需要两件事同时成立:

  1. 谦逊:愿意听社区说"这个假设是错的",愿意承认"这条路跑了但跑慢了"。

  2. 笃定:算法的核心是对的,BFS 从入口点找不可达函数,这件事在 67 KLOC 外部工具里正确,在 422 行 in-tree 实现里也正确。

三个月、67,000 行代码变成了422行,在我看来这不是浪费,而是一个验证算法有效性的必经之路。社区的这每一刀,都是在帮我们寻找算法的最小表达。

422 行,是答案的雏形。当然,用了这个表达以后,编译器性能优化的收益减少到了5~7%,而不是20~30%,但是框架搭好以后,需要极致性能的话,外部crates-slicer工具的更多优化手段还是可以发挥作用的。

从这422行代码的合入过程,你应该理解,Rust编译器的这百万行代码,该是怎样浓缩的存在了吧。

在Agentic AI时代,如果要对大模型生成的代码的安全、性能、可靠性质量兜底,Rust编译器的每一行代码都要经过千锤百炼。能够为它的性能优化贡献这么一点,与有荣焉!


如果你的 Rust 项目编译很慢,可以关注 cargo-slicer 的进展,或者等 -Z dead-fn-elimination 进入 nightly。

项目地址:github.com/yijunyu/cargo-slicer

 

Logo

开放原子旋武开源社区(简称“旋武社区”)是由开放原子开源基金会孵化及运营的技术社区,致力于在中国推广和发展Rust编程语言生态,推动Rust在操作系统、终端设备、安全技术、基础软件等关键领域的产业落地,构建安全、可靠、高效的软件基础设施。

更多推荐