如何对Rust编译器进行最后一次加速

2020-09-08 07:16:25

由于Mozilla最近的变化,我从事Rust编译器的时间即将结束。我仍然在Mozilla工作,但在可预见的未来,我将专注于Firefox的工作。

所以我想我应该结束我的“如何加速铁锈编译器”系列,这个系列始于2016年。

如何提高铁锈编译器的速度。原始的帖子,标题最有意义的帖子。它主要关注如何为性能工作设置编译器,包括性能分析和基准测试。它只提到了我的四个公关,所有这些公关都优化了分配。

如何进一步提高铁锈编译器的速度。这篇帖子转向主要关注与业绩相关的公关(其中15人),为本系列的其余部分定下了基调。我重复使用了“How to…”命名方案,因为我喜欢它的声音,尽管它有点不准确。

2018年如何加速铁锈编译器。在休息了一年多之后,我回到了铁锈编译工作。这篇帖子包括了关于配置编译器的最新信息,并描述了我的另外7个PR。

如何在2018年进一步提高Rust编译器的速度。这篇文章描述了对标准基准测试套件的一些改进,以及对更多分析工具的支持,涵盖了我的14个PR。由于来自读者的多个请求,我还包括了对失败的优化尝试的描述,事实证明这很受欢迎,我在随后的几篇帖子中也这样做了。(有几次,读者提出了一些建议,然后带来了后续的改进,这是很棒的。)。

如何在2018年加速Rustc编译器:NLL版。这篇文章描述了我的13个PR,它们帮助大大提高了新的借用检查器的速度,并介绍了整个系列中我最喜欢的一段:“html5ever基准测试在CI…上触发了内存不足故障。在2.5个月的时间里,我们将内存使用量从14 GB减少到10 GB、2 GB、1.2 GB、600MB、501MB,最后减少到266MB“。这是我最引以为豪的一些表演作品。新的借用检查器对Rust的可用性来说是一个巨大的胜利,而且它发布时几乎没有什么编译时间,这一结果在几个月内还远未确定。

如何在2019年加速铁锈编译器。这篇帖子覆盖了44(!)。我的PR包括更快的全局访问、性能分析改进、流水线编译,以及在新版DHAT的帮助下减少分配带来的一大堆微不足道的好处。

如何在2019年进一步提高铁锈编译器的速度。这篇文章描述了我的11个PR,包括几个对memcpy的最小化调用,几个改进ObligationForest数据结构的调用。它讨论了其他人减少库代码膨胀的一些PR。我还包括了自上一篇文章以来的总体性能变化的表格,这是我在后续文章中继续做的事情。

如何在2019年最后一次加速铁锈编译器。这篇文章描述了我的21个公关,包括两个不同的重构公关序列,它们无意中带来了性能上的胜利。

2020年如何加快Rust编译器的速度:这篇文章描述了我在性能方面的23个成功的PR,包括一个很大的改变,以及在不使用LTO时避免生成LLVM位码的好处(这是常见的情况)。这篇帖子还描述了我的5个代表失败尝试的公关。

如何在2020年进一步提高Rust编译器的速度:这篇文章描述了我的19个PR,包括几个与Cargo-llvm-line发现的LLVM IR减少有关的PR,以及几个与性能分析支持改进相关的PR。这篇帖子还描述了我开始的重要的每周绩效分类过程,并将由其他人继续进行。

铁锈编译器的速度越来越快。这篇文章提供了一些测量结果,表明总体速度有了显著的提高。

特别分析。这篇文章描述了一个简单但效果惊人的分析工具,我在我的很多公关中都用到了它。

如何使用-Zprint-type-size获取锈蚀类型的大小。这篇文章描述了如何查看Rust类型是如何在内存中布局的。

一顶更好的帽子。这篇帖子描述了我对dhat所做的改进,我在很多公关中都用到了它。

可视化的铁锈编译。这篇文章描述了一个新的货运功能,它可以生成图表,显示项目中铁锈板条箱的编译情况。它可以帮助项目作者重新排列他们的代码,以便更快地编译。

除了分享我一直在做的工作之外,这些帖子的一个目标是显示有人关心Rust编译器的性能,并且正在积极地进行研究。

将编译器速度降低到一个数字是很困难的,因为调用编译器的方法太多了,工作负载也太多了。尽管如此,我认为说编译器在很多情况下至少比几年前快了2-3倍并不是不准确的。(这是我所知道的最好的远程性能跟踪。)。

当我第一次开始分析编译器时,很明显,它在协调的分析驱动的优化工作方面没有收到太多东西。(可以稍微夸张地说,编译器基本上是对分配器和哈希表实现的压力测试。)。有很多容易摘到的果实,以简单而明显的变化的形式出现,取得了重大的胜利。今天,个人资料变得平坦得多,明显的改进对我来说也很难找到。

我的方法在很大程度上是由分析器驱动的。我所做的改进大多可以被描述为“自下而上的微优化”。我的意思是,它们是相对较小的更改,响应于概要文件,不需要太多自上而下理解编译器体系结构的方式。基本上,配置文件会指示某段代码是热的,我会尝试(A)使该代码更快,或(B)避免调用该代码。

很少有一个微优化是大问题,但是几十个和几十个都是大问题。坚持是关键。

我花了很多时间仔细研究个人资料,以寻找改进之处。我用不同的剖面仪测量了各种不同的东西。按用处从大到小的顺序排列:

周期(Perf),但只有在我发现了优秀的热点查看器…之后。我发现Perf自己的查看器工具几乎无法使用。(我没有发现有用的周期,因为它们与指令计数密切相关,并且指令计数测量的噪音较小。)

每次我做一种新的侧写,我都会发现新的需要改进的地方。我通常会联合使用多个分析器。例如,Cachegrind/Callgrind的输出显示malloc/free和memcpy是许多基准测试中最热门的函数之一,这促使我对dhat进行了改进,以跟踪分配和memcpys。我多次使用计数来洞察一段热门代码。

在我的脑海中,我可以想到一些未探索的分析领域:自我分析/查询、线程化东西(例如锁争用,特别是在并行前端中)、缓存未命中、分支预测失误、系统调用、I/O(例如磁盘活动)。此外,市场上有很多分析器,每个分析器都有自己的优势和劣势,每个人都有自己的专业领域,所以我相信即使是我确实仔细考虑过的分析器指标,也仍然有改进的余地。

我还做了两个更大的“架构”或“自上而下”的更改:流水线编译和LLVM位代码省略。这些变化显然在你可以做到的时候是很棒的,尽管它们需要自上而下的专业知识,而且新手很难做出贡献。我很高兴有一个增量编译工作组正在成立,因为我认为这是一个可能会在性能上取得一些重大胜利的领域。

良好的基准测试非常重要,因为编译器输入是复杂且高度可变的。不同的输入会以非常不同的方式给编译器带来压力。我几乎完全使用rustc-perf作为我的基准套件,它很适合我。该套件在过去几年中发生了相当大的变化,添加和删除了各种基准。我花了相当多的精力让所有不同的分析器使用它的工具。因为rustc-perf针对性能分析进行了很好的设置,所以每当我需要对一些新代码进行性能分析时,我都会简单地将其放入我本地的rustc-perf副本中。

编译器非常易于分析和优化,因为它们是确定性或几乎确定性的批处理程序。例如,评测Rust编译器要比评测Firefox容易得多,也更有趣。

与您可能预期的相反,事实证明,在检测CI性能变化时,指令计数比挂起时间要好得多,因为指令计数比挂起时间变化小得多(例如,±0.1%比±3%;前者非常有用,而后者几乎没有什么用处)。使用指令计数来比较两个完全不同程序的性能(例如,GCC vs.clang)是愚蠢的,但使用它们来比较两个几乎相同的程序(例如,PR#12345之前的rustc和PR#12345之后的rustc)的性能是合理的。在这种情况下,指令计数更改很少与墙时间更改不匹配。如果rustc前端的并行版本成为默认版本,那么看看指令计数是否继续以这种方式有效将是一件很有趣的事情。

令我惊讶的是,有这么多人说他们喜欢读这篇博文系列。(积极的反馈在一定程度上解释了为什么我写了这么多。)。人们对“我从这块石头上榨出了更多的血”的故事的胃口很高。也许这与人们对Rust的高度兴趣有关,也与人们从它的编译时代感受到的痛苦有关。人们也喜欢阅读失败的优化尝试。

Mark Rousskov负责维护Rustc-Perf和CI性能基础设施,并帮助我进行了许多Rustc-Perf更改。

安东尼·琼斯(Anthony Jones)和埃里克·拉姆(Eric Rahm),感谢他们理解了这项铁锈工作是如何让Firefox受益的,并让我花了一些Mozilla的工作时间在上面。

铁锈的存在和成功在某种程度上是一个奇迹。我期待成为一名铁锈用户很长一段时间。感谢所有做出贡献的人,并祝所有未来将为之做出贡献的人好运!