12.5 活跃性分析与循环优化
如果某个程序点之后的执行可能在该值被覆盖前读取它,那么这个值在该点是活跃的(live)。活跃性是向后的 may-analysis:未来决定当前值是否有用,只要任意后继路径可能使用该值,它就活跃。
对每个基本块:
OUT[B] = 所有后继 S 的 IN[S] 并集
IN[B] = USE[B] ∪ (OUT[B] − DEF[B])USE[B] 包含块内在定义前就读取的值;DEF[B] 包含块定义的值。定义会 kill 进入块的旧版本,使用则让值变为 live。
从块事实得到指令活跃区间
解出块级活跃性后,再从块尾向前扫描指令。初始集合是块的 OUT。对 d = op(a, b),先从 live 集合移除 d,再加入 a、b;得到的集合就是该指令之前的 live-in。
1: a = load input
2: b = add a, 1
3: c = mul b, 2
4: print a
5: return c由于第 4 行再次使用 a,它的活跃区间会与 b、c 重叠。若删掉 print a,a 在第 2 行后就死亡,寄存器压力随之下降。两个活跃区间重叠的值会互相干涉,不能在该时刻占用同一寄存器。
活跃性服务于死 store 消除、寄存器分配、stack map 和精确垃圾回收。函数调用还会依据 calling convention 破坏部分物理寄存器;跨 call 活跃的值可能需要 callee-saved 寄存器或 spill。
为什么循环值得特殊优化
循环会重复工作,因此 body 中省下一条指令,动态上可能省下数百万次。第 10 章的自然循环检测与支配关系能识别 header、回边、body、preheader 与 exits,这些结构事实支持多种变换。
循环不变量外提
循环不变量代码移动(Loop-Invariant Code Motion,LICM)只有在以下条件成立时,才能把计算移到 preheader:
- 操作数是常量、在循环外定义,或由其他不变量产生;
- 进入循环时提前执行该操作是安全的;
- 移动不改变内存、异常与顺序行为;
- 该定义支配所有依赖它的循环内使用。
从 limit_ptr load 的地址不变,并不代表 load 结果不变。循环中的 store 或未知 call 可能修改它,必须由 alias 与 effect 分析证明稳定。
归纳变量与强度削弱
在 addr = base + i * 8 中,若 i 每轮加一,乘法可以变成循环携带地址,并用 addr_next = addr + 8 更新。这种强度削弱(strength reduction)用加法替代重复乘法,但仍要保持溢出、指针语义,并在每条回边正确更新。
循环展开
循环展开(unrolling)复制 body,让一次分支处理多轮迭代。它减少循环控制开销,并可能暴露向量化机会;代价是代码体积增大。当 trip count 不能被展开因子整除时,还需要 remainder path。
变换会与寄存器压力互相作用
循环优化不是互不相关的开关。LICM 可能让一个值跨整个循环保持活跃;unrolling 会制造更多同时存活的临时值;vectorization 使用更宽寄存器,还可能需要对齐检查。即使动态指令减少,也可能因 spill 或指令缓存膨胀而变慢。
因此循环优化器先证明合法,再结合 trip count、目标成本、profile 与寄存器压力判断收益。测试必须比较 0、1、少量和大量迭代,覆盖每个 exit 与 continue,并包含 alias、trap、溢出边界和不能整除的 remainder。