如何在2020年内进一步提高Rust编译器的速度

2020-08-05 13:07:14

我上一次写这篇文章是在4月份,讲述了我在加速Rust编译器方面所做的工作。又到了更新的时间了。

首先是流程改变:我已经开始每周进行性能分类。每周二,我都会查看过去一周合并的所有公关的业绩结果。对于性能下降或提高了不可忽略的每个PR,我都会向PR添加一条注释,并提供指向测量结果的链接。我还将这些结果收集到一份每周报告中,本周的“锈”杂志中提到了这一点,并在每周的编译器团队会议上进行了研究。

这样做的目标是确保快速捕获倒退并采取适当的操作,并提高对总体性能问题的认识。每次大约需要45分钟。说明书的编写方式是任何人都可以做的,尽管新手需要一些练习才能适应这个过程。我已经开始分享这项任务,马克·鲁斯科夫(Mark Rousskov)正在进行最近的分诊。

这一流程变化的灵感来自尼基塔·波波夫(又名尼基塔·波波夫)的一篇优秀的BLOST帖子中的“防止倒退”一节。Nikic),关于他们一直在做的提高LLVM速度的工作。(这个过程还借鉴了我几年前领导Project Uptime时建立的Firefox Nighly Crash分类的一些想法。)。

LLVM的速度直接影响到rustc的速度,因为rustc使用的是LLVM作为后端。这在实践中是一件大事。升级到LLVM10导致了rustc的一些显著的性能倒退,尽管几乎在同一时间有足够的其他性能改进,相关的rustc版本总体上仍然更快。然而,多亏了Nikic的工作,升级到LLVM 11将夺回升级到LLVM 10时损失的大部分性能。

似乎LLVM的性能在过去可能没有受到太多关注,所以我很高兴看到这个新的关注点。有条不紊的绩效工作需要大量的时间和精力,从长远来看,不是一个人就能有效地完成的。我强烈建议那些从事LLVM工作的人将这作为一个团队工作,并且任何有相关技能和/或兴趣的人都可以参与进来。

对rustc-perf也进行了一些重大改进,rustc-perf是驱动perf.rust-lang.org的性能套件和工具,也用于本地基准测试和评测。

#683:本地基准测试和性能分析命令的命令行界面很难看,令人困惑,以至于有人在Zulip上提到他们尝试使用它们,但都失败了。我们真的希望人们进行本地基准测试和分析,所以我提交了这个问题,然后实现了PR#685和#687来修复它。为了让您对改进有一个了解,下面显示了对整个套件进行基准测试的最小命令。

#旧目标/发布/收集器--db<;DB>;BENCH_LOCAL--rustc<;RUSTC>;--Cargo<;Cargo>;<;ID>;#新目标/发布/收集器BENCH_LOCAL<;RUSTC<;ID>;

#675:Joshua Nelson增加了对Rustdoc基准测试的支持。这很好,因为Rustdoc的性能在过去很少受到关注。

#699、#702、#727、#730:这些PR为本地基准测试和性能分析命令添加了一些适当的CI测试,这些命令有被无意破坏的历史。

Mark Rousskov还对rustc-perf做了许多小改进,包括减少运行套件所需的时间,以及改进状态信息的表示。

去年我写了关于内联和代码膨胀的文章,以及它们如何对编译时间产生重大影响。我提到过测量代码大小的工具会很有帮助。因此,我很高兴了解到非常棒的Cargo-llvm-line,它测量每个函数生成了多少行LLVM IR。结果可能会令人惊讶,因为泛型函数(特别是像vec::ush()、option::map()和result::map_err()这样的公共函数)可以在单个机箱中被实例化数十次甚至数百次。

#15:这个PR增加了Cargo-llvm-line输出的百分比,从而更容易判断每个函数对代码总量的贡献有多重要。

#20、#663:这些PR在rustc-perf中添加了对Cargo-llvm-line的支持,这使得测量为标准基准生成的LLVM IR变得容易。

#72013:rawVec::Growth()是由vec::Push()调用的函数。它是一个很大的泛型函数,处理与向量增长有关的各种情况。此PR将大多数非泛型代码移到单独的非泛型函数中,可获得高达5%的收益。

(即使在那次PR之后,测量显示向量增长代码仍然占据了不小的代码量,而且感觉还有进一步改进的空间。我做了几次尝试,试图进一步改进它,但都失败了:#72189,#73912,#75093,#75129。即使它们减少了LLVM IR生成的量,它们也是性能损失。我怀疑这是因为这些额外的更改影响了其中一些函数的内联,这可能会很热门。)。

#72166:这份PR为切片添加了一些专门的迭代器方法(for_each(),all(),any(),find(),find_map()),在clap-rs上赢得了高达9%的胜利,在其他各种基准上赢得了高达2%的胜利。

#72139:此PR为Iterator::Fold()添加了一个直接实现,取代了调用更通用的Iterator::Try_Fold()的旧实现。在几个基准上,这赢得了高达2%的胜利。

#73882:此PR简化了RawVec::Allocation_in()中的代码,在众多基准测试中赢得了高达1%的支持。

Cargo-llvm-line对应用程序/机箱作者也很有用。例如,Simon Sapin设法将Servo中最大的板条箱的编译速度提高了28%!使用Cargo安装它,安装Cargo-llvm-line,然后使用Cargo llvm-line(用于调试版本)或Cargo llvm-line--release(用于发布版本)运行它。

#71942:此PR将LocalDecl类型从128byte缩小到56byte,将一些基准测试的峰值内存使用率降低了几个百分点。

#72227:如果您将多个元素推送到一个空的vec上,它必须重复重新分配内存。所使用的增长战略产生了以下容量序列:0、1、2、4、8、16等。“微小的VEC是愚蠢的”,所以这个PR在大多数情况下将其更改为0、4、8、16等,这使得rustc本身完成的分配数量减少了10%或更多,并将许多基准测试的速度提高了高达4%。理论上,此更改可能会增加内存使用量,但实际上并非如此。

#74310:此PR将SparseBitSet更改为使用数组VEC而不是SmallVEC作为其存储,这是可能的,因为预先知道最大长度,对于高达1%的WIND。

#75133:此PR消除了结构中的两个字段,这两个字段仅在内部编译器错误的情况下用于打印错误消息,WINS高达2%。

自从我上一篇博客文章以来,编译时间的变化好坏参半(表、图)。没有像上次那样在表格结果中看到一片绿色的海洋,这是令人失望的,而且还有许多似乎令人担忧的倒退。但这并不像乍看起来的那么糟糕!要理解这一点,需要稍微了解基准测试套件的细节。

大多数出现较大百分比回归的基准都是极短时间运行的。(基准测试说明有助于使这一点更加清晰。)。例如,HelloWorld的非增量检查构建从0.03s变为0.08s。(#70107和#74682是两个主要原因。)。在实践中,当许多板条箱需要几秒钟或几十秒来编译时,每个板条箱几毫秒的微小额外开销将不会被注意到。

在“现实世界”基准中,一些基准的结果好坏参半(例如regex、ripgrep),而另一些则有明显的改进,其中一些改进幅度很大(例如clap-rs、style-server、webrender、webrender-wench)。

考虑到所有这些,从我上一篇文章开始,对于大多数实际情况,编译器可能要么不慢,要么稍微快一点。

另一个关于长期生锈速度的有趣数据点来自Hacker News:一个项目(Lewton)的编译速度在过去三年中快了2.5倍。

LLVM11还没有落地,所以这很快就会给现实世界的案例带来很大的改进。希望在我的下一篇帖子中,结果会更加一致地积极。