大牛最新研究!提速Rust编译器!


大牛最新研究!提速Rust编译器!

文章插图
作者 | .NEThercote
编译 | 王瑞平、言征
Nethercote是一位研究Rust编译器的软件工程师 。最近 , 他正在探索如何提升Rust编译器的性能 , 在他的博客文章中介绍了Rust编译器是如何将代码分割成代码生成单元(CGU)的以及rustc的性能加速 。
他解释了不同数量和大小的CGU之间的权衡以及Rustc是如何使用LLVM并行化代码生成和优化的 。此外 , Nethercote还探索了一些形成和排序CGU的替代方法 , 并报告了他的实验结果 。
Nethercote发现 , 很多时候 , 无法在编译速度、内存占用、编译体积和质量上都实现提升 , 一个指标的提升 , 经常伴随另一个性能指标的下降 。尽管他没有发现比现有方法更明显的改进 , 但还是希望在未来继续研究这个问题 。
如何提升Rust编译器速度?这篇文章或许能帮助到你!
LLVM:Rust编译加速的秘诀Rust的MIR是HIR到LLVM IR的中间产物 , 将MIR转换为LLVM IR , 然后将其传递给LLVM , 从而生成机器代码 。
在此过程中 , LLVM能通过处理多个模块实现并行 。Rustc使用LLVM加速Rust的编译 。我们称其中的每个模块为 “代码生成单元(CGU)” 。
大牛最新研究!提速Rust编译器!

文章插图
图:时间位于 x 轴上 , 每条水平线代表一个线程 。主线程显示在顶部 , 标有 PID 。它在开始时处于活动状态 , 时间足以产生另一个标记为 的线程rustc 。rustc底部显示的线程在大部分执行过程中都处于活动状态 。还有 16 个 LLVM 线程标记opt cgu.00为 到opt cgu.15 , 每个线程都会在短时间内处于活动状态 。
CGU实际上是如何形成的呢?粗略地说 , Rust 程序由许多函数组成 , 这些函数形成一个有向图 , 其中从一个函数到另一个函数的调用构成了一条边 。我们需要将这个图分割成块(CGU) , 这是一个图分区问题 。我们希望创建大小大致相等的 CGU(因此 LLVM 处理它们所需的时间长度大致相同) , 并最大限度地减少它们之间的边数(因为这使 LLVM 的工作更轻松 , 并带来更好的代码质量)。
实际上 , 由于我们上面看到的阶梯效应 , 我们不希望 CGU 的大小完全相同 。理想的情况是 CGU 大小存在与梯度相匹配的轻微梯度 。这样 , 所有 CGU 将完全相同地完成处理 , 以实现最大程度的并行化 。
大牛最新研究!提速Rust编译器!

文章插图
合并之前的CGU(9个)
Nethercote认为在合并之前“调整”CGU可能会有所帮助 , 在某些情况下将函数从一个CGU移动到另一个 。例如 , 如果在CGU A中被调用f的叶函数(即不调用任何其他函数的叶函数)在CGU B中有一个调用方g , 那么将f从A移动到B是有意义的 , 从而去除CGU间的边 。(还有其他类似的情况涉及非叶函数 , 移动也有意义) 。我实现了这一点 , 它给出了一些适度的改进 , 但我目前还没有决定它是否值得额外的复杂性 。
大牛最新研究!提速Rust编译器!

文章插图
调整之后的CGU(5个)
在实现这一点的同时 , 我还花了一些时间来可视化调用图 。我从GraphViz开始 。这些图表对于非常小的程序来说看起来不错 , 但对于较大的程序来说 , 它们很快就变得无法读取和导航 。我在Mastodon上抱怨过这一点 , 并得到了使用d2的建议 , d2速度较慢 , 但图形可读性更强 。
后端并行方法的软肋
图划分是一个 NP 难题 。有几种常见的算法 , 实现起来相当复杂 。相反 , rustc 做了一些更简单的事情 。首先简单地为每个 Rust 模块创建一个 CGU:模块中的每个函数都放入同一个 CGU 中 。然后 , 如果 CGU 数量超过限制(默认情况下 , 非增量构建为 16 个 , 增量构建为 256 个) , 它会重复合并两个最小的 CGU , 直到达到限制 。这种方法简单、快速 , 并以有用的方式利用特定领域的知识——程序模块往往提供良好的自然边界 。
【大牛最新研究!提速Rust编译器!】所有这一切都依赖于测量 CGU 大小的方法 。目前使用CGU中的MIR语句的数量来估计LLVM处理CGU需要多长时间 。这里有很大的设计空间 , 有许多其他可能的形成和规划CGU 的方法 。


推荐阅读