写点什么

提速 Rust 编译器!

作者:这我可不懂
  • 2023-08-07
    福建
  • 本文字数:3143 字

    阅读完需:约 10 分钟

提速Rust编译器!

Nethercote 是一位研究 Rust 编译器的软件工程师。最近,他正在探索如何提升 Rust 编译器的性能,在他的博客文章中介绍了 Rust 编译器是如何将代码分割成代码生成单元(CGU)的以及 rustc 的性能加速。


他解释了不同数量和大小的 CGU 之间的权衡以及 Rustc 是如何使用 LLVM 并行化代码生成和优化的。此外,Nethercote 还探索了一些形成和排序 CGU 的替代方法,并报告了他的实验结果。


Nethercote 发现,很多时候,无法在编译速度、内存占用、编译体积和质量上都实现提升,一个指标的提升,经常伴随另一个性能指标的下降。尽管他没有发现比现有方法更明显的改进,但还是希望在未来继续研究这个问题。


如何提升 Rust 编译器速度?这篇文章或许能帮助到你!

1、LLVM:Rust 编译加速的秘诀


Rust 的 MIR 是 HIR 到 LLVM IR 的中间产物,将 MIR 转换为 LLVM IR,然后将其传递给 LLVM,从而生成机器代码。在此过程中,LLVM 能通过处理多个模块实现并行。Rustc 使用 LLVM 加速 Rust 的编译。我们称其中的每个模块为“代码生成单元(CGU)”。


图片


图:时间位于 x 轴上,每条水平线代表一个线程。主线程显示在顶部,标有 PID。它在开始时处于活动状态,时间足以产生另一个标记为 的线程 rustc。rustc 底部显示的线程在大部分执行过程中都处于活动状态。还有 16 个 LLVM 线程标记 opt cgu.00 为 到 opt cgu.15,每个线程都会在短时间内处于活动状态。


CGU 实际上是如何形成的呢?粗略地说,Rust 程序由许多函数组成,这些函数形成一个有向图,其中从一个函数到另一个函数的调用构成了一条边。我们需要将这个图分割成块(CGU),这是一个图分区问题。


我们希望创建大小大致相等的 CGU(因此 LLVM 处理它们所需的时间长度大致相同),并最大限度地减少它们之间的边数(因为这使 LLVM 的工作更轻松,并带来更好的代码质量)。


实际上,由于我们上面看到的阶梯效应,我们不希望 CGU 的大小完全相同。理想的情况是 CGU 大小存在与梯度相匹配的轻微梯度。这样,所有 CGU 将完全相同地完成处理,以实现最大程度的并行化。


合并之前的CGU(9个)


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


调整之后的CGU(5个)


在实现这一点的同时,我还花了一些时间来可视化调用图。我从 GraphViz 开始。这些图表对于非常小的程序来说看起来不错,但对于较大的程序来说,它们很快就变得无法读取和导航。我在 Mastodon 上抱怨过这一点,并得到了使用 d2 的建议,d2 速度较慢,但图形可读性更强。


图片

2、后端并行方法的软肋


图划分是一个 NP 难题。有几种常见的算法,实现起来相当复杂。相反,rustc 做了一些更简单的事情。首先简单地为每个 Rust 模块创建一个 CGU:模块中的每个函数都放入同一个 CGU 中。然后,如果 CGU 数量超过限制(默认情况下,非增量构建为 16 个,增量构建为 256 个),它会重复合并两个最小的 CGU,直到达到限制。这种方法简单、快速,并以有用的方式利用特定领域的知识——程序模块往往提供良好的自然边界。


所有这一切都依赖于测量 CGU 大小的方法。目前使用 CGU 中的 MIR 语句的数量来估计 LLVM 处理 CGU 需要多长时间。这里有很大的设计空间,有许多其他可能的形成和规划 CGU 的方法。


图片


这种转换对 Rust 众多语法糖进行了脱糖,并且极大精简了 Rust 的语法(但并非其语法子集),是观察和分析 Rust 代码的常用手段,尤其是在控制流图和借用检查等方面。


在这篇文章的最后,Nethercote 提供了几个数据集的链接,每个数据集都记录了编译 rust -performance 基准时每个 CGU 的测量值。这些数据集包括许多测量静态代码大小的输入(独立变量),例如,函数数量和 MIR 数量等。


Nethercote 试着用 scikit-learn 做一些基本的分析。并且,通过这些基本的分析,能让 Nethercote 仔细推敲到底应该搜集哪些测量值。


通过一系列的改进优化,他获得的最终数据集比刚开始时的数据更准确。但是,并没有通过这些数据获得多少实际的结果。实际上,每次我对测量的内容改变后都会得到完全不同的结果。

3、实现更快的 Lexer


词法分析(lexical analysis)是编译器的第一个阶段,实现词法分析的代码称为 lexer。


有人最近研究了 logos(https://github.com/maciejhirsz/logos)这个在 rust 中广受欢迎的 lexer。


此前,logos 声称其目标是能比手动实现的 lexer 更快,作者提出了质疑,因为在他看来,通用性和性能无法兼得。因此,他一步步实现了 lexer,探索了多种优化技巧,并与 logos 进行了多轮性能对比。


最终的结果表明,手动实现的基于状态机的 lexer 比 logos 实现了 20%左右的性能提升。

4、从错误中学习:使用 Rust 实现 DLL 注入


Rust 是一种注重安全性的编程语言,但在某些情况下,开发人员可能需要使用 unsafe 关键字来执行某些操作。unsafe 可以提供更高的性能,但可能会牺牲安全性。因此,开发人员在使用时需要非常小心。几个使用 unsafe 的常见场景包括:访问裸指针、调用外部 C 函数等,并提供了一些建议和最佳实践,以确保在使用 unsafe 时不会引入潜在的安全隐患。


举个应用方面的例子:原来,作者一直在用 C++编写逆向工具,但是,C++这门语言并不友好,于是研究了下如何使用 Rust 实现 DLL 注入的“工具”。


大致原理就是让 Rust 首先生成一个 C 样式的 DLL,然后,使用 unsafe 操作裸指针,操作程序内存,最后实现 DLL 注入就可以了。

5、期待更准确的估计函数


Nethercote 希望具有数据分析专业知识的人可以做得更好,重点关注以下几个方面:

1)更匹配的估计函数

2)想要使编译器比现在更快,一个更好的估计函数也许不会达到预期的效果。我提出了一些更好的统计方法,但并没有提升编译速度,甚至变差。

3)CGU 调度效果不可预测,你不能假设一个估计函数好几个百分点就会使编译器更快。话虽如此,我希望改进力度足够大,能够转化为实际的加速。

4)对于估计函数来说,最好高估 CGU 编译所需的时间,而不是低估。

5)我很担心过度拟合。数据集来自一台机器,但实际上,rustc 会运行在不同的机器上,具有各种各样的体系结构和微体系结构。

6)这些数据集来自单一版本的 rustc,使用单一版本的 LLVM。我担心随着时间的推移准确性可能会漂移。

7)我更喜欢不太复杂且易于理解的估计函数。当前的函数非常简单,在大多数情况下只是增加了基本模块和语句的数量。例如:0 大小的 CGU 应该别估计为花费非常接近于 0 的时间。

8)估计函数有一个明确的问题,即如果不考虑其内部公式,计算 MIR 语句可能非常不准确。特别是,单个 MIR 语句可能变得很长。举个例子:深度向量压力测试的 MIR 包含一条语句,该语句定义了包含超过 100,000 个元素的向量字面量。不出所料,当前的估计函数严重低估了编译这个基准所需的时间。

Nethercote 最后提醒:希望以上的请求是合理的!


以下是上文提到的数据集:

  • 调试构建,主要基准测试

https://nnethercote.github.io/aux/2023/07/25/Debug-Primary.txt

  • 选择构建,主要基准

https://nnethercote.github.io/aux/2023/07/25/Opt-Primary.txt

  • 调试构建,二级基准测试

https://nnethercote.github.io/aux/2023/07/25/Debug-Secondary.txt

  • 选择构建,二级基准

https://nnethercote.github.io/aux/2023/07/25/Opt-Secondary.txt

  • 顺便说一句:在这些数据集中,主要基准测试比次要基准测试更重要,次要基准测试包括压力测试、微基准测试和其它不符合实际的代码。


参考资料:

1.https://nnethercote.github.io/2023/07/25/how-to-speed-up-the-rust-compiler-data-analysis-assistance-requested.html

2.https://geo-ant.github.io/blog/2023/unsafe-rust-exploration/

3.https://nnethercote.github.io/2023/07/11/back-end-parallelism-in-the-rust-compiler.html

发布于: 刚刚阅读数: 4
用户头像

低代码技术追随者,为全民开发而努力 2023-02-15 加入

大家好,我是老王,专注于分享低代码图文知识,感兴趣的伙伴就请关注我吧!

评论

发布
暂无评论
提速Rust编译器!_rust_这我可不懂_InfoQ写作社区