C2 - Register Allocation


0. 参考

主要参考:
http://cr.openjdk.java.net/~thartmann/offsite_2018/register_allocator.pdf
?

https://wiki.openjdk.java.net/display/HotSpot/The+C2+Register+Allocator
?

https://wiki.openjdk.java.net/display/HotSpot/C2+Register+Allocator+Notes
?

https://ssw.jku.at/Research/Papers/Wimmer04Master/Wimmer04Master.pdf
?

1. 图染色寄存器分配算法

一点翻译,来自C.W.的研究生论文,附带一点我的私货。

目前最常见的方法是将寄存器分配问题视作图染色问题。第一个实现G.J.Chaitin做出的,接着P.Briggs对它做了些提升,C2的寄存器分配(以下可能简称RA)几乎遵照Briggs-Chaitin style实现。

1.1 构造干扰图

图染色的目的是给每个虚拟寄存器(virtual register)赋予一个物理寄存器(physical register)。每个虚拟寄存器都有一个live range,始于值第一次定义,终于值最后一次使用。live range不必是连续的,它允许出现空洞,因为可能存在重复定义和使用。
?

N个虚拟寄存器必须映射到R个物理寄存器上,N通常比R大得多。物理寄存器就是“颜色”,要“染”的东西是寄存器干扰图,这个图中的节点就是虚拟寄存器。如果两个节点有一条边连接,那么这两个节点(虚拟寄存器)必须使用不同的颜色染(物理寄存器),以保障同一时刻一个物理寄存器只对应一个虚拟寄存器。
?

Fig2.1有个简单的例子,包括五个虚拟寄存器v1,v2,v3,v4,v5,假设只有两个物理寄存器r1,r2。指令有七条,右边是它们各自的live range。v1从(1)开始,(7)结束,因为它在(1)定义在(7)最后一次使用。因为(5)重复定义,所以(3)-(5)出现了空洞。

v1在(1)定义,(3)使用。因为v2在(2)处定义,它干扰了v1 live range,所以v1 v2两个节点连线。同理,v1在(5)处干扰v4,所以v1 v4连线。v5干扰v1,v5 v1连线。v4干扰v3,v3 v4连线。如此继续,最终形成Fig2.2的寄存器干扰图。

1.2 修剪干扰图

现在寄存器干扰图必须被R个颜色染色,并且连线的两个节点必须染不一样的颜色,让它们拿到不同的物理寄存器。穷尽出染色的所有方案是不可行的,因为这是个已知的NP完全问题。我们用迭代的方案,来简化图。如果找不到染色方案就说明有些虚拟寄存器必须spill到内存。需要一个成本函数来找出哪些值spill到内存对性能的影响最小。
?

要想对干扰图染色,可以迭代式修剪图。每次迭代移除选择的节点和所有关联边,然后放到栈上。选择哪个节点取决于两条规则:
1) 如果图包含一个节点,它的度小于R(即节点关联边少于R),那么当且仅当没有这个节点的剩下的图是可染色的,该节点才是可染色的。如果剩下的图最后成功染色,那么总能找到一个染色给当前节点染色,因为当前节点关联边小于度R,既然剩下染成功了,那么它们不可能拿到所有颜色,至少有一个可用颜色。为啥可以这样呢?之前也说过了,只要关联节点不染同一个颜色,当前节点就是可染色的(colorable)。
2)用成本函数选择最不重要的节点,从图中移除并让到栈上。即使所有节点的度都高于R,也可以通过这条规则继续染色。
?

先用第一条规则,移除度为1的v5放到栈上。然后剩下所有节点度都等于2,假设成本函数选了v2,移除v2放栈上。接着用第一条规则选v3,移除v3放栈上。在用第一条规选v1,移除放栈上。最后选v4,移除放栈上。

1.3 重建干扰图

这一步我们从栈上pop节点,根据之前的边重建干扰图,重建的时候会给节点染上色,只要关联节点没有用到的颜色都可以选。如果这个时候找不到可用颜色,那么这个虚拟寄存器需要标记为spilling。当所有节点处理完毕,并且假设没有spilling节点,那么该图已经完全被染色,每个虚拟寄存器都可以替换为颜色对应的物理寄存器。如果需要spilling,将插入对应的spilling代码到IR中,因为这个spilling代码本身也需要寄存器,所有需要再来一次算法,即构造新的干扰图、修剪、重构,直到最后没有spilling,图被完全染色。
?

重建过程如Fig2.4所示:pop v4,染r1。pop v1,染r2。pop v3,染r2。pop v2,染r1。pop v5染r1。此时图完全染色,算法结束。

用染色结果代替之前的IR,如图Fig2.5所示:

2. RA实现概览

由于Niclas Adlertz的slide已经很直白了,我不一定做的比他好,所以以他的slide为主,附上一些解释,以及补充。

回顾之前的算法,有几个关键点:

  • 干扰图的节点表示变量(虚拟寄存器)
  • 干扰图的颜色表示寄存器(物理寄存器)
  • 关联节点不能同一时刻染同一颜色(同一时刻这些变量不能同时存活)

Chaitin算法一些核心流程如下:

  • Build IFG: 使用live range数据构造干扰图(Interference graph,IFG)
  • Simplify: 从IFG移除节点,放到栈上
    • 规则1: degree
    • 规则2:degree>=R
  • Spill:如果必须Spill,插入spill代码,然后算法重来
  • Select:从栈pop所有节点,重建IFG,期间给节点染色,要求选的颜色不能是关联节点已经选的

Chaitin完整流程如下:

注意上面说的是Chaitin算法,Briggs还做了一些改进,而C2用的是Briggs-Chaitin算法。改进如下:

  • 即使Simplify不能用第一条规则给所有节点染色,也尝试在Select节点染色。
  • Chaitin能染色的该算法能,Chaitin不能染色的该算法部分能。

3. C2 RA实现

3.0 Overview

3.1 Renumber

3.1.1 收集LRG

这一步我理解为每个指令生成LRG(Live RanGe)数据结构,LRG包含很多东西,比如选择的寄存器(_reg),允许使用哪些寄存器(_mask),寄存器压力(_reg_pressure),需要多少个寄存器来表达这个数据(_num_regs)。这些信息的收集由PhaseChaitin::gathe_lrg_masks完成,实现也很简单,遍历所有block的每个node,然后处理node,接着再处理node所有的input。

3.1.2 计算live out

搜集完所有指令的lrg之后,接着是指令live_out/live_gen(use)/live_kill(def)的计算,在PhaseLive::compute里面完成,熟悉C1寄存器分配的朋友对这过程应该不会陌生。这是个反向数据流传播问题。
?

3.2 Build virtual IFG

构造虚拟寄存器的IFG这步还比较简单。这一步遍历所有指令,然后检查当前指令_mask和live_out里面指令_mask是否重叠,_mask之前说了表示指令可能用到的所有寄存器,这一步就是说当前指令可能用到的所有寄存器是否和live out里面指令所有可能用到的寄存器是否有重叠,有重叠表示它们可能同时存活,即两者干扰。

void PhaseChaitin::build_ifg_virtual( ) {
  // 遍历所有block
  for (uint i = 0; i < _cfg.number_of_blocks(); i++) {
    Block* block = _cfg.get_block(i);
    IndexSet* liveout = _live->live(block);
    // 遍历当前block的所有node,即instruction
    for (uint j = block->end_idx() + 1; j > 1; j--) {
      Node* n = block->get_node(j - 1);
      // 拿到当前节点的lrg id(live range id)
      uint r = _lrg_map.live_range_id(n);
      // 如果存在live range
      if (r) {
        // 从live-out移除当前节点
        liveout->remove(r);
        ...
        // 求干扰图
        interfere_with_live(r, liveout);
      }

      // 如果当前node不是Phi,遍历当前node的所有input node
      if (!n->is_Phi()) {
        for(uint k = 1; k < n->req(); k++) {
          // 在live-out插入input node
          liveout->insert(_lrg_map.live_range_id(n->in(k)));
        }
      }
      ...
    }
  }
}
void PhaseChaitin::interfere_with_live(uint lid, IndexSet* liveout) {
  if (!liveout->is_empty()) {
    LRG& lrg = lrgs(lid);
    const RegMask &rm = lrg.mask();
    // 遍历live_out
    IndexSetIterator elements(liveout);
    uint interfering_lid = elements.next();
    while (interfering_lid != 0) {
      LRG& interfering_lrg = lrgs(interfering_lid);
      // 如果当前lrg id和liveout某个lrg id生命周期重叠,那么两者存在干扰
      if (rm.overlap(interfering_lrg.mask())) {
        // 在干扰图上连接两个node
        _ifg->add_edge(lid, interfering_lid);
      }
      interfering_lid = elements.next();
    }
  }
}

3.3 Aggressive coalescing

3.3.1 合并Phi/2-address指令及其输入

到这里IR存在一些virtual copy。所谓virtual copy是指我们没有真的复制代码,只是把变量作为phi的输入。phi本身有一个单独的live range,这一步的目的就是合并这些输入和phi,让它们具有相同的生命周期,如果没法合并,那就等下插入真正的copy代码。
?

所谓2-address就是指只有一个input,一个output的指令,比如最典型的mov eax,ebx。

void PhaseCoalesce::coalesce_driver() {
  // 遍历所有block
  for (uint i = 0; i < _phc._cfg.number_of_blocks(); i++) {
    coalesce(_phc._blks[i]);
  }
}
void PhaseAggressiveCoalesce::coalesce( Block *b ) {
  // 遍历successor
  uint i;
  for( i=0; i_num_succs; i++ ) {
    Block *bs = b->_succs[i];
    ...
    // 遍历successor的所有Phi
    for( uint k = 1; knumber_of_nodes(); k++ ) {
      Node *n = bs->get_node(k);
      if( !n->is_Phi() ) break;
      // 合并这些Phi和它的input
      combine_these_two( n, n->in(j) );
    }
  }
  // 寻找2-address指令,然后合并
  uint cnt = b->end_idx();
  for( i = 1; iget_node(i);
    uint idx;
    if (n->is_Mach() && (idx = n->as_Mach()->two_adr())) {
      MachNode *mach = n->as_Mach();
      combine_these_two(mach, mach->in(idx));
    }
  }
}

3.3.2 插入真正的copy

如果不能合并phi和它的输入,那就插入真的copy指令(2-address指令同理)。

3.4 Build phys. IFG

to write, to learn