电子说
在介绍算法之前,我们回顾下基本概念:
a=b+c
就可以看作四元式(+,a,b,c)。n: c <- c+b
来说 use[n]={c,b},def[n]={c}。live-in
集合中。需要注意的是,我们后续的算法会作用在最普通的四元式上,而不是SSA。在介绍寄存器分配算法之前,我们需要活跃变量分析来构建干涉图。
简单来说,就是计算每个点上有哪些变量被使用。
算法描述如下[1]:
input: CFG = (N, E, Entry, Exit)
begin
// init
for each basic block B in CFG
in[B] = ∅
// iterate
do{
for each basic block B other than Exit{
out[B] = ∪(in[s]),for all successors s of B
in[B] = use[B]∪(out[B]-def[B])
}
}until all in[] do't change
活跃变量分析还有孪生兄弟叫Reaching Definitions,不过实现功能类似,不再赘述。
举个例子:对图1的代码进行活跃变量分析
图1[2]
可以得到每个点的活跃变量如图2所示:
图2
过程呢?限于篇幅,仅仅计算第一轮指令1的结果,剩余部分读者可自行计算。
步骤 | 下标 | out | in |
---|---|---|---|
第一次迭代 | 1 | {} | {b,c} |
... | ... | ... | ... |
可画出RIG如图3:
图3
经过上文的活跃变量分析,我们得到了干涉图,下一步对其进行上色。
但是图着色是一个NP问题,我们会采用启发式算法对干涉图进行着色。基本思路是:
算法描述[3]:
input: RIG, k
// init
stack = {}
// iterate
while RIG != {} {
t := pick a node with fewer than k neighbors from RIG // 这里RIG可以先按度数排序节点再返回
stack.push(t)
RIG.remove(t)
}
// coloring
while stack != {} {
t := stack.pop()
t.color = a color different from t's assigned colored neighbors
}
对于例子1,假设有4个寄存器r1、r2、r3、r4可供分配。
步骤 | stack | RIG |
---|---|---|
0 | {} | |
1 | {a} | |
2 | {d,a} | |
所以图3中的RIG是4-着色
的。但如果只有三种颜色可用,怎么办呢?
没关系,我们还有大容量的内存,虽然速度慢了那么一点点。着色失败就把变量放在内存里,用的时候再取出来。
依然是上例,但是k=3,只有三个颜色。
如果f的邻居是2-着色
的就好了,但不是。那就只能选一个变量存入内存了。这里我们选择将变量f
溢出至内存。溢出后的IR和RIG如图:
图4 溢出后的IR
图5 溢出后的RIG
所以,溢出其实是分割了变量的生命周期以降低被溢出节点的邻居数量。溢出后的着色图如图6:
图6 着色后的图5
这里溢出变量f
并不是明智的选择,关于如何优化溢出变量读者可自行查阅资料。
至此,图着色算法基本介绍完毕。不过,如果代码中的复制指令,应该怎么处理呢?
寄存器分配之前会有Copy Propagation和Dead Code Elimination优化掉部分复制指令,但是两者并不是全能的。
比如:代码段1中,我们可以合并Y和X。但是代码段2中Copy Propagation就无能为力了,因为分支会导致不同的Y值。
// 代码段1
X = ...
A = 10
Y = X
Z = Y + A
return Z
// 代码段2
X= A + B
Y = C
if (...) {Y = X}
Z = Y + 4
所以,寄存器分配算法也需要对复制指令进行处理。如何处理?给复制指令的源和目标分配同一寄存器。
那么如何在RIG中表示呢?如果把复制指令的源和目标看作 RIG中相同的节点 ,自然会分配同一寄存器。
Y=X
指令中的X和Y是可合并的。那么如何计算局部的度数?介绍三种算法:
(|X|+|Y|),很保守的算法但是可能会错过一些场景
比如k=2时,图7应用简单算法是没办法合并的
图7[3]
但明显图7可以合并成图8:
图8[3]
到此,图着色算法介绍完毕。
全部0条评论
快来发表一下你的评论吧 !