支配树这个结构在后端优化中有很重要的作用,尤其是SSA格式的程序中,高效的支配树生成尤其重要。
我们说对于一个图G=(V,E,entry),
计算支配关系主要有数据流法, Lengauer-Tarjan法和消去法,其中前两种是主流算法。
第一个算法是最简单的,通过计算LCA得到idom从而构造出支配树,其实也就是常规的数据流算法的基本方法。对于一个有向无环图,如果有一个非root的子节点n,其前继为n1…nk,那么节点m dom n当且仅当m dom (n的所有前继n1…nk),也就是说m是n1…nk在支配树上的公共祖先,而idom(n)也就是idom(n1)…idom(nk)的最近公共祖先了。
idom(n)=LCA(idom(pred(n))) (G是可规约流图)
Ramalingam在论文中给出的伪代码[2]:
function ConstructDominatorTree(G : Acyclic Graph)
DomTree := an empty tree;
add entry(G) as the root of DomTree ;
for every vertex u in V (G) − { entry(G) } in topological sort order do
let z denote the least-common-ancestor, in DomTree, of u’s predecessors;
add u as a child of z in DomTree ;
end for
return DomTree;
这种算法在我之前分析libpcap源码时已经出现了,在函数find_dom中,其计算LCA用的是位向量的交集运算。
而对于一个带循环的可规约流图,如果去除其中的所有回边,整个流图就退化成了一个有向无环图。根据循环的性质,在可规约流图中循环头是支配其循环体中所有结点的,也就是说,回边的存在并不会改变支配关系。所以对于可规约流图,可以简单的DFS遍历去除掉回边后再使用上面的算法来计算支配关系。
无论是DAG还是带循环的可规约流图,都只需要计算一次就可以得到支配树,因为在计算子节点时,父节点的数据流值已经被计算完毕了。而对于不可规约流图,由于在遍历顺序问题,在计算一些节点的LCA时,其前继的LCA可能还没计算:
for all nodes, n
DOM[n] ← {1 …N}
Changed ← true
while (Changed)
Changed ← false
for all nodes, n, in reverse postorder
new_set ← ( ∏ \prod ∏p ∈ preds(n)DOM [p]) ∐ \coprod ∐{n}
if (new_set != DOM [n])
DOM[n] ← new_set
Changed ← true
Cooper发现,在计算交集时,利用节点在支配树中的顺序关系,可以用一个双指针算法来加速交集的计算。如果所有节点按后序序列编号,则序号越高的节点在支配树中也越高,也就是说,支配树中一个节点的所有孩子节点是这个节点在后序序列中的前继。算法伪代码[1]:
for all nodes, b /* initialize the dominators array */
doms[b] ← Undefined
doms[start node] ← start_node
Changed ← true
while (Changed)
Changed ← false
for all nodes, b, in reverse postorder (except start node)
new_idom ← first (processed) predecessor of b /* (pick one) */
for all other predecessors, p, of b
if doms[p] != Undefined /* i.e., if doms[p] already calculated */
new_idom ← intersect(p, new_idom)
if doms[b] != new_idom
doms[b] ← new_idom
Changed ← true
function intersect(b1, b2) returns node
finger1 ← b1
finger2 ← b2
while (finger1 != finger2)
while (finger1 < finger2)
finger1 = doms[finger1]
while (finger2 < finger1)
finger2 = doms[finger2]
return finger1
Cooper的论文中也给出了算法的时间复杂度。迭代的最大次数是d(G)+3,也就是和结点数量有关系。而具体的迭代次数则和图的循环连通度有关(即最大回边数量)。通过实验数据,这种快速算法在规模不是很庞大的图中性能往往比LT算法更好。(现实中的流图,往往节点数量不多并且循环连通度比较低。)
C++实现算法。一般情况下,我们希望基本块结构中只保存和基本块相关的信息,而对于全局信息,则通过和基本块建立映射来保存:
void Dominator::calcFrontDominator(CFGNode* node) {
bool changed = true;
// 获得节点逆后序
DFSOrder& doPass = getPM().getPass<DFSOrder>(string("DFSOrderPass"));
DFSOrder::BBOrderList& rpo_sequence = doPass.getDFSReversePostOrder();
// 所有节点的idom首先被初始化为空
vector<unsigned> idoms(rpo_sequence.size(), (unsigned)-1);
idoms[0] = 0;
unsigned index = 0;
// 给所有节点建立逆后序序号映射
map<BasicBlock*, unsigned> block_map;
for (auto i : rpo_sequence) {
block_map[i] = index++;
}
while (changed) {
changed = false;
for (auto i = 1;i < rpo_sequence.size();i++) {
BasicBlock* block = rpo_sequence[i];
auto prev_iter = block->prev_begin();
BasicBlock* pred = *prev_iter;
// 本轮的idom首先初始化成第一个前继的idom
unsigned new_idom = block_map[pred];
// 找到第一个已经计算过idom的前继
while (idoms[new_idom] == -1) {
++prev_iter;
assert(prev_iter != block->prev_end());
pred = *prev_iter;
new_idom = block_map[pred];
}
// 求出所有前继的LCA
for (++prev_iter;
prev_iter != block->prev_end();
++prev_iter) {
unsigned block_id = block_map[*prev_iter];
if (idoms[block_id] != (unsigned)-1)
new_idom = intersect(new_idom, block_id, idoms);
}
// 更新idom
if (idoms[i] != new_idom) {
idoms[i] = new_idom;
changed = true;
}
}
}
// 构造dom tree
for (size_t i = 0;i < idoms.size();i++) {
front_dom_tree[rpo_sequence[i]] = rpo_sequence[idoms[i]];
}
}
// 双指针计算交集
unsigned intersect(unsigned B1, unsigned B2,
vector<unsigned>& idoms) {
while (B1 != B2) {
while (B1 > B2) {
B1 = idoms[B1];
}
while (B2 > B1) {
B2 = idoms[B2];
}
}
return B1;
}
参考资料:
1,《A Simple, Fast Dominance Algorithm》
2,《On Loops, Dominators, and Dominance Frontiers》