12.1 常量折叠与常量传播
优化会改变程序的实现方式,但必须保持可观察行为。最直观的一类优化,是删除那些结果在编译期已经确定的工作。常量折叠(constant folding) 在编译期直接计算一个运算;常量传播(constant propagation) 则把已经证明为常量的变量或 SSA 值替换到它的使用点。
二者会互相创造机会:传播让运算数变成常量,折叠把运算变成新常量,新结果又可能让分支条件固定,从而暴露不可达代码。
%x = const 4
%y = mul %x, 2 ; 传播后是 mul 4, 2
%c = icmp.eq %y, 8 ; 折叠后是 true
br %c, yes, no ; 变成 br yes优化后不仅文本更短,还真正删除了一次乘法、一次比较和一次条件跳转。后续死代码消除可以继续删掉不再可达的 no 块。
事实如何穿过 CFG
在单个基本块内,维护 {x → 4, y → 8} 这样的环境即可。但在控制流图(CFG)中,一个值可能从多个前驱到达,因此常量传播通常为每个 SSA 值使用一个小型格(lattice):
UNDEF表示尚无可执行路径提供该值;
CONST(c)表示目前所有可执行路径都提供同一个常量c;
NAC或 overdefined 表示无法证明它是某一个常量。
在汇合点,CONST(4) 与 CONST(4) 的 join 仍是 CONST(4);CONST(4) 与 CONST(9) 则上升为 NAC。UNDEF 绝不表示数值 0,它只是“分析尚未从可执行路径学到值”。
稀疏条件常量传播
稀疏条件常量传播(Sparse Conditional Constant Propagation,SCCP)同时追踪值事实和可执行 CFG 边。如果分支条件成为常量,SCCP 只标记被选择的边可执行;不可能到达的前驱就不会污染 phi 节点。
entry:
%flag = const true
br %flag, left, right
left:
br join(5)
right:
br join(runtime_value)
join(%x):
%y = add %x, 3不区分路径的分析会把 5 与 runtime_value 合并,得到 %x = NAC。SCCP 先证明 right 边不可达,因此 %x = 5,进而 %y = 8。这种“可达性帮助值、值又帮助可达性”的反馈,使 SCCP 强于简单的文本替换。
折叠必须保持源语义
编译器只有在能复现运行时语义时,才能提前计算。常见危险包括:
- checked 整数溢出与 wrapping 算术的差异;
- 整数除零、非法移位位数;
- 浮点
NaN、无穷、带符号零和舍入模式;
- 会 trap、抛异常、分配或改变状态的操作;
- 指针宽度等目标相关事实。
对有符号 6 位 checked 整数,31 + 1 应触发 trap;若折叠成 -32,就擅自换成了 wrapping 语义。对 IEEE 浮点,0.0 / 0.0 可以精确折叠为 NaN,但通常不能把 x * 0.0 简化为 0.0,因为 x 可能是 NaN 或负零。fast-math 标志可以有意放宽部分规则,但它属于明确契约,不能全局默认。
实现一个可终止的 pass
实际 pass 会维护“事实发生变化的值或指令”工作列表。某个操作数成为常量时,只重新访问它的 users;输入事实没变,就无需重复计算。有限函数中,格值只能单调地从 UNDEF 经过 CONST 走向 NAC,因此最终会到达不动点。
重写完成后应运行 IR verifier,再交给死代码消除。常量传播经常留下无人使用的定义和未被选择的块;把分析、重写与清理拆开,能让每个 pass 的正确性更容易理解和测试。