我很高兴地宣布,中间层 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 频道 并打个招呼!