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

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

我很高兴地宣布,中间层 IR (MIR) 常量传播通道已在 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

传播到控制流中

常量传播通道还处理传播到控制流中。例如

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 有自己的常量传播通道,但我们仍然看到改进,因为我们的通道在 MIR 仍然是泛型时对其进行操作。泛型函数的实例化越多,此优化带来的好处就越大。

我们早就怀疑 Rust 编译器生成的冗长 LLVM IR 会大大延长编译时间。通过实现这样的优化,我们相信通过生成更好的 LLVM IR 有很大的潜力来缩短编译时间。如果您想参与 MIR 优化工作组,请访问我们的 Zulip 频道 并打个招呼!