Liveness Analysis⚓︎
约 3212 个字 27 行代码 预计阅读时间 16 分钟
在 IR 到机器码的转换过程中,问题为 IR 会用到无限数量的临时变量,而机器只能用有限数量的寄存器。
两个临时变量 a 和 b 如果在不同时间上被使用,则可以分配到同一个寄存器中。当临时变量太多无法全部放入寄存器时,多余的临时变量可以存放在内存中。
本讲将会介绍一种判断两个变量是否在不同时间上被使用的方法——活跃性分析(liveness analysis):确定每个变量的活跃性,并共享寄存器。若一个变量保存的值可能在将来被用到,则该变量是活跃的(live)。
简单来说,活跃性分析就是在判断变量 x 是否在语句 n 后被使用。这个过程可分为两部分:
-
n之后会执行什么语句 -> 绘制控制流图- 节点:一条语句
- 边:控制流;若语句 m 之后会执行语句 n,则有边 m -> n
-
这些语句是否会用到
x-> 分析语句
一般我们采用反向分析(backward analysis),即从未来到过去。
Solution of Dataflow Equations⚓︎
变量的活跃性沿着控制流图的边“流动”,因此活跃性分析是数据流问题的一个例子。
流图相关术语:流图节点 n 具有
- 出边:指向后继节点
- 入边:来自前驱节点
pred[n]:前驱节点集合succ[n]:后继节点集合
Uses and Defs⚓︎
- 变量或临时变量的赋值定义(defines) 了该变量
- 在赋值语句的右侧(或其他表达式中)出现一个变量,意味着使用(uses) 了该变量
- 变量的定义:定义它的图节点集合
- 图节点的定义:它定义的变量集合
- 变量或图节点的使用类似
Calculation of Liveness⚓︎
活跃性(liveness):如果从一条边到该变量的某个使用之间存在一条有向路径,且该路径不经过任何定义,则该变量在该边边上是活跃的。
- 该变量随后将被使用
- 该变量在使用前不会被重新定义
- 参与变量(live-in):在某个节点的任何入边上活跃的变量
- 传出变量(live-out):在某个节点的任何出边上活跃的变量
- 符号说明:
in[n]:节点n的 live-in 集合out[n]:节点n的 live-out 集合
下面介绍如何通过 use 和 def 计算活跃性信息(in[n] 和 out[n]
- 如果对于任意
m\(\in\)succ[n],有a\(\in\)in[m],则a\(\in\)out[n] - 如果
a\(\in\)use[n],则a\(\in\)in[n],即如果语句n使用了变量a,则a在n的入口处是活跃的 - 如果
a\(\in\)out[n]且a\(\notin\)def[n],那么a\(\in\)in[n]
活跃性分析的数据流方程:
下面给出通过迭代求得上述方程解的算法:
通过迭代求解数据流方程时,计算顺序应遵循流。活跃性沿控制流箭头向后传播,且从出(out)到入(in
计算的变体:
- 基本块:只有一个前驱和一个后继节点的流图比较简单,因此将它们与其前驱和后继合并,得到一个节点更少的图,其中每个节点代表一个基本块
- 一次处理一个变量:当需要某个变量的信息时,一次只计算该变量的数据流,因为许多临时变量具有非常短的活跃范围(因此搜索会快速终止)
Representations of Sets⚓︎
表示 in[n] 和 out[n] 的方法:
-
位数组(适用于密集集合)
- 假设程序中有 N 个变量,每个字有 K 位
- 每个集合占用 N 位
- 两个集合的并集:将对应位置的位进行或运算
- 一次集合并集运算需要 N/K 次操作
-
排序列表(适用于稀疏集合)
- 按任何全序键(如变量名)排序
- 并集操作:合并列表
- 当集合稀疏时(平均元素数远小于 N/K
) ,排序列表表示更快
Time Complexity⚓︎
迭代数据流分析的速度分析
- 一个大小为 N 的程序:最多 N 个节点,最多 N 个变量
- 每个集合并集操作需要 \(O(N)\) 时间
for循环:每个节点计算恒定数量的集合操作。\(O(N)\) 个节点 => \(O(N^2)\) 时间repeat循环:所有in和out集合的大小总和为 \(2N^2\),这是repeat循环能迭代的最大次数,因为in和out集合是单调的,不能无限增长- 最坏情况运行时间:\(O(N^4)\)
- 但在实践中时间介于 \(O(N)\) 和 \(O(N^2)\) 之间(如果计算顺序合理)
Least Fixed Points⚓︎
在活跃性分析中,如果认为变量同时活跃,我们会确保将它们的不同值保存在不同寄存器中。这便是活跃性的保守近似(conservative approximation):可能会错误地认为某个变量是活跃的,但绝不会错误地认为它是非活跃的。后果是:编译后的代码可能会使用比实际需要更多的寄存器,但能保证计算出正确结果。
用于编译器优化的数据流方程应这样设置:任何解都向优化器提供保守信息;不精确的信息可能生成次优但绝不会错误的程序。
定理:活跃性分析的数据流方程有不止一个解。假设有解 X, Y
- 若 X 是方程的解,且方程的所有解都包含解 X,则称 X 为方程的最小解(最小不动点
) 。 - 方程存在一个最小不动点,且上述迭代算法总是计算出该最小不动点
Static and Dynamic Liveness⚓︎
- 这些方程不知道条件分支会走向哪一边
- “更智能”的方程会允许将
a和c分配至同一寄存器
没有编译器能够完全理解每个程序中的所有控制流是如何工作的。这可以通过停机问题来证明
定理:停机问题
不存在这样的程序 H(P, X),它接受任意程序 P 和输入 X 作为输入,并在 P(X) 停机(没有无限循环)时返回真,否则返回假。
推论
不存在这样的程序 H'(P, L),对于任意程序 P 和 P 中的标签 L,能够判断在 P 的执行过程中是否曾到达标签 L。
- 证明:通过说明如果
H'存在,那么H也存在 - 令
L为程序的末尾,停机 => 跳转至L
这个定理并不意味着我们永远无法判断某个标签是否可达,只是说明不存在一个总能给出答案的通用算法。我们可以通过一些特殊情况下的算法来改进活跃性分析。但事实上,没有编译器能真正判断一个变量的值是否确实被需要,即该变量是否真正 " 活跃 "。
我们只能采用保守近似的处理方法:假设每个条件分支都可能双向执行,因此我们得到动态条件及其静态近似。
- 动态活跃性:如果程序从节点
n开始执行,并在不经过变量a任何定义的情况下到达对a的使用,则变量a在节点n处是动态活跃的 - 静态活跃性:如果存在一条从节点
n出发的控制流路径,该路径在不经过变量a定义的情况下到达对a的某个使用,则变量a在节点n处是静态活跃的
如果变量 a 是动态活跃的,那么它也是静态活跃的。
Interference Graphs⚓︎
活跃性分析最重要的应用之一是寄存器分配。假设有一组临时变量a , b , c , ... 以及寄存器r1 , ..., rk。
我们称阻止 a 和 b 分配到同一寄存器的条件为干扰(interference)。有两种干扰类型:
- 重叠的活跃范围
- 当
a必须由一条无法寻址寄存器r1的指令生成时,a和r1相互干扰
干扰的表达:
- 矩阵,其中元素 x 标记干扰
- 无向图
- 节点:变量
- 边:连接相互干扰的变量
- 通过对节点上色来分配寄存器
不要在 MOVE 操作的源和目标之间制造人为干扰。考虑:
- 通常会创建一个干扰边
(s, t) - 但我们不需要为
s和t分别设置寄存器,因为它们包含相同的值。 - 解决方案:在这种情况下,不添加干扰边
(s, t) - 如果在
s仍然活跃的情况下,稍后对t进行(非移动)定义,这将创建干扰边(t, s)
因此,为每个新定义添加干扰边的方式是:
- 在任意定义变量
a的非移动指令n处,其中out[n] = (b1, ..., bj),添加干扰边(a, b1), ...,(a, bj) - 在移动指令
a := c处,其中{b1, ..., bj}是活跃输出集,对于任何与c不同的bi,添加干扰边(a, b1), ...,(a, bj)
Liveness in the Tiger Compiler⚓︎
Tiger 编译器的流程分析分为两个阶段:
- 分析 Assem 程序的控制流,生成控制流图
- 分析控制流图中变量的活跃性,生成干扰图
详细实现请参考教材。
- 在算法中使用图时,我们希望每个节点代表某种内容(例如程序中的一条指令)
- 为了实现节点到内容的映射,我们使用
G_table(一个哈希表) - 以下惯用法将信息
x与节点n关联到映射mytable中:G_enter(mytable, n, x) - 我们也可以将
x直接放入n中:n = G_Node(g, x),即创建一个包含信息x的新节点n G_nodeInfo(n):检索x
FlowGraph 包用于管理控制流图:
- 节点:一条指令(或基本块)
- 边
(m, n):指令m后跟随指令n(通过跳转或顺序执行)
/* flowgraph.h */
Temp_tempList FG_def(G_node n);
Temp_tempList FG_use(G_node n);
bool FG_isMove(G_node n);
G_graph FG_AssemFlowGraph(AS_instrList il);
每个节点具有以下属性:
FG_def():在此节点定义哪些临时变量(指令的目的寄存器)FG_use():在此节点使用哪些临时变量(指令的源寄存器)FG_isMove():此指令是否为MOVE指令,即在定义和使用相相同时可以删除的指令
与节点相关联的信息:
- 流图:将一些使用和定义信息关联到每个节点
- 活跃性分析算法:在每个节点处记录入活跃和出活跃信息
- 对流图的其他分析:关于每个节点的其他类型信息
我们可能不希望为每次新的分析修改数据结构(这是一个广泛使用的接口
/* liveness.h */
typedef struct Live_moveList_ *Live_moveList;
struct Live_moveList_ {G_node src, dst; Live_moveList tail;};
Live_moveList Live_MoveList(G_node src, G_node dst, Live_moveList tail);
struct Live_graph { G_graph graph; Live_moveList moves; };
Temp_temp Live_gtemp(G_node n);
struct Live_graph Live_liveness(G_graph flow);
Liveness 模块:
- 输入:流图
- 输出:干扰图 + 节点对列表(表示
MOVE指令) ,这些节点对应分配相同的寄存器 Live_gtemp指示n所代表的临时变量
在 Liveness 模块的实现中,维护一个数据结构是有用的,它可以记住每个流图节点出口的活动:
static void enterLiveMap(G_table t, G_node flowNode, Temp_tempList temps) {
G_enter(t, flowNode, temps);
}
static Temp_tempList lookupLiveMap(G_table t, G_node flownode) {
return (Temp_tempList)G_look(t, flownode);
}
有了完整的 liveMap,我们就可以使用刚才描述的方法构建一个干扰图。
如果一个新定义的临时变量在定义之后立即不活跃
- 变量被定义但从未使用 -> 无需将其放入寄存器。
- 定义指令将要执行(也许是为了其他副作用)-> 它会被写入某个寄存器,该寄存器最好不包含任何其他活跃变量。
因此,零长度的活跃范围确实会与任何与其重叠的活跃范围发生冲突。
评论区










