为何 Rust 不在 编译器性能 上投入更多?
本文译自 Why doesn’t Rust care more about compiler performance?,作者 Kobzol,译者 @Baram X 3,原文发表于 2025 年 6 月 9 日。
也许关于 Rust 最常被重复的抱怨就是其缓慢的反馈循环和漫长的编译时间。我经常听到这样的说法;在 Rust 播客、博客文章 (blog posts)、调查 (surveys)、会议演讲或线下讨论中。作为一名 Rust 用户,我自己也经常抱怨这一点!
最近,除了通常的编译时间抱怨之外,我也开始注意到沮丧的 Rust 开发者表达了以下观点:“为什么 Rust 项目对这个紧迫且已知的问题不够重视?为什么他们不采取一些措施呢?” 作为 Rust 编译器性能工作组 的一员,我非常重视这些问题,而且我当然对这个问题有一些看法。在这篇博客文章中,我想提供一些可能作为对这些(以及其他类似)问题的答案的想法。
免责声明:本帖中表达的所有观点仅代表我个人, 并不一定代表 Rust 项目(为 Rust 工具链做出贡献的各种贡献者和维护者)的更广泛观点。
我们真的关心吗?
首先,请允许我向您保证——是的,我们(指的是 Rust 项目)绝对关心我们心爱的编译器的性能,并且我们投入了大量的精力来改进它。我们每周都会 分类 性能改进和回退。我们在每次 合并 PR 后都会运行全面的 基准测试套件。我们热情地欢迎任何性能改进(除非它们有复杂的权衡,稍后会详细介绍),并尝试快速恢复(或修复)引入性能回退的 PR。非常聪明的人不断致力于寻找瓶颈并 加速编译器。并且一些旨在默认情况下更快编译的重大改进目前正在 进行中。
所有这些努力都显示出了成果;在过去的几年里,Rust 的构建性能有了很大的提高!当我们谈论长期趋势时,我们通常会展示 这个仪表板,但我发现它有点枯燥,因为它平均了几个基准测试的结果,而且这些基准测试都很短,所以它们很快就开始遇到收益递减的情况。相反,我做了一个小实验来展示我最喜欢的测试对象 hyperqueue 的构建性能是如何演变的。我选取了它的第一次提交(2021 年 3 月 1),并用几个不同的 Rust 编译器版本进行了编译,版本之间大约间隔 1 年 2。我是在一台配置相对一般的笔记本电脑上进行的,该笔记本电脑配备 8 个 AMD 核心,运行 Linux。以下是结果:
rustc 版本 | 完整构建 [s] | 增量重建 [s] | 加速比 (完整构建) |
---|---|---|---|
1.61.0 (2022 年 5 月) | 26.1s | ~0.39 | - |
1.70.0 (2023 年 6 月) | 20.2s | ~0.37 | 1.29x |
1.78.0 (2024 年 5 月) | 17.0s | ~0.30 | 1.53x |
1.87.0 (2025 年 5 月) | 14.7s | ~0.26 | 1.77x |
在这个基准测试中,编译器比三年前快了近一倍。当然,这只是一个单一的数据点,在不同的情况下,加速效果可能会更大或更小,而且在其他平台上(x64 Linux 除外),加速效果通常会更小,因为我们花费了最多的精力来优化该特定目标。但事实仍然是,编译器的性能正在持续提高。
但它仍然不够快
现在,所有这些都很好,但这并不能改变这样一个事实:对于许多 Rust 开发者来说,编译性能仍然是一个很大的瓶颈,如果反馈循环能够快一个数量级(或更多),他们的生产力将会大大提高。公平地说,这取决于你问谁,例如,一些 C++ 开发者根本不介意 Rust 的编译时间,因为他们已经习惯了相同(或更糟)的构建时间,而 Python 程序员可能不会对 Rust 的反馈循环速度印象深刻。但毫不夸张地说,对于许多用户来说,今天的 Rust 编译速度仍然不够快,这是一个问题。
在我们更详细地检查这个问题之前,我认为有趣的是思考这个问题是否从根本上是可解决的,以及我们最终想要达到的最终目标是什么。凭借其复杂的类型系统、借用检查、单态化、过程宏和构建脚本、大型翻译单元、机器代码生成以及“从源代码重建整个世界”的编译模型,Rust 是否有可能实现近乎即时的(重新)构建速度?当它在 几乎每一个机会 都倾向于运行时(而不是编译)性能的历史记录时?
我认为这取决于用例。如果我们谈论的是从头开始构建具有数百个依赖项的项目,或者进行超级彻底的 LTO 优化的发布版本构建,我认为我们永远不会达到构建真正即时的程度。另一方面,我个人相信(尽管其他人可能不同意!)在进行小的源代码更改后,拥有一个能够在增量(并且至少是适度优化的)模式下几乎立即重建任何大小的 Rust 项目的 Rust 编译器并非根本不可能,在这种模式下,重建真正是 O(执行的更改数量)
而不是 O(代码库大小)
。我们可能需要做出一些权衡,尤其是在最终的运行时性能方面,但我认为我们可以实现这一目标。事实上,我们甚至可以看到一些已经可以实现类似效果的例子(目前在某些有限的场景下) 今天!
而且,我们并非没有关于如何(至少更接近)实现这一目标的想法。有几种“北极星”方法可以以各种方式加速编译过程,例如 并行前端、替代 代码生成后端、默认使用 更快的链接器、延迟代码生成 (仅 MIR 的 rlib、-Zhint-mostly-unused)、 避免无用的工作区重建、更智能的增量编译(包括 增量链接 甚至二进制热补丁)等等。
其中一些方法尚未为广泛使用做好准备,但另一些已经准备就绪,您今天就可以 选择使用 它们。它们通常(但并非总是)有帮助。我相信,如果我们拥有一根魔杖,并且今天就能实现所有这些更改,那么对于很大一部分(肯定比现在大得多)Rust 项目来说,我们可以获得非常快速的增量重建。
这不仅仅是更广泛的 Rust 社区会受益于此。提高构建性能也会使 Rust 工具链贡献者本身的工作效率更高。当对编译器进行更改时,它可以减少构建编译器本身所需的时间,加快我们的 CI 工作流程,减少我们等待测试和编译器性能基准测试的时间……每个人都会受益!那么是什么阻碍我们更快地取得进展呢?为什么我们不采取一些措施呢? 3
那么为什么我们做得不够多呢?
这是一个棘手的问题,没有简单的答案。我将尝试基于我个人在 Rust 项目中的工作经验,提供各种我认为我们进展不够快的原因(顺序大致随意)。
技术原因
首先,让我们从技术原因开始,这在某种程度上是最直接的。简而言之,对 Rust 编译器的性能进行非微小的改进是很困难的!这是一个庞大的代码库 4,正如所有大型(编译器)代码库一样,它有很多技术债务 风险,尽管我们提供了 贡献指南,但仍然需要时间才能理解发生了什么。事实上,可能没有一个人完全理解整个编译器代码库(也许除了 compiler-errors :laughing:)。
虽然即使不深入理解编译器的工作原理,也可以通过分析和微优化其各个部分来改进性能,但这种方法只能让你走这么远,而且大部分唾手可得的果实已经被摘走了 5。为了在各种基准测试和用例中达到“局部性能最小值”,很多东西都经过了优化和过度拟合,因此即使对某些用例进行了改进,它通常也会导致其他方面的性能下降,这可能会导致该局部改进不被接受。
在决定是否采纳某些性能优化时,我们还必须考虑各种权衡。例如,我们知道一些可以使编译器在最流行的目标 x64 Linux 上更快一点的 技巧。我们可以使用对“更新”指令集架构(例如 AVX256)的支持来编译 rustc
,这似乎提供了一些 提升。然而,这意味着编译器将无法在仍在 官方支持 的旧 x64 CPU 上运行!或者我们可以使用不同的内存分配器(例如 mimalloc),这似乎也提供了一些适度的 性能提升,但代价是 内存使用量增加,这可能会导致 rustc
在 RAM 较小的设备上发生 OOM。也许你会想,为什么我们不能为同一个目标发布多个具有不同优化的编译器版本,然后让人们(或 rustup
)选择使用哪个版本?好吧,仅仅为 Linux 构建最优化变体的编译器现在已经消耗了大量的 CI 资源,更不用说以多种变体分发工具链可能会使我们通过网络发送给用户的数据量膨胀,而这已经非常庞大了。到处都存在权衡。
我认为要实现真正显著的构建性能提升,主要有两种方法。第一种方法,我发现非常有前途,是改进某些编译工作流程。如果我们能够极大地提高目前限制了大部分 Rust 开发者生产力的特定工作流程的性能(或减少所需的工作量),我们不一定需要使整个编译器都更快。这方面的一个例子是 重新链接,而不是重新构建 的提议,它有可能减少在大型 Cargo 工作区中修改 crate 时必须执行的编译量。像这样的方法本身并不一定使编译器更快,而是使编译过程更智能,而这正是 rustc
可以大幅改进的领域。许多这些潜在的工作流程改进也不仅仅与编译器本身严格相关,而是发生在 rustc
和所使用的构建系统(最常见的是 Cargo)的交汇处。
我希望在短期到中期内,这些想法中的一部分能够使最常见的 Rust 编译工作流程更快得多,而无需对编译器的实现进行大规模更改。然而,即使是这种有针对性的优化也不容易实现。通过概念验证实现令人印象深刻的性能提升是“简单”的部分。但随后而来的是漫长而艰巨的后续工作——确保你的更改支持 Rust 编译过程的所有边缘情况和怪癖,适用于所有目标,不会导致重要的用例和基准测试性能下降,不是维护的噩梦,保持向后兼容性等等。
正如你可能已经猜到的,第二种方法是对编译器的实现进行大规模的更改和/或重构。然而,由于一些原因,这当然具有挑战性。首先,你需要通过一个 重大变更提案 与编译器团队的其他成员进行”氛围检查” 6,以确保其他编译器维护者同意你进行如此重大的变更。然后,你显然需要实现这些变更,这可能需要大量的精力。例如,如果你更改了编译器”底层”的某个部分,那么你将需要检查数百个地方并进行修复,并且还可能需要修复许多测试用例,这可能非常耗时 7。你还需要找到有空闲时间的审阅者来批准你的更改,这可能跨越许多 PR,耗时数周、数月甚至数年。
执行大型的跨领域更改也很棘手,因为它必然会与在此期间对编译器进行的其他许多更改发生冲突。你可以尝试在主编译器树之外执行修改,但这几乎注定要失败,因为编译器变化太快了 8。或者,你可以尝试将所有更改放在一个巨大的 PR 中,这可能会导致无休止的 rebase(并且可能使寻找审阅者更加困难)。或者,你需要逐步进行迁移,这可能需要长时间维护同一事物的两个独立实现,这可能会令人疲惫。
一个大型内部更改的例子(尽管它并非直接与性能相关)是编译器 trait 解析器 的重新实现。即使它是由非常聪明且技术极其娴熟的人员开发的,这个过程也需要数年才能完成。这就是如果我们想要对编译器内部进行一些大规模重构的规模,它应该为那些问我们什么时候才能使用 面向数据设计 重写整个编译器以使其速度快上无数倍的人们设定一个预期。
说到 DoD,还需要考虑编译器代码库的可维护性。想象一下,我们再次挥动魔法棒,一夜之间使用 DoD、SIMD 向量化、手写汇编等重写了一切。它(可能)会快得多,太棒了!然而,我们不仅关心眼前的性能,还关心我们进行长期改进的能力。如果我们过度拟合性能,使用了难以演进、理解、调试或维护的代码,从长远来看,这对我们不利。编译器接受来自数百名志愿者的贡献,我们希望代码库对他们来说(至少在某种程度上)是容易理解的。因此,代码库的长期可维护性也是我们需要考虑的事情。
虽然上面提出的挑战非常真实,但它们当然并非 rustc
所独有的。对大型(编译器)代码库进行重大更改很困难的事实可能并不令人惊讶。我相信 GCC/LLVM/Clang 的维护者也有很多关于如何改进或加速的想法,但这需要大量的努力和时间才能完成。但编译器性能并非我们唯一关注的事情,还有其他原因。
优先级
尽管许多 Rust 用户似乎希望编译器性能成为 Rust 项目的首要优先事项,但我们不应忘记我们还必须牢记其他优先级。
Rust 编译器(至少在我看来)非常稳定和可靠,我可以指望每六周发布一个新版本。这绝非易事!我们不应将其视为理所当然。这并非仅仅从一个可以工作的状态开始,然后什么都不做就能实现的。仅仅为了确保一切正常运行,我们的基础设施和 CI 工作正常,真正糟糕的错误得到及时修复,收到的问题得到分类,向后兼容性得到维护,PR 得到审查,安全问题得到快速解决,我们紧跟 LLVM 等外部依赖项的变化等等,都需要大量的工作。我们还必须确保所有这些都能在我们所有 支持的目标 上运行!目前,这是 8 个一级目标(必须构建成功且所有测试都通过)和 91 个(!)二级目标(必须至少构建成功)。很大程度上,这项工作是由那些非常关心 Rust 及其工具链(:heart:)并希望它像以前一样良好地运行的志愿者完成的。Rust 上简直有大量的工作要做 9。
所有这些工作当然会占用原本可以用来例如加速编译器的时间。如果你认为这项工作并不那么重要,那么问问自己,如果你的编译器速度快了一倍,但开始随机错误编译你的代码,你是否会喜欢?:)
另一件事是,语言和编译器都在不断地获得新特性。新的 编译器标志 以支持像 Rust for Linux 这样的项目、 语法改进、 复杂的语言特性 以及许多其他东西。总的来说,这门语言似乎远未“完成”,很多事情都在进行中;几乎有 200 个开放的 RFC 拉取请求,还有许多其他的 RFC 被接受了,但尚未实现或稳定下来。在任何给定时间,我们在主要的 Rust 仓库中都有大约 700 个 打开的 PR,超过 1 万个 打开的 issue,简直有太多事情在发生了。
而且 似乎 Rust 用户希望 Rust 像今天一样快速发展,甚至更快,这意味着更多的变化!你上次“只是希望这个小的特性 X 最终稳定下来”,以便让你的代码更漂亮是什么时候?想要一门语言的新特性是完全可以的,但我们不应忘记(除其他事项外,如设计讨论)这需要在编译器中进行大量的实验、重构、bug 修复、测试和实现工作,这又会占用优化性能的时间。更不用说添加新特性通常会使编译器性能略微下降,因为它往往会做更多的工作,而不是相反。
贡献者
最后,花费在优化编译器性能上的努力量最终取决于为此做出贡献和维护它的人。他们通常是志愿者,并且有着不同的兴趣。一些(编译器)贡献者根本对性能优化不感兴趣,这完全没问题!请记住, Rust 不是一家公司(这是 Mara Bos 的一篇很棒的博客文章,我建议你去读一下!)。我们不会告诉人们他们应该做什么,也不会给他们分配任务或 issue。如果没有人对加速编译器感兴趣,那么它很可能不会完成,就这么简单。
也就是说,我们可以通过某些方式来赋能人们从事编译器性能工作。Rust 项目现在有了 项目目标 计划,该计划指定了一系列我们认为重要的目标和项目,我们明确欢迎在这些领域做出贡献(并承诺提供审查和支持)。我打算在不久的将来将编译器性能作为旗舰目标之一,因为我认为它确实应该成为其中之一。这或许可以激励更多的人参与进来。
即使这听起来有点老生常谈,更快地取得进展也(至少部分地)取决于资金。有时我会听到人们抱怨某些旨在提高编译性能的项目需要太长时间才能准备就绪。这听起来好像他们期望有一个由薪水丰厚的工程师组成的全职团队在从事这些项目。好吧,我们不应该忘记 Rust 项目仍然主要依靠志愿者,许多人都在业余时间为其做出贡献。Rust 的许多改进(包括一些重要的编译器性能项目)实际上是由一个不知疲倦地让 Rust 变得更好的大学学生推动的,他们甚至可能没有因此获得任何报酬,而进展停滞的原因可能是(例如)他们必须完成大学考试或做一些实际的带薪工作,以便能够支付房租和食物。
在最近的 RustWeek 会议和相关的全体会议活动中,我看到越来越多的公司对投资改进 Rust 编译器的性能表现出兴趣,这很棒!改进 rustc
的性能需要长期集中的努力,为从事这项工作的贡献者提供稳定的资金绝对是我们加快进展的方法之一。然而,重要的是,做出改进不仅仅是做实现工作;还需要有人来审查它,然后在将来维护它。在理想的世界中,公司会资助编译器维护的长期持续工作,而不是支付员工实施一个解决他们痛点的优化然后消失。有时,帮助 Rust 向前发展的最佳方式不是去实现它,而是积累知识,成为某个编译器领域的专家,并帮助审查其他人的代码,以便你可以让其他人能够完成实际工作:) 但这当然需要长期的投资(包括时间和金钱)。
根据我的经验,Rust 编译器贡献者对从事编译器性能工作的兴趣与这个话题在网上引发的讨论量并不相符(而且,这也没关系!)。如果我再次使用魔法棒,并且可以说服更多的贡献者关心这个问题,我会这样做吗?可能会的。从长远来看,这对 Rust 来说是好事吗?我不知道!到目前为止,语言和编译器的发展似乎进展顺利,我又有什么资格来决定优先级呢?我相信项目中的人们会共同做出对 Rust 现在和未来最好的决定。
我觉得有趣的一点是,也许更多的人实际上会从事性能工作,如果他们不必同时处理那么多其他事情的话。举个例子:当我 2021 年开始为 Rust 做出贡献时,我的主要兴趣是编译器性能。所以我开始做一些优化工作。然后我注意到 编译器基准测试套件 需要一些维护,所以我开始着手处理它。然后我注意到我们没有使用尽可能多的优化来编译编译器本身,所以我开始添加对 LTO/PGO/BOLT 的支持,这进一步导致了我们 CI 基础设施的改进。然后我注意到我们的 CI 工作流程需要很长时间,所以我开始 优化它们。然后我开始运行 Rust 年度调查,然后是我们的 GSoC 项目,然后改进我们的 bot,然后…… 许多这些贡献(至少我希望)以某种间接的方式影响了编译器的性能,要么是通过解除或支持其他贡献者,要么是通过改进我们的基础设施(就这个词的各种含义而言)。然而,直到现在,在几年分心之后,我终于试图回到直接的编译器性能工作。
换句话说,我们在维护 Rust 方面获得的帮助越多(无论在哪个具体领域!),我们集体拥有更多时间来改进编译器性能的机会就越大:)
结论
有趣的是,在我写完这篇文章后,我意识到我几乎可以用任何其他人们对 Rust 的期望(“为什么 Rust 不做 X”)来替换“编译器性能”这个术语,而许多相同的推理仍然适用。在这方面,编译器性能并没有那么特殊,它只是我们关心并尽力改进的(众多)非常重要的事情之一。
就未来的编译器性能工作而言,我对在全体会议上讨论的几个倡议以及 使 LLD 成为 Linux 上的默认链接器 持乐观态度,后者有望在未来几个月内最终落地。我还计划 很快 进行一次编译器性能调查,以便我们可以了解哪些工作流程导致 Rust 用户的瓶颈,我还想构建更好的基础设施来分析编译器在编译过程中实际在做什么。
我希望这篇博客文章能让您对为什么在构建性能方面取得进展如此不易有所了解。如果您想加入我们并为此做出贡献,我们 非常欢迎您的到来!请在 Reddit 上告诉我您的想法。
Footnotes
-
我使用了一个较旧的提交,这样即使使用较旧版本的
rustc
也能编译 ↩ -
抱歉,忍不住 ↩
-
至少对于某些”大型”的定义而言。编译器包含大约 60 万行 Rust 代码,标准库的大小也差不多 ↩
-
我最近看到有人称之为 vibeck(以编译器中使用的 typeck 和 borrowck 的缩写命名),我觉得非常可爱 ↩
-
如果你认为”为什么 AI 不能做到这一点?“,它可能可以在这方面帮助很多,但尤其是在编译器诊断方面的差异(这占据了我们测试套件很大一部分检查内容)通常归结为我们仍然(幸运地)留给人类的判断 ↩
-
我喜欢说,如果你给我一百名全职工程师,我立刻就能在 Rust 工具链中找到所有人都需要做的工作 ↩