本文来自 kobzol.github.io/rust/rustc/…
关于Rust最常被吐槽的,莫过于它那慢吞吞的反馈循环和漫长的编译时间了。我在各种场合都听到这种抱怨——Rust播客里、技术博客中、用户调查结果、大会演讲甚至线下闲聊。作为一个Rust用户,我自己也经常为此抓狂!
最近除了常规的编译速度吐槽,我还注意到一些沮丧的Rust开发者开始表达这样的情绪:"Rust项目组为什么对这个迫在眉睫的问题如此漠视?他们就不能做点什么吗?"作为Rust编译器性能工作组的成员,我认真思考了这些问题,当然也有些自己的看法。在这篇博客中,我想分享一些可能解答这些疑问的观点。
免责声明:本文所有观点仅代表我个人,不代表Rust项目组(这个由众多贡献者和维护者组成的多元化社区)的普遍立场。
我们真的在乎吗?
首先请大家放心——我们(指Rust项目组)当然在乎这个宝贝编译器的性能,而且一直在努力改进。我们每周都会梳理性能改进和退化情况,每个合并的PR都要通过全面的基准测试套件。只要不涉及复杂权衡(后面会详述),我们热烈欢迎任何性能优化,并会尽快回退(或修复)导致性能退化的PR。一群聪明绝顶的家伙持续在寻找性能瓶颈和加速方案,而且目前确实有几个能让默认编译速度显著提升的重要改进正在推进中。
这些努力已经初见成效!过去几年Rust的构建性能提升了不少。虽然我们通常用这个仪表盘展示长期趋势,但我觉得它有点无聊——因为它是多个基准测试的平均值,而且测试用例都偏短,容易遇到收益递减的情况。于是我做了个小实验,用我最爱的测试对象hyperqueue来展示构建性能的演进:选取它在2021年3月的首个提交,用间隔约一年的几个Rust编译器版本进行编译测试(在一台8核AMD处理器的普通笔记本上跑Linux系统)。结果如下:
| rustc版本 | 全量构建[s] | 增量重建[s] | 加速比(全量) |
|---|---|---|---|
| 1.61.0 (2022.5) | 26.1 | ~0.39 | - |
| 1.70.0 (2023.6) | 20.2 | ~0.37 | 1.29倍 |
| 1.78.0 (2024.5) | 17.0 | ~0.30 | 1.53倍 |
| 1.87.0 (2025.5) | 14.7 | ~0.26 | 1.77倍 |
在这个测试中,编译器比三年前快了近一倍。当然这只是单个数据点,不同场景下加速幅度会有所波动,而且在x64 Linux之外的其他平台可能提升更小(毕竟我们在这个主要目标平台投入了最多优化精力)。但不可否认的是,编译器性能确实在持续改善。
但还是不够快
虽然进步可喜,但对很多Rust开发者来说,编译性能仍然是重大瓶颈——如果反馈循环能快上一个数量级,他们的生产力会大幅提升。公平地说,这取决于你问谁:某些C++开发者觉得Rust的编译时间完全能接受(毕竟他们习惯了同等或更慢的构建速度),而Python程序员可能对Rust的反馈速度嗤之以鼻。但可以毫不夸张地说,对许多用户而言,Rust当前的编译速度确实不够快,这是个问题。
在深入探讨之前,有个更根本的问题值得思考:考虑到Rust复杂的类型系统、借用检查、单态化、过程宏、构建脚本、庞大的翻译单元、机器码生成以及"从源码重建世界"的编译模型,再加上它历史上几乎每次都优先选择运行时性能而非编译速度的设计倾向——Rust真的有可能实现近乎即时的(重新)构建速度吗?
我认为要分情况讨论。如果是构建带有数百个依赖项的项目,或者进行极致优化的LTO发布构建,那确实永远不可能做到真正的即时编译。但另一方面(虽然可能有人不同意),我个人相信让Rust编译器在增量模式下(以及适度优化级别)实现近乎即时的项目重建是可能的——无论项目规模多大,只要代码改动很小,重建时间应该只与改动量成正比,而不是与代码库规模成正比。当然这可能需要一些权衡,特别是在运行时性能方面,但我认为我们能做到。事实上,现在已经能看到一些雏形了(虽然应用场景还比较有限)!
而且我们并非没有改进方向。有几种"北极星"级别的方案可以从不同角度加速编译过程:并行前端、替代性代码生成后端、默认使用更快链接器、延迟代码生成(MIR-only rlibs)、避免无效的工作区重建、更智能的增量编译(包括增量链接甚至二进制热补丁)等等。其中一些方案尚未成熟,但有些现在已经可以尝鲜使用了(通常确实有效果)。我相信如果能有魔法立刻落地所有这些改进,大多数Rust项目的增量重建速度都会有质的飞跃。
而且受益者远不止普通Rust社区——提升构建性能也能让工具链贡献者事半功倍。这能缩短他们修改编译器时的自举时间,加速CI流程,减少等待测试和性能基准结果的时间...简直是多方共赢!那到底是什么在阻碍我们更快进步?为!什!么!不!赶!紧!动!起!来!啊?!
所以为啥不加快进度?
这是个棘手的问题,答案并不简单。根据我在Rust项目的工作经验,以下从多个角度(排序不分先后)分析进展缓慢的原因。
技术原因
首先是最直白的技术原因:对Rust编译器进行实质性性能改进真的很难!这个庞大的代码库(约60万行Rust代码)和其他大型编译器一样背负着技术债务风险。虽然我们提供了贡献指南,但要真正理解整个系统仍需时日。事实上可能没一个人能完全掌握整个编译器代码库(compiler-errors大神除外😂)。
在不深入理解编译器原理的情况下,确实可以通过性能剖析和微观优化来改进——但这种方法收效有限,而且低垂的果实早已被摘完。很多组件已经被优化(甚至过度优化)到在各种基准测试和使用场景下达到"局部最优",因此某个场景的改进常常会导致其他场景退化,最终可能无法被采纳。
我们在决定是否采用某些性能优化时还要考虑各种权衡。比如我们知道几个能让x64 Linux(最主流的目标平台)跑得更快的技巧:可以用支持新指令集(如AVX256)的方式编译rustc,但代价是旧CPU将无法运行;或者换用mimalloc等内存分配器,虽然能小幅提升性能,但会增加内存占用,导致内存较小的设备更容易OOM。你可能会问为什么不发布多个优化版本的编译器让用户自选?问题是现在光是构建Linux平台的最优版本就已经消耗大量CI资源了,更别提多版本分发会导致本已庞大的工具链体积进一步膨胀。到处都是两难选择。
要实现真正的重大性能突破,我认为主要有两个方向。第一个(也是我觉得最有希望的)是改进特定编译工作流。与其全面加速编译器,不如大幅优化那些制约开发者生产力的关键工作流。比如"重新链接而非重建"方案就能显著减少在大型Cargo工作区中修改crate时的编译量。这类改进不是让编译器本身更快,而是让编译过程更智能——这正是rustc需要大力提升的方向。很多这类优化不仅涉及编译器,还需要与构建系统(主要是Cargo)协同改进。
我希望其中一些方案能在中短期内显著提升常见工作流的编译速度,且不需要对编译器实现大动干戈。但即便是这种针对性优化也不容易落地:做出一个漂亮的性能提升原型只是"简单"部分,真正困难的是确保改动能处理Rust编译过程中的各种边界情况和特殊需求,在所有目标平台正常工作,不导致重要场景性能退化,不会变成维护噩梦,保持向后兼容等等。
第二个方向你肯定猜到了——对编译器实现进行大规模改造。但这面临诸多挑战:首先需要通过"氛围审查"(类似RFC的Major Change Proposal流程)获得编译器团队认可;然后要投入大量精力实现(比如改动底层某个组件可能牵扯数百处代码和测试用例);还要找到有精力的评审者(可能涉及持续数周甚至数年的多个PR)。大规模改造还容易与其他开发中的改动冲突——如果放在主代码库外开发很容易因代码快速迭代而失败,如果做成大单体PR又可能导致无限rebase,而分阶段迁移又需要长期维护两套实现,令人精疲力尽。
举个例子:编译器特质求解器的重写(虽然不直接关乎性能)由顶尖高手操刀,但仍需数年才能完成。这就是我们面对大规模重构时的现实时间尺度,也应该能让那些质问"为什么不用面向数据设计重写整个编译器来获得百倍加速"的人调整预期。
说到面向数据设计(DoD),还要考虑代码库的可维护性。假设我们挥动魔法棒,一夜之间用DoD/SIMD/手写汇编重写了所有代码,确实可能获得巨大性能提升!但我们不仅要考虑即时性能,还要兼顾长期演进能力。如果用难以理解/调试/维护的代码换取性能,从长远看得不偿失。编译器由数百名志愿者共同维护,我们需要保持代码库的一定亲和力。
虽然上述挑战确实存在,但并非rustc独有。大型编译器代码库本就难以改动,GCC/LLVM/Clang的维护者同样面临这种困境。但除此之外,还有其他因素导致我们无法只专注性能优化。
优先级问题
虽然很多Rust用户希望编译器性能成为最高优先级,但我们还有其他重要目标需要兼顾。
Rust编译器(在我看来)非常稳定可靠,每六周准时发布新版——这绝非易事!这背后是海量工作:确保基础设施和CI正常运转、及时修复严重bug、分类处理新问题、维护向后兼容性、评审PR、快速响应安全问题、跟进LLVM等依赖项的更新...而且要确保所有功能在8个Tier1目标平台(必须通过全部测试)和91个Tier2平台(至少能编译)上正常工作!这些主要由关心Rust生态的志愿者(❤️)默默完成。Rust需要维护的工作量实在太庞大了。
这些工作自然会挤占优化编译器性能的时间。如果你觉得这些维护工作不重要,不妨想象一下:如果编译器速度提升一倍,但会随机错误编译你的代码,你真的会开心吗?
另一个事实是语言和编译器功能在不断扩充:支持Rust for Linux的新编译标志、语法改进、复杂语言特性等等。这门语言远未"定型",目前有近200个开放RFC提案,还有大量已接受但未实现/稳定的RFC。任何时候都有约700个PR和上万个issue等待处理。
而且用户似乎希望Rust保持甚至加快演进速度——大家都盼着"那个小功能X赶紧稳定"好让代码更优雅吧?追求新功能很正常,但别忘了(除了设计讨论外)这需要大量实验、重构、修复、测试和实现工作,这些都会挤占性能优化资源。更何况新功能通常会让编译器稍微变慢而不是变快。
贡献者生态
最终,编译器性能优化的进展取决于贡献者和维护者。他们大多是志愿者,兴趣各异。有些编译器贡献者对性能优化毫无兴趣——这完全没问题!记住Rust不是公司(推荐阅读Mara Bos的这篇精彩博文)。我们不会指定工作内容或分配任务。如果没有足够多的人关心编译器速度,相关改进自然难以推进。
不过我们确实有些方法可以激励性能优化工作。Rust项目现在有"目标计划",列出了重点领域并承诺提供评审支持。我正努力让编译器性能成为旗舰目标之一,希望能吸引更多贡献者。
说得直白点,进展速度某种程度上也取决于资金。有时人们抱怨某些编译优化项目进展缓慢,仿佛默认有支高薪工程师团队专职负责。但请记住Rust项目主要依靠志愿者利用业余时间贡献。许多改进(包括重要的编译器性能项目)都是由某个大学生单枪匹马推动的,他们可能没有任何报酬,而进度暂停可能只是因为要应付考试或打工糊口。
最近在RustWeek大会及相关活动中,我看到多家公司对投资Rust编译器性能优化表现出兴趣,这很棒!但要注意优化不仅需要实现,还需要评审和长期维护。理想情况下,企业应该资助编译器的长期维护,而不是只赞助解决自身痛点的优化就消失。有时候推动Rust进步的最佳方式不是亲自编码,而是深耕某个领域成为专家,通过评审他人代码来解锁更多贡献——当然这需要长期投入(时间和金钱)。
根据我的观察,编译器贡献者对性能优化的兴趣程度与网络上的热议并不匹配(这很正常!)。如果我能用魔法让更多贡献者关心性能,我会这么做吗?大概会。但这长期来看对Rust是好事吗?我不确定。目前语言和编译器的发展态势良好,优先级的判断应该交给社区集体决定。
有趣的是,如果人们不需要同时操心那么多其他事情,或许会有更多精力投入性能优化。以我自己为例:2021年刚贡献Rust时我一心扑在编译器性能上,后来发现基准测试套件需要维护就转去改进,接着又折腾起用LTO/PGO/BOLT优化编译器自身,随后开始改进CI基础设施,优化工作流等待时间,再后来负责年度调查、GSoC项目、机器人改进...这些工作虽然间接影响了编译器性能(比如为其他贡献者铺路或改善基础设施),但直到现在,在分心数年后,我才真正回归直接的性能优化工作。
换句话说,只要能获得更多维护帮助(无论哪个领域!),我们就更有可能腾出手来优化编译器性能 :)
结语
写完这篇文章后我忽然意识到,如果把"编译器性能"替换成人们希望Rust改进的任何其他事情("为啥Rust不做X"),很多道理依然成立。从这个角度看,编译器性能并不特殊,它只是我们关心的众多重要事项之一,我们一直在力所能及的范围内努力改进。
对于未来的编译器性能工作,我对All Hands活动中讨论的几个计划充满期待,比如默认使用LLD链接器(希望未来几个月能落地)。我还计划开展编译器性能调查,找出最影响用户的工作流瓶颈,并构建更好的性能剖析基础设施。
希望本文能让大家理解编译性能改进为何如此不易。