加速 Rust 编译: 等待的艺术
Zed 是一个用 Rust 写的代码编辑器,快、漂亮、现代,是那种让你觉得"Rust 真香"的项目……
以下文章来自微信公众号“嘻话 Tech”,作者俞一峻,已获授权。
你的 17 分钟,就这么没了!
让我们从 Zed聊起。
Zed 是一个用 Rust 写的代码编辑器,快、漂亮、现代,是那种让你觉得"Rust 真香"的项目。
首次使用前你必须从源码编译它:
$ time cargo build --release
...
Finished `release` profile in 16m 52s
几乎花了十七分钟差八秒。
从你启动编译、看邮件、倒咖啡,喝完咖啡,刷了会儿手机,直到开始怀疑人生…终于编译完了!却发现有个 typo,你还得重新来过。
这不是 Zed 的问题。这是 Rust 的问题。或者更准确地说,是大型 Rust 项目的问题。Zed 有超过 50 万行代码,分布在 198 个 workspace crate 中。编译器要把这些全嚼一遍。
但我们肯定能搞得更快吧?毕竟Rust 社区优化编译器都优化了这么多年了。来来来,让我们把能试的都试一遍:
第一招:并行前端
Rust nightly 有并行前端。多线程嘛,一定更快。
RUSTFLAGS="-Z threads=8" cargo +nightly build --release
结果:快了大概 5-10%。不错,但还是要等 15 分钟。并行前端帮的主要是解析和类型检查,但真正吃时间的是 LLVM 代码生成——那本来就是按 codegen unit 并行的。
第二招:换个更快的链接器
链接需要时间。让我们用 wild,一个用 Rust 写的快速链接器:
RUSTFLAGS="-C link-arg=-fuse-ld=wild" cargo +nightly build --release
结果:链接时间从 8 秒降到 3 秒。对链接来说很棒了。但链接只占总编译时间的不到 1%。我们从 1,012 秒里省了 5 秒。可见瓶颈不在链接。
第三招:编译缓存
sccache可以缓存编译结果,这样下次编译就快了:
RUSTC_WRAPPER=sccache cargo build --release
结果:第二次编译确实快得飞起。但第一次编译——全新的、干净的编译——跟没缓存一模一样。而在 CI 里,每次都是全新编译。另外,你的新同事第一次 git clone && cargo build全新编译,或者,切换到一个依赖不兼容的分支,都必须全新编译。
缓存并没有减少工作量。它只是记住了上次做过的活儿。
第四招:Cranelift 后端
不用 LLVM 怎么样?相对于LLVM,Cranelift 后端编译快得多:
RUSTFLAGS="-Z codegen-backend=cranelift" cargo +nightly build
结果:编译确实快了不少。但生成的代码没有优化。Cranelift 适合开发构建,但做 release 构建——就是给用户用的、CI 产出的那种——你需要 LLVM 的优化。实际上,我们需要的是让 --release变快。
第五招:优化 rustc 本身
Rust 项目已经用 PGO(基于性能分析的优化)来编译发布编译器了。多年的努力已经让 rustc 本身尽可能地快。你正在用的 nightly 已经享受到了所有这些优化。
然而,十七分钟的等待艺术没有大的变化。
到目前为止,我们已经尝试了并行前端、快速链接器、缓存技术、替代了后端,还尝试了优化编译器本身。
灵魂拷问
如果编译器已经做得够好了呢?如果问题不在于编译器怎么编译,而在于我们让它编译了什么呢?
想想看: 当你对 Zed 执行 cargo build --release的时候,编译器老老实实地编译每个 library crate 里的每一个公开函数。regex crate 暴露了几十个函数——你的项目可能只用了三个。serde crate 有几百个方法——你用了其中一小部分。编译器不知道这些。它也不可能知道。它在独立地编译每个 crate,而任何公开函数都可能被下游调用。
或许编译器做的工作里,有一大块其实是……不必要的呢?
或许我们能在编译开始之前就告诉编译器,"嘿,这 9,000 个函数你不用管了",
而且编译器能听明白,那就有意思了。
…未完待续…
开放原子旋武开源社区(简称“旋武社区”)是由开放原子开源基金会孵化及运营的技术社区,致力于在中国推广和发展Rust编程语言生态,推动Rust在操作系统、终端设备、安全技术、基础软件等关键领域的产业落地,构建安全、可靠、高效的软件基础设施。
更多推荐

所有评论(0)