让我们从一个具体的例子说起:Zed编辑器。这是一个用 Rust 写就的现代代码编辑器,以其快速、美观而闻名,堪称展示 Rust 魅力的标杆项目。然而,在你初次上手体验这份“快”之前,有一个必经的、令人望而却步的环节:从源码编译它。
执行一个标准的发布构建命令,漫长的等待便开始了:
$ time cargo build --release
...
Finished `release` profile in 16m 52s
将近 17 分钟!这段时间,足够你回几封邮件、冲杯咖啡、悠闲地喝完,甚至刷会儿手机开始怀疑人生……好不容易编译完成,结果发现代码里有个拼写错误,一切又得从头再来。
这并非 Zed 独有的问题,而是大型 Rust 项目普遍面临的挑战。Zed 拥有超过 50 万行代码,分布在 198 个工作区 Crate 中。编译器需要逐一处理所有这些代码。
但真的只能这么慢吗?Rust 社区在编译器优化上耕耘多年,我们能否找到一些加速的窍门?抱着这个疑问,让我们把市面上常见的方法都尝试一遍。
第一招:启用并行前端
既然是多线程时代,让编译器前端也并行起来总没错。Rust nightly 版本提供了并行前端功能。
RUSTFLAGS="-Z threads=8" cargo +nightly build --release
结果:编译时间缩短了大约 5-10%。有提升,但依然需要等待 15 分钟左右。原因在于,并行前端主要优化的是解析和类型检查阶段,而编译过程中最耗时的部分——LLVM 的代码生成——本身就已经是按照代码生成单元(codegen unit)并行处理了。
第二招:更换更快的链接器
链接阶段也会占用时间。我们可以尝试 mold,一个用 C++ 编写的高性能链接器(注:原文提到的 wild 可能是对 mold 的误写,这里根据通用实践校正为 mold)。
RUSTFLAGS="-C link-arg=-fuse-ld=mold" cargo +nightly build --release
结果:链接时间从 8 秒减少到了 3 秒,对于链接环节来说提升显著。然而,链接在整个编译耗时中占比不到 1%。我们从总计 1012 秒的编译时间里只节省了 5 秒。这说明,瓶颈显然不在链接器上。
第三招:利用编译缓存
sccache 这类工具可以缓存编译产物,理论上能让后续的编译飞快。
RUSTC_WRAPPER=sccache cargo build --release
结果:第二次及之后的编译确实快如闪电。但问题在于,第一次、也就是全新的全量编译,耗时与不使用缓存时完全一样。在 CI/CD 流水线中,每次构建往往都是全新的环境。同样,新同事克隆项目后的首次编译,或者当你切换到一个依赖版本不同的分支时,都不得不经历一次漫长的“初体验”。缓存并没有减少实质的编译工作量,它只是记住了上次的劳动成果。
第四招:尝试 Cranelift 后端
LLVM 虽强大但笨重,那么换用更轻量的 Cranelift 后端如何?
RUSTFLAGS="-Z codegen-backend=cranelift" cargo +nightly build
结果:编译速度确实得到了可观的提升。但代价是生成的代码几乎没有经过优化。Cranelift 后端适用于快速的开发调试构建,而对于要交付给用户、需要极致性能的 --release 构建,你依然离不开 LLVM 的强大优化能力。我们的目标,正是让发布构建也变得更快。
第五招:优化编译器本身
其实,Rust 项目本身早已使用 PGO(Profile-Guided Optimization)等技术来编译发布的 rustc。多年的努力已经让编译器自身的执行效率尽可能高了。你现在使用的 nightly 版本,已经包含了所有这些优化。
然而,十七分钟的“等待艺术”并没有发生根本性的改变。
至此,我们已经尝试了并行处理、更换链接器、引入缓存、替换代码生成后端,甚至用上了最优化的编译器本身。
一个灵魂拷问
如果编译器本身已经足够优秀了呢?如果问题的关键不在于“编译器如何工作”,而在于“我们让编译器编译了什么”?
让我们仔细想想:当你对 Zed 执行 cargo build --release 时,编译器会忠实地编译每一个库 Crate 中的每一个公开函数。以 regex 库为例,它可能暴露了几十个函数,而你的项目也许只调用了其中的三个。serde 库拥有数百个方法,你用到的可能只是冰山一角。编译器对此一无所知,也不可能知道。它在独立编译每个 Crate,任何公开的符号都可能被下游代码所引用。
那么,有没有可能,编译器所做的很大一部分工作,实际上是……不必要的?
如果我们能在编译开始前就告诉编译器:“嘿,这 9000 个函数你不用管了”,并且编译器能听懂并照做,那事情就变得非常有趣了。
这引向了一个更深层次的优化思路:减少不必要的编译单元,而不仅仅是加快编译每个单元的速度。在云栈社区的 Rust 板块,开发者们经常探讨类似的话题,如何从项目结构和依赖管理的角度为编译过程“减负”。
……关于如何精确地裁剪编译依赖、实现更智能的增量编译,我们将在后续的探讨中继续展开。