vlambda博客
学习文章列表

Rust 视界 | 为 Rust 编译器提速

 

理清头脑混沌,觉醒心智天地


Mozilla 工程师 Nicholas 总结了他自己迄今为止为提升 Rust 编译器的编译速度而作的改进(Pull Request)。

我们可以从他所做的贡献中,对 Rust 编译器的编译细节有所了解。因为 rustc 也是 Rust 实现的,所以也能从中学习一些编写高性能 Rust 代码的经验。

注:本文并非完整翻译,只是重点摘录,以及针对其中的某些内容进行了一些内容扩展。

https://blog.mozilla.org/nnethercote/2020/04/24/how-to-speed-up-the-rust-compiler-in-2020/



增量编译


#68914 :  增量编译使用「SipHasher128」哈希算法来确定自上一次编译器调用以来更改了哪些代码。此PR极大地改善了从输入字节流中提取字节的过程(通过反复进行来确保它在big-endian和little-endian平台上均可工作),在大多数情况下,编译速度最多可提升13%。

在该 PR 中,Nicholas 使用一种简单的移位算法,来替代之前的缓慢算法,带来的好处是,代码量更小,消除了很多 unsafe 代码,性能也提升了。在代码的 Review过程中,还讨论了大小端字节序对哈希算法的影响。而 Rust 的 CI 跑在 ARM、x86 和 WASM 上运行测试,没有大端(big-endian)平台。但通常来说, 对于不同的 CPU 架构,Rust 默认会用对应的主机字节次序存储整形数据,而提升性能。所以,最后的讨论结果是,默认按小端序实现正确,然后留下了注释,在大端序调用相关函数的时候,需要调用方转换字节序。

#69050 :Rust 的 crate 中存储元数据(metadata)广泛使用 LEB 128 编码。但是Rustc 对其编解码的速度还不够快,这个 PR 就是减少了编解码过程中的循环次数,从而提升了性能。并且还消除了一个 Unsafe 的使用。

作者为了这个 PR ,通过使用Callgrind进行性能分析,作者发现 clap-rs-Check-CleanIncr 是受 LEB128 编码影响最大的基准测试+运行+构建组合。先后尝试了 18 种不同的方法进行分析,并且其中有 10 种方法都有性能改进效果。最终选择了现在的改进方法。

可想而知,要写出性能极致的 Rust 代码,还需要耐心且科学地分析才能做到。


LLVM 中间代码(Bitcode)


BitCode 是 LLVM 引入的一种中间代码,它是源码被编译为二进制机器码过程中的中间形态,也就是说,它既不是源码,也不是机器码。

LLVM 在编译过程中会对代码进行优化,这个优化就是基于BitCode来做。对 BitCode 进行各种类型优化,进行某种逻辑等价的交换,从而使得代码执行效率更高,体积更小。

关于 BitCode 更多介绍,可以查看这篇文章:https://xelz.info/blog/2018/11/24/all-you-need-to-know-about-bitcode/

Rust 在 rlib 和 dylib 中会存储 LLVM BitCode,以便 Rustc 能执行 跨 crate LTO(链接时优化)。

去年,作者从 Rust 的配置文件中注意到 rustc 花了一些时间来压缩它生成的LLVM BitCode,尤其是在 Debug 模式下。于是作者尝试将其更改为不去压缩 BitCode,这样可以加快一些速度,但也显着增加了磁盘上已编译工件的大小。

然后 Alex Crichton (官方人员)告诉作者一些重要的事情:编译器总会为 crate 生成目标代码和 BitCode。正常编译时使用目标代码,而通过链接时间优化(LTO)进行编译时则使用BitCode。用户只能同时而选一,因此生成两种代码通常浪费时间和磁盘空间。

于是作者发了一个 RR #66961,希望从 rlib 中不要存储 LLVM BitCode ,否则会导致增量编译的缓存过大。然而这引起了广泛的讨论,经历了七八个PR 重构之后,最终在 #71323 解决了此问题。

在 Debug 模式下,性能提升了 18% ,rlibs 磁盘占用缩减了 15% 到 20%。如果没有用 Cargo 而直接使用 rustc,则需要加 -Cbitcode-in-rlib=no 才能应用该特性。

其他改进


#67079:  改进用于热调用模式(hot calling pattern)的 shallow_resolved 函数,性能提升 2%。

#67340: 缩减 Nonterminal 字符(一般可认为是变量,可被替换的符号)大小(到40字节),在构建 serde_derive 的时候大量降低了 memcpy  的调用。性能提升 2% 。

#68694:  减少了InferCtxt中对 RefCell结构的借用,性能提升 5%。

#68848:  编译器的宏解析代码包含一个循环,该循环在每次迭代时实例化一个大型的(Parser类型的)复杂值,但是这些迭代中的大多数并没有修改该值。此PR更改了代码,因此它在循环外初始化了一个解析器值,然后使用Cow避免 Clone 它(修改迭代除外),从而使html5ever基准测试速度提高了15%。(比较有意思的是, 作者说他经常用 Cow,但是他从来却记不住关于 CoW 的使用细节,每次只能去翻文档。。


困扰链接速度提升的一个悬而未决的Bug



将 LLD (LLVM 4.0 引入的)作为链接器,可以将链接的时间成倍地提升。然而,  issues 39915 报告了一个 Bug,导致至今 LLD 都无法成为 rustc 的默认链接器。

LLD 的特色:

  1. 交叉编译非常友好(重点在于嵌入式目标)。

  2. 速度非常快。对于增量编译来说,链接时间会占编译时间的一大部分,因此能把这个时间减半相当重要。

当前 Rust 和 LLD 的状态:

  1. Rust 以二进制文件发布了一个 lld 的副本,rust-lld,可以用于大多数平台

  2. rust-lld 默认以 裸机(bare metal)为目标

  3. rust-lld 默认用于 wasm

  4. 可以使用“ -C linker-flavor”明确要求使用 rust-lld

在其他地方(Linux/ Mac/ Windows)使用 LLD 的问题:

  1. lld 的 macOS 后端崩溃了,虽然已经开始重写,但还太早期

  2. 在linux / unix平台上,不应直接调用ld / lld。而应该通过系统c编译器(即gcc)来调用链接器,链接器的职责是发现像crt1.o这样的系统符号并将其提供给ld。这意味着不能“仅仅”使用rust-lld,而必须将其输入gcc / clang 等等。

  3. Windows-msvc显然还可以,并且似乎在后端使用rust-lld的支持有限,但是Rust 官方还不清楚在这里需要做什么。

  4. Windows-mingw似乎与linux / unix大致类似,除了可能会得到一个古老的GCC,而且事情有些古怪,因为伪Windows-Linux并不是经过严格测试的配置?

更一般地来说,lld是新事物,它不是大多数操作系统的默认设置,如果我们在更多地方使用它,几乎可以肯定会出现随机的复合错误。

和去年的编译性能比较





总体而言,还不错。绿色代表性能提升,而红色则表示相反。