Hacker News 中文摘要

RSS订阅

提前输出元数据,Rust 构建检查最高提速 2 倍 -- Emitting metadata early makes building/checking Rust up to twice as fast

Headstart 让下游 crate 在依赖项完成类型检查之前就开始编译。通常每个 crate 都要等它依赖的 crate 完全检查完,包括函数体,但类型检查自身并不需要那些函数体,只需依赖项接口中的元数据,即 .rmeta 文件里的内容。Headstart 让 rustc 在接口检查完成后立刻写出早期元数据文件,cargo 据此启动依赖方,各 crate 的函数体则在下游已开始编译时再检查。

正文

在依赖项完成类型检查之前,就开始编译依赖它的 crate。

目前每个 crate 都要等它所依赖的 crate 完全检查完(包括函数体)才会开始。但它做自身类型检查并不需要那些函数体,只需对照依赖的接口,也就是 .rmeta 文件里的元数据。

Headstart 让 rustc 在接口一检查完就写出早期元数据文件,并让 cargo 据此启动下游依赖。这样各 crate 的函数体在其下游已经开始编译时才检查。

  • cargo check:下游依赖基于早期元数据一直运行到结束。
  • cargo build:下游依赖基于早期元数据完成所有分析,然后在生成代码前等待依赖的完整元数据;等待期间把任务槽让给其他工作。

如果函数体有错误,构建仍会带着同样的诊断信息和退出状态失败;只有进度行和跨 crate 的 JSON 消息顺序可能不同。Cargo 只在其所有依赖都干净完成后才报告某个 crate 的输出,若有依赖失败则丢弃。代价是下游可能有作废的工作、错误报告稍晚、以及同时占用更多内存(见 docs/design.md 的风险部分)。

组成部分

  • rustc 的 -Zearly-metadata(6 个补丁):

    • 新增 analysis_interfaces 查询,将分析拆分为条目接口和函数体;
    • 驱动在两者之间进行时写出 .early-rmeta;
    • crate 加载接受早期元数据,并在代码生成前换入完整元数据,必要时在生产者持有到写完的锁上等待。
  • cargo 的 -Zheadstart(3 个补丁):

    • 给每次编译传递 -Zearly-metadata;
    • 在 check 和 build 中,收到早期元数据通知就启动下游依赖;
    • 将暂停的编译任务槽让给其他工作;
    • 仅当依赖成功时才报告某个 crate 的输出。

这些补丁是一系列提交,每个都有提交说明和测试,意在成为上游的拉取请求(见 patches/README.md)。

在 rustc 默认前端上,headstart 让 13 个真实项目(rust-analyzer、zed、bevy、lemmy、polars 等)的干净构建提速:cargo check 最高快 54%,cargo build 最高快 42%,没有变慢的。并行前端(-Zthreads=8)本身已覆盖部分收益,叠加后最高再快 25%。这些是 16 核的数据。加速来自原本会闲置的核,因此在小机器上收益减少。4 核时,rust-analyzer 的 check 快 24%、build 快 13–15%,codex-rs 的 check 快 14%,宽构建则持平。

codex-rs 干净 cargo build,headstart 关闭与开启对比

用 cratebank 记录的 16 核上 codex-rs 的干净 cargo build:不开 headstart 时,工作区自己的 crate 逐个编译、机器大多闲置;开启后,每个 crate 在前一个的早期元数据上启动,构建快了 37%。

codex-rs 等待关系,headstart 关闭与开启对比

同样两次构建,横轴时间、纵轴依赖深度:每个单元画在释放它的依赖下方,按编译阶段着色。不开 headstart 时工作区 crate 像长楼梯,开启后则相互重叠。详见 docs/results.md。

工作原理、早期元数据省略了什么及风险见 docs/design.md;测量数据见 docs/results.md;是否准备好提交给编译器和 cargo 团队见 docs/readiness.md。

试用

运行 scripts/setup.sh 可以检出 rustc + cargo、应用补丁并构建两者。之后在任何 Rust 项目中,设置 RUSTC=/path/to/headstart/rustc/build/host/stage1/bin/rustc 并用 headstart 路径下的 cargo 执行 check -Zheadstart(或 build)。设置 CARGO_UNSTABLE_HEADSTART=true 也能开启,在 .cargo/config.toml 里写 [unstable] headstart = true 同理。不开启时,打了补丁的 cargo 行为和上游一致,因此同一套二进制可作为公平基线。

tests/smoke 是一个双 crate 工作区,用来展示效果:它的 slow 库检查要几秒,几乎全花在函数体上;开启 headstart 后,app 大约在 0.2 秒时就启动,而不用等 slow 结束。

scripts/check-errors.sh 检验关于 tests/errors 中错误的说法:它对干净构建、依赖出错、二进制出错三种场景分别跑 cargo check 和 cargo build 的开/关对比,然后比较可读输出、JSON 输出、退出状态和构建出的二进制打印内容。

scripts/check-incremental.sh [check|build] 在一系列增量编辑上做同样的事,步骤包括给下游调用的依赖加 impl Fn、破坏再修复接口,并最终状态与干净构建对比。

scripts/check-swap.sh 让库在每个优化级别下都基于依赖的早期元数据进行、暂停时换入完整元数据,最终构建出的程序打印内容必须与基于完整元数据的完全一致。

scripts/sweep.sh 用 -Zearly-metadata-verify 开关 headstart 构建全部 53 个 rustc-perf 编译基准;两种模式都成功且诊断一致、verify 无报告才算通过。-c build 扫 cargo build,-r 为 release 配置,-t 为并行前端(-Zthreads=8)。

基准测试

scripts/bench.sh -n 5 [-c build] path/to/project ... 用来对干净构建计时,交替开关 headstart 并打印中位数。项目可在 :: 后接 cargo 参数(如 path/to/vaultwarden::--features=sqlite)。scripts/real-projects.sh 按测量的提交克隆 docs/results.md 中的 13 个真实项目并以上述格式输出,例如 scripts/bench.sh -n 3 -c build $(scripts/real-projects.sh ~/hs-real)。

scripts/setup-codex.sh <dir> 对 codex-rs 做同样准备(它需要打补丁的依赖和预构建的 V8,见脚本),计时前先 source <dir>/codex/headstart.env。

-w 为每个项目增加一次不计时的预热构建,供在 target 外做一次性工作的构建脚本使用(如 helix 把语法编译进源码树)。

scripts/bench-suite.sh <out-dir> 在当前机器跑 docs/results.md 的完整套件:rust-analyzer、21 个 rustc-perf 基准、其他真实项目和 codex-rs 的 check 与 build。它分开保存各基准结果并跳过已完成的,可重启,结束时写汇总表。这是获取未测机器规模(如 8 核)数据的办法。

scripts/bench-mem.sh 在构建期间(于项目目录内)采样所有 rustc 进程的总内存(在空闲机器上跑)。scripts/bench-incremental.sh 对改一个函数体后的增量重检计时。scripts/log-rustc 记录每次 rustc 运行的起止时间以查看调度。rustc-perf 基准位于 rustc/src/tools/rustc-perf/collector/compile-benchmarks(用 git -C rustc submodule update --init --depth 1 src/tools/rustc-perf 获取)。

评论

整体气氛相当正面:多数人认为这是一次幅度可观、甚至早该发生的优化,少数人则提醒落地障碍、潜在缺点和仓库维护方式的问题。分歧不在效果本身,而在它能否进入主线、Rust 的 AI 政策会怎样影响实现,以及把改动以补丁形式存进仓库是否合理。

认为这是显而易见却迟迟没人做的优化

  • 多人表示改进幅度很大,并惊讶此前无人实现;也有人以为类似机制早已存在,只是处在更晚阶段(较多人)。 > 这些改进幅度很大!(@rouanvde)

期待方案能进入主线编译器

  • 希望看到合入主线编译器的路径,认为演示本身已经足够说明方向可行(少数人)。 > 很期待看看有没有路径把它带进主线编译器。(@swiftcoder)

担心 AI 政策迫使方案被手工重写

  • 有人指出,由于 Rust 的 AI 政策,最终可能得靠人手工重新实现,但这反而证明这个想法确实值得做(少数人)。 > 考虑到 Rust 的 AI 政策,大概得有人手工重新实现一遍,不过这仍然很棒,因为它证明了这个想法确实很好。(@IshKebab)

提醒已有讨论列出潜在缺点

  • 有人指出两天前关于加速 Rust 编译器的提交下已有相关讨论,其中包含潜在弊端,值得一并参考(少数人)。 > 几天前那篇关于九月加速 Rust 编译器的文章里,有个很有意思的讨论串也谈到了这个技术,包括潜在缺点。(@WalterGR)

批评把仓库变成一堆签入的补丁

  • 认为 Git 本身就是版本控制,没必要在版本控制里再做一层版本控制(少数人)。 > 把仓库搞成一堆签入的补丁实在很傻,Git 已经是版本控制了,你不需要在版本控制里再做版本控制。(@quotemstr)

有价值的补充

  • 有人把该机制类比为缓存,并追问它是否类似 TypeScript 生态的 Turborepo:同一件事只做一次,然后复用结果。 > 这有点像缓存?让我想到 TypeScript 的 Turborepo,是类似的概念吗?(@hmokiguess)
  • 另有设想:让 rustc 不必自己实例化泛型方法,而是输出它期望找到的符号,例如 std::Vec<String>::push,再由独立构建进程收集这些请求,并确保同一个实例在整个构建中只被构建一次,以减少跨 crate 重复;不过按 crate 区分的构建配置等因素会让问题复杂化。