Rust 编译模型之殇
Rust 与 TiKV 的编译时冒险:第 1 集
为什么 Rust 编译那么慢,或者说让人感觉那么慢; Rust 的发展如何造就了编译时间的缓慢; 编译时用例; 我们测量过的,以及想要测量但还没有或者不知道如何测量的项目; 改善编译时间的一些思路; 事实上未能改善编译时间的思路; TiKV 编译时间的历史演进; 有关如何组织 Rust 项目可加速编译的建议; 最近和未来,上游将对编译时间的改进。
PingCAP 的阴影:TiKV 编译次数 “余额不足”
概览:TiKV 编译时冒险历程
造就编译时间缓慢的 Rust 设计
实用性(Practicality) :它应该是一种可以在现实世界中使用的语言;
务实(Pragmatism) :它应该是符合人性化体验,并且能与现有系统方便集成的语言;
内存安全性(Memory-safety) :它必须加强内存安全,不允许出现段错误和其他类似的内存访问违规操作;
高性能(Performance) :它必须拥有能和 C++ 比肩的性能;
高并发(Concurrency) :它必须为编写并发代码提供现代化的解决方案。
但这并不是说设计者没有为编译速度做任何考虑。例如,对于编译 Rust 代码所要做的任何分析,团队都试图确保合理的算法复杂度。然而,Rust 的设计历史也是其一步步陷入糟糕的编译时性能沼泽的历史。
注意
兔子飞奔几米(7):rustboot 构建 Rust 的时间;
仓鼠狂奔一公里(49):在 rustboot 退役后使用 rustc 构建 Rust 的时间;
树獭移动一万米(188):在 2020 年构建 rustc 所需的时间。
反正,几个月前我构建 Rust 的时候,花了五个小时。
(非)良性循环
为了 自动生成HTML解析器,实现了带标签的 break 和 continue 。
在分析了 Servo 内闭包使用情况之后实现了,所有权闭包(Owned closures)。
外部函数调用曾经被认为是安全的。这部分变化(改为了 Unsafe)得益于 Servo 的经验。
从绿色线程迁移到本地线程,也是由构建 Sevro、观察 Servo 中 SpiderMonkey 集成的 FFI 开销以及剖析“hot splits”的经验所决定的,其中绿色线程堆栈需要扩展和收缩。
Rust 和 Servo 的共同发展创造了一个 良性循环,使这两个项目蓬勃发展。今天,Servo 组件被深度集成到火狐(Firefox)中,确保在火狐存活的时候,Rust 不会死去。
运行时优先于编译时的早期决策
借用(Borrowing)——Rust 的典型功能。其复杂的指针分析以编译时的花费来换取运行时安全。
单态化(Monomorphization)——Rust 将每个泛型实例转换为各自的机器代码,从而导致代码膨胀并增加了编译时间。
栈展开(Stack unwinding)——不可恢复异常发生后,栈展开向后遍历调用栈并运行清理代码。它需要大量的编译时登记(book-keeping)和代码生成。
构建脚本(Build scripts)——构建脚本允许在编译时运行任意代码,并引入它们自己需要编译的依赖项。它们未知的副作用和未知的输入输出限制了工具对它们的假设,例如限制了缓存的可能。
宏(Macros)——宏需要多次遍历才能展开,展开得到的隐藏代码量惊人,并对部分解析施加限制。过程宏与构建脚本类似,具有负面影响。
LLVM 后端(LLVM backend)——LLVM 产生良好的机器代码,但编译相对较慢。
过于依赖LLVM优化器(Relying too much on the LLVM optimizer)——Rust 以生成大量 LLVM IR 并让 LLVM 对其进行优化而闻名。单态化则会加剧这种情况。
拆分编译器/软件包管理器(Split compiler/package manager)——尽管对于语言来说,将包管理器与编译器分开是很正常的,但是在 Rust 中,至少这会导致 cargo 和 rustc 同时携带关于整个编译流水线的不完善和冗余的信息。当流水线的更多部分被短路以便提高效率时,则需要在编译器实例之间传输更多的元数据。这主要是通过文件系统进行传输,会产生开销。
每个编译单元的代码生成(Per-compilation-unit code-generation)——rustc 每次编译单包(crate)时都会生成机器码,但是它不需要这样做,因为大多数 Rust 项目都是静态链接的,直到最后一个链接步骤才需要机器码。可以通过完全分离分析和代码生成来提高效率。
单线程的编译器(Single-threaded compiler)——理想情况下,整个编译过程都将占用所有 CPU 。然而,Rust 并非如此。由于原始编译器是单线程的,因此该语言对并行编译不够友好。目前正在努力使编译器并行化,但它可能永远不会使用所有 CPU 核心。
trait 一致性(trait coherence)——Rust 的 trait(特质)需要遵循“一致性(conherence)”,这使得开发者不可能定义相互冲突的实现。trait 一致性对允许代码驻留的位置施加了限制。这样,很难将 Rust 抽象分解为更小的、易于并行化的编译单元。
“亲密”的代码测试(Tests next to code)——Rust 鼓励测试代码与功能代码驻留在同一代码库中。由于 Rust 的编译模型,这需要将该代码编译和链接两次,这份开销非常昂贵,尤其是对于有很多包(crate)的大型项目而言。
改善 Rust 编译时间的最新进展
Rust 编译时 主要问题:
跟踪各种工作以缩短编译时间。
全面概述了影响 Rust 编译性能的因素和潜在的缓解策略。
流水线编译 ( 1 , 2 , 3 )
与上游代码生成并行地对下游包进行类型检查。现在默认情况下在稳定(Stable)频道上。
由 @alexcrichton 和 @nikomatsakis 开发。
并行 rustc ( 1 , 2 , 3 )
并行运行编译器的分析阶段。稳定(Stable)频道尚不可用。
由 @Zoxc , @michaelwoerister , @oli-obk , 以及其他一些人开发。
MIR 级别的常量传播(constant propagation)
在 MIR 上执行常量传播,从而减少了 LLVM 对单态函数的重复工作。
由 @wesleywiser 开发。
MIR 优化
优化 MIR 应该比优化单态 LLVM IR 更快。
稳定(Stable)编译器尚不可用。
由 @wesleywiser 和其他人一起开发。
cargo build -Ztimings ( 1 , 2 )
收集并图形化有关 Cargo 并行建造时间的信息。
由 @ehuss 和 @luser 开发。
rustc -Zself-profile ( 1 , 2 , 3 )
生成有关 rustc 内部性能的详细信息。
由 @wesleywiser 和 @michaelwoerister 开发。
共享单态化(Shared monomorphizations)
通过消除多个包(crate)中出现的单态化来减少代码膨胀。
如果优化级别小于 3,则默认启用。
由 @michaelwoerister 开发。
Cranelift 后端
通过使用 cranelift 来生成代码,减少了 Debug 模式的编译时间。
由 @bjorn3 开发。
perf.rust-lang.org
详细跟踪了 Rust 的编译时性能,基准测试持续增加中。
由 @nrc , @Mark-Simulacrum , @nnethercote 以及其他人一起开发。
cargo-bloat
查找二进制文件中占用最多空间的地方。膨胀(Bloat)会影响编译时间。
由 @RazrFalcon 和其他人一起开发。
cargo-feature-analyst
发现未使用的特性(features)。
由 @psinghal20 开发。
cargo-udeps
发现未使用的包(crate)。
由 @est31 开发。
twiggy
分析代码大小,该大小与编译时间相关。
由 @fitzgen , @data-pup 以及其他人一起开发。
rust-analyzer
用于Rust的新语言服务器,其响应时间比原始 RLS 更快。
由 @matklad , @flodiebold , @kjeremy 以及其他人一起开发。
“如何缓解 Rust 编译时间带来的痛苦”
vfoley 写的博文。
“关于 Rust 代码膨胀的思考”
@raphlinus 写的博文。
Nicholas Nethercote 对 rustc 的优化工作:
“2019 年 Rust 编译器如何提速”
“Rust 编译器的速度持续变快”
“可视化 Rust 编译”
“如何在 2019 年进一步提升 Rust 编译器的速度”
“如何在 2019 年最后一次提升 Rust 编译器”
《原力计划【第二季】- 学习力挑战》
正式开始
即日起至 3月21日
千万流量支持原创作者
更有专属【勋章】等你来挑战
☞狂赚 1200 亿,差点收购苹果,影响千万程序员,那个叫做太阳的公司却陨落了!
☞AWS还是Firebase?在移动应用后端应该使用哪个?
☞两成开发者月薪超 1.7 万、算法工程师最紧缺!| 中国开发者年度报告