“活跃变量分析补充这里使用的逐指令逆序计算、块内先用后定义及φ的边使用规则;死代码消除则明确何时可以先删除纯总的无用赋值。后者仍不能省略本页对保留写入、调用clobber和spill重写的约束。”
形式陈述
活跃不是“已经初始化”
在程序点 p,若存在一条从 p 出发的有限图路径,先读到 x、在此之前没有写 x,则 x 在 p 活跃。这是存在路径的性质:一条分支会使用就足够。变量已经赋过值但以后不再使用,可以不活跃;输入参数在入口就可能活跃,根本不需要先出现程序内赋值。
模型沿用过程内局部变量 CFG,所有真实后继都要列入。普通指令的 USE 是读变量集,DEF 是写变量集;返回和分支也读取自己的参数。调用、异常和内存若加入语言,必须提供对应 USE/DEF 和控制边。活跃性的正确性不能补救一张漏边的图。
从最后一行倒着算
考虑直线片段:
x := a+b
y := x
z := y+1
return z
返回前必须保存 z,所以集合为 {z}。越过 z:=y+1 向前看,旧 z 会被覆盖,已不需要;为算新 z 却要 y,故得到 {y}。继续越过 y:=x 得 {x},最后越过 x:=a+b 得 {a,b}。这是在每一步询问“要满足后面的需求,现在需要哪些输入”。
一般地,语句 s 前后的集合满足
x:=x+1 的 USE 与 DEF 都有 x。先从后续需求中删除旧 x,再并入右侧使用的 x,最终 x 仍在语句前活跃。若先并 USE 再删 DEF,就会错过右侧所需的旧值。
直觉
一行赋值完成以后,哪些旧值还不能丢?只要未来可能读取某个值,而且在读取之前没有重新定义它,就要继续保管。活跃变量分析从使用点向后寻找这些需要保留的值,因此信息的传播方向与程序的执行方向相反。
例子与边界
同一菱形图的完整边界
使用到达定义页的 E、T、F、J 程序:E 计算 x=a+b、y=x;T 与 F 各计算 u=a+b;J 依次计算 z=a+b、dead=z+1、r=u+y,返回 r。
| 块 | LiveIn | LiveOut |
|---|---|---|
| E | ||
| T | ||
| F | ||
| J | ∅ |
J 的逆序轨迹尤其值得手算:返回前是 {r};越过 r:=u+y 后为 {u,y};越过 dead:=z+1 后为 {z,u,y};越过 z:=a+b 后为 {a,b,u,y}。dead 自己从未活跃,但它的右侧 z 在原程序中仍是一次使用。因此普通活跃性不会自动假装某条语句已经删除;死代码消除需要在实际删除之后更新分析。
块级 USE 也不是块内所有 USE 的无序并集。E 中 y:=x 的 x 先在同块定义,所以 x 不属于 E 的“进入时即需要”的 USE。顺序扫描块,从 USE 中只加入当前尚未在 DEF 出现的读变量,再把当前写变量加入 DEF,就得到正确块摘要。
φ 的使用属于哪一条边
φ 节点 v:=phi(T:x,F:y) 在从 T 来时读 x,从 F 来时读 y。它不在 J 入口同时读取两个变量。令
这里计算的是保留全部 φ 指令时的普通活跃性,所有对应槽位都作为使用;若先删除死 φ,才可缩减这些使用。P 的 OUT 是各离开边 LiveEdge 的并集。J 普通指令向后扫描的起点不再把 φ 当普通指令重复处理。
例如 J 只返回 v,则 φ 后 LiveIn(J)={v}。T→J 上只需要 x,F→J 上只需要 y。把 {x,y} 都传给每个前驱虽是保守近似,却会制造不必要的同时活跃,进而增加寄存器干涉。本单元可运行检查器的主 IR 没有 φ;这条边敏感扩展在此给出公式与独立纸面验算,SCCP 页另有 φ 的专用实现。
推论与应用
工作队列怎样处理回边
这是单调数据流框架的后向 may 实例,合流保留任一后继的需求。后向方程是
正常出口 OUT 为空;返回值由返回指令 USE 加入,不要既把返回藏起来、又以空出口为由忘掉它。所有集合从空开始,把全部可达块入队。某块 IN 改变时通知它的前驱,因为前驱的未来需求变了。
循环中可能要多轮:若 B 增加一个使用,需求沿回边传到循环头,再沿前驱继续传递。值只增加,变量总数 V 有限,所以每个块最多增加 V 项。对每条有限见证路径归纳,最终集合不会漏掉其首次读取;反过来,每次 USE 生成和后继传播都能构造一条见证路径。因此得到的是图路径定义的精确最小不动点,而非具体可行路径的完美判定。
用位集和去重工作队列,N 个块、M 条边、最大出度
活跃事实如何使用
若 x 不在一条赋值之后活跃,赋值的新值不会在任何图路径上被使用。但能否删掉该指令,还要看右侧是否有副作用、陷阱或发散;x:=read_input() 即使 x 死了仍会消耗输入。活跃性回答“值需不需要”,不回答“这次计算能不能不发生”。
迁移任务:删掉菱形图的 dead:=z+1 后重新计算。J.1 的 z 没有使用了,但普通分析仍会把计算 z 时的 a、b 列入 J 的入口;再删掉 z:=a+b,J 入口才变为 {u,y}。另将返回改为 return z,则 r 死了,z 成为真正的返回需求。请分别写出两种变更的逆序轨迹,不只改集合表的最后一格。
参考资料
- Andrew W. Appel, Modern Compiler Implementation in ML, Cambridge University Press, 1998,Chapter 10 “Liveness Analysis”。定义、反向方程与寄存器用途的标准教材。
- Cornell CS 4120,Live variable analysis。课程讲义用于核对有限路径上的先使用、后重定义口径;本页表格与迁移轨迹独立计算。
- Fabrice Rastello and Florent Bouchez Tichadou, eds., SSA-based Compiler Design, Springer, 2022,Chapter 9,pp. 107–122 的 SSA 活跃性与寄存器分配。φ 参数按前驱边处理,不按普通块内调用处理。