常量传播现在在 nightly 版本中默认开启

2019年12月2日 · Wesley Wiser 代表 MIR 优化工作组

我很高兴地宣布,中间层 IR (MIR) 常量传播 passes 已在 Rust nightly 版本中默认开启,最终将成为 Rust 1.41!

什么是常量传播?

常量传播是一种优化,编译器识别可以在编译时运行的代码,对其进行求值,并将原始代码替换为结果。

例如

const X: u32 = 2;

let y = X + X;

编译器可以识别出 X 的值在编译时已知,而不是在运行时对 X + X 进行求值,并将其替换为正确的值,结果为

const X: u32 = 2;

let y = 4;

此优化是机会性的,即使常量未声明为常量,它也会自动识别

struct Point {
  x: u32,
  y: u32,
}

let a = 2 + 2; // optimizes to 4
let b = [0, 1, 2, 3, 4, 5][3]; // optimizes to 3
let c = (Point { x: 21, y: 42 }).y; // optimizes to 42

传播到控制流中

常量传播 passes 还会处理传播到控制流中。例如

const Foo: Option<u8> = Some(12);

let x = match Foo {
   None => panic!("no value"),
   Some(v) => v,
};

变为

const Foo: Option<u8> = Some(12);

let x = 12;

这对于检查数学运算(在 debug 模式下默认启用)非常有用,它在每次操作后引入额外的控制流

let x = 2 + 4 * 6;

实际上在启用溢出检查后是这样运作的

let (_tmp0, overflowed) = CheckedMultiply(4, 6);
assert!(!overflowed, "attempt to multiply with overflow");

let (_tmp1, overflowed) = CheckedAdd(_tmp0, 2);
assert!(!overflowed, "attempt to add with overflow");

let x = _temp1;

这增加了相当多的控制流!常量传播在编译时对数学运算进行求值,将其简化为

let _tmp0 = 24;
assert!(!false, "attempt to multiply with overflow");

let _tmp1 = 26;
assert!(!false, "attempt to add with overflow");

let x = 26;

并进一步简化为仅

let x = 26;

编译器性能

正如你可能猜到的,减少 Rust 编译器处理的控制流数量对编译时间有积极影响。我们在 debug 和 release 模式下的各种测试用例中看到了 2-10% 的改进。尽管 LLVM 有它自己的常量传播 passes,但我们看到了改进,因为我们的 passes 在 MIR 仍然是泛型时进行操作。泛型函数的具体实例化越多,此优化的收益就越大。

我们怀疑 Rust 编译器生成的冗长 LLVM IR 在很大程度上导致了漫长的编译时间已有一段时间了。通过实现这样的优化,我们相信通过生成更好的 LLVM IR,有很大的潜力可以缩短编译时间。如果你想参与到 MIR 优化工作组中,可以到我们的 Zulip 频道 打个招呼!