12.3 死代码消除
死代码消除(Dead Code Elimination,DCE)会删除不可能影响任何可观察行为的计算。一个值仅仅“被算出来”并不代表有用;它最终必须参与返回、可见 store、带副作用调用、控制决策、异常、volatile 操作或其他必须保留的效果。
%a = load %input
%b = mul %a, 2
%dead = call @pure_expensive()
%unused = add %dead, 9
ret %b若 pure_expensive 确实纯净且不会 trap,%dead 与 %unused 可以删除;%a、%b 因为流向 return 而保留。
从根沿 def-use 反向标记
在 SSA 中,DCE 很像标记-清除垃圾回收:
1. 标记本身可观察或必须存在的根指令;
2. 沿每条已标记指令的操作数找到其定义;
3. 重复直到不再出现新定义;
4. 删除所有未标记指令。
常见根包括 ret、外部可见 store、branch、带副作用 call、异常可观察的 throwing 操作、volatile/atomic 操作,以及语言运行时必需的 bookkeeping。调试构建可能额外保留供变量检查的值,但普通 debug metadata 不应改变 release 语义。
“结果没人用”不等于 dead
call print(x) 即使没有返回值被使用,也必须保留。边界检查若被删除,会让非法内存访问替代语言规定的异常;整数除法可能因除零而可观察;volatile load 可能读取硬件设备。
因此 pass 依赖可靠的 effect 与 trap 信息。只要无法证明安全,保守编译器就必须保留指令。错误的 effect 标注会让 DCE 从清理工具变成错误编译来源。
不可达代码是图问题
不可达块消除从函数 entry 出发,沿所有可行 CFG 边标记块。没有被标记的块、其中指令和出边都能删除。常量分支常制造这种机会:
entry:
br true, fast, slow
fast:
ret 1
slow: ; 不可达
call @expensive()
ret 2异常边、间接跳转、协程恢复点与语言清理边都是真实控制流。pretty printer 中看似孤立的块,可能通过 exceptional successor 到达。反过来,return 已经关闭当前块,文本上排在它后面的代码不能获得虚假的 fallthrough 边。
DCE 会继续改变周围图结构
删除一条指令,可能让它的操作数定义也变得无人使用,所以 DCE 需要递归或工作列表。删除不可达前驱后,phi 可能从多个输入缩减为一个;删除常量分支又可能暴露新死块。因此优化器常交替运行 SCCP、CFG 简化、phi 简化与 DCE,直到没有新变化。
无限循环和不终止行为需要特别谨慎。在某些语言契约中,用循环后的代码替换一个确定不终止的循环会改变行为,即使循环没有普通输出。若 out-of-memory 被语言视为可观察,删除 allocation 也可能改变行为。“可观察”的定义来自语言与优化契约,而不是直觉。
可靠测试应同时记录返回值与事件轨迹,覆盖纯净无用计算、无用但有副作用的调用、trap、volatile 读取、常量分支、异常边、删前驱后的 phi 和嵌套不可达区。删除完成后必须再次验证修复后的 CFG。