0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

不同思路的寄存器分配算法

openEuler 来源:毕昇编译 作者:王博洋 2022-08-24 10:17 次阅读

概念介绍

在介绍算法之前,我们回顾下基本概念:

  • |X|:X的度数,(无向图中)节点的邻居个数。

  • CFG:控制流图。

  • successor:本文指CFG中基本块的后继。

  • 四元式:(op,result,arg1,arg2),比如常见的a=b+c就可以看作四元式(+,a,b,c)。

  • SSA(Static Single Assignment):静态单赋值。

  • use/def:举个例子,对于指令n: c <- c+b来说 use[n]={c,b},def[n]={c}。

  • live-in:当以下任一条件满足时,则称变量a在节点n中是live-in的,写作a∈in[n]。节点n本文中代表指令。

  1. a∈use[n];
  2. 存在从节点n到其他节点的路径使用了a且不包括a的def。
  • live-out: 变量a在节点n的任一后继的live-in集合中。写作a∈out[n]

  • 干涉:在某一时刻,两个变量在同一live-in集合中。

  • RIG(Register Interfere Graph): 无向图,其点集和边集构成如下:

  • 节点:变量
  • 边:如果两节点存在干涉,那么这两节点之间就有一条干涉边
  • k-着色:给定无向图G=(V,E),其中V为顶点集合,E为边集合。将V分为k个组,每组中没有相邻顶点,可称该图G是k着色的。当然可着色前提下,k越小越好。

需要注意的是,我们后续的算法会作用在最普通的四元式上,而不是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的代码进行活跃变量分析

4c584276-22c3-11ed-ba43-dac502259ad0.png

图1[2]

可以得到每个点的活跃变量如图2所示:

4c7c6d68-22c3-11ed-ba43-dac502259ad0.png

图2

过程呢?限于篇幅,仅仅计算第一轮指令1的结果,剩余部分读者可自行计算。

步骤 下标 out in
第一次迭代 1 {} {b,c}
... ... ... ...

可画出RIG如图3:

4c8d3bde-22c3-11ed-ba43-dac502259ad0.png

图3

图着色

经过上文的活跃变量分析,我们得到了干涉图,下一步对其进行上色。

但是图着色是一个NP问题,我们会采用启发式算法对干涉图进行着色。基本思路是:

  1. 找到度小于k的节点;
  2. 从图中删除;
  3. 判断是否为可着色的图;
  4. 迭代运行前3步直到着色完成。

算法描述[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 {} 4c8d3bde-22c3-11ed-ba43-dac502259ad0.png
1 {a}

4cb49e2c-22c3-11ed-ba43-dac502259ad0.png

2 {d,a}

4ccf088e-22c3-11ed-ba43-dac502259ad0.png

3 {c,d,a}

4cd9b4fa-22c3-11ed-ba43-dac502259ad0.png

4 {b,c,d,a}

4ced3430-22c3-11ed-ba43-dac502259ad0.png

5 {e,b,c,d,a}

4cfbeae8-22c3-11ed-ba43-dac502259ad0.png

6 {f,e,b,c,d,a}
寄存器分配 stack

4d0e7dfc-22c3-11ed-ba43-dac502259ad0.png

{e,b,c,d,a}

4d243d22-22c3-11ed-ba43-dac502259ad0.png

{b,c,d,a}

4d38f3d4-22c3-11ed-ba43-dac502259ad0.png

{c,d,a}

4d437052-22c3-11ed-ba43-dac502259ad0.png

{d,a}

4d52c2b4-22c3-11ed-ba43-dac502259ad0.png

{a}

4d747c88-22c3-11ed-ba43-dac502259ad0.png

{}

所以图3中的RIG是4-着色的。但如果只有三种颜色可用,怎么办呢?

没关系,我们还有大容量的内存,虽然速度慢了那么一点点。着色失败就把变量放在内存里,用的时候再取出来。

依然是上例,但是k=3,只有三个颜色。

步骤 stack RIG
0 {}

4c8d3bde-22c3-11ed-ba43-dac502259ad0.png

1 {a}

4cb49e2c-22c3-11ed-ba43-dac502259ad0.png

2 没有度数小于3的节点了,需要溢出变量了 /

如果f的邻居是2-着色的就好了,但不是。那就只能选一个变量存入内存了。这里我们选择将变量f溢出至内存。溢出后的IR和RIG如图:

4da4d202-22c3-11ed-ba43-dac502259ad0.png

图4 溢出后的IR

4dbbbd8c-22c3-11ed-ba43-dac502259ad0.png

图5 溢出后的RIG

所以,溢出其实是分割了变量的生命周期以降低被溢出节点的邻居数量。溢出后的着色图如图6:

4dd239a4-22c3-11ed-ba43-dac502259ad0.png

图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中相同的节点,自然会分配同一寄存器。

  • 相同节点?可以扩展RIG:新增虚线边,代表合并候选人。

  • 成为合并候选人的条件是:如果X和Y的生命周期不重合,那么对于Y=X指令中的X和Y是可合并的。

  • 为了保证合并合法且不造成溢出:合并后局部的度数

那么如何计算局部的度数?介绍三种算法:

  • 简单算法
  • Brigg's 算法
  • George's 算法
  1. 简单算法:(|X|+|Y|),很保守的算法但是可能会错过一些场景

    比如k=2时,图7应用简单算法是没办法合并的

    4de756a4-22c3-11ed-ba43-dac502259ad0.png

    图7[3]

    但明显图7可以合并成图8:

    4dff6e2e-22c3-11ed-ba43-dac502259ad0.png

    图8[3]

  2. Brigg's 算法:X和Y可合并,如果X和Y中度数≥k的邻居个数<k。但是如果X的度数很大,算法效率就不高

  3. George's算法:X和Y可合并,如果对Y的每个邻居T,|T|

    比如k=2时,图9就可以合并X和Y。

    4e16fcce-22c3-11ed-ba43-dac502259ad0.png

    图9[3]

    相对于Brigg算法、George算法不用遍历节点的邻居。注意,图着色时可以按节点度数从小到大依次访问。

到此,图着色算法介绍完毕。

线性扫描

接下来介绍一种不同思路的算法:线性扫描。算法描述如下[4]

LinearScanRegisterAllocation:
active := {}
for i in live interval in order of increasing start point
ExpireOldIntervals(i)
if length(avtive) == R
SpillAtInterval(i)
else
register[i] := a regsiter removed from pool of free registers
add i to active, sorted by increasing end point
ExpireOldInterval(i)
for interval j in active, in order of increaing end point
if endpoint[j] >= startpoint[i]
return
remove j from active
add register[j] to pool of free registers
SpillAtInterval(i)
spill := last interval in active
if endpoint[spill] > endpoint[i]
register[i] := register[spill]
location[spill] := new stack location
remove spill from active
add i to active, sorted by increasing end point
else
location[i] := new stack location

live interval其实就是变量的生命期,用活跃变量分析可以算出来。不过需要标识第一次出现和最后一次出现的时间点。

举个例子:

4e2cd620-22c3-11ed-ba43-dac502259ad0.png

图10

变量名 live interval
a 1,2
d 2,3,4,5
e 3,4,5,6

llvm中实现

在上文中介绍的算法都是作用在最普通的四元式上,但LLVM-IR是SSA形式,有PHI节点,但PHI节点没有机器指令表示,所以在寄存器分配前需要把PHI节点干掉,消除PHI节点的算法限于篇幅不展开,如感兴趣的话请后台留言。

llvm作为工业级编译器,有多种分配算法,可以通过llc的命令行选项-regalloc=pbqp|greedy|basic|fast来手动控制分配算法。

不同优化等级默认使用算法也不同:O2和O3默认使用greedy,其他默认使用fast。

fast算法的策略很简单,扫描代码并为出现的变量分配寄存器,寄存器不够用就溢出到内存。用途主要是调试

basic算法以linearscan为基础并对life interval设置了溢出权重而且用优先队列来存储life interval。

greedy算法也使用优先队列,但特点是先为生命期长的变量分配寄存器,而短生命期的变量可以放在间隙中,详情可以参考[5]

pbqp算法全称是Partitioned Boolean Quadratic Programming,限于篇幅,感兴趣的读者请查阅[6]

至于具体实现,自顶向下依次是:

  • TargetPassConfig::addMachinePasses含有寄存器分配和其他优化
  • addOptimizedRegAlloc中是与寄存器分配密切相关的pass,比如上文提到的消除PHI节点
  • addRegAssignAndRewriteOptimized是实际的寄存器分配算法
  • 寄存器分配相关文件在lib/CodeGen下的RegAllocBase.cpp、RegAllocGreedy.cpp、RegAllocFast.cpp、RegAllocBasic.cpp和RegAllocPBQP.cpp等。
  • RegAllocBase类定义了一系列接口,重点是selectOrSplit和enqueue/dequeue方法,数据结构的重点是priority queue。selectOrSplit方法可以类比上文中提到的SpillAtInterval。priority queue类比active list。简要代码如下:
voidRegAllocBase::allocatePhysRegs(){
//1.virtualreg其实就是变量
while(LiveInterval*VirtReg=dequeue()){

//2.selectOrSplit会返回一个可用的物理寄存器然后返回新的liveintervals列表
usingVirtRegVec=SmallVector4>;
VirtRegVecSplitVRegs;
MCRegisterAvailablePhysReg=selectOrSplit(*VirtReg,SplitVRegs);
//3.分配失败检查
if(AvailablePhysReg==~0u){
...
}
//4.正式分配
if(AvailablePhysReg)
Matrix->assign(*VirtReg,AvailablePhysReg);

for(RegisterReg:SplitVRegs){
//5.入队分割后的liverinterval
LiveInterval*SplitVirtReg=&LIS->getInterval(Reg);
enqueue(SplitVirtReg);
}
}
}

至于这四种算法的性能对比,我们主要考虑三个指标:运行时间、编译时间和溢出次数。

4e3c515e-22c3-11ed-ba43-dac502259ad0.png

图11 各算法的运行时间,图源[6]

横坐标是测试集,纵坐标是以秒为单位的运行时间

4e73d2d2-22c3-11ed-ba43-dac502259ad0.png

12各算法的编译时间,图源[6]

横坐标是测试集,纵坐标是编译时间

4e857aaa-22c3-11ed-ba43-dac502259ad0.png

13 各算法的溢出次数,图源[6]

从这三幅图可以看出greedy算法在大多数测试集上都优于其他算法,因此greedy作为默认分配器是可行的。

小结

我们通过一个例子介绍了活跃变量分析和图着色算法。借助活跃变量分析,我们知道了变量的生命期,有了变量生命期建立干涉图,对干涉图进行着色。如果着色失败,可以选择某个变量溢出到内存中。之后在RIG的基础上介绍了寄存器合并这一变换。

然后我们简单介绍了不同思路的寄存器分配算法:linearscan。最后介绍了llvm12中算法的实现并对比了llvm中四种算法的性能差异。

审核编辑:汤梓红


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 寄存器
    +关注

    关注

    30

    文章

    5028

    浏览量

    117721
  • 算法
    +关注

    关注

    23

    文章

    4455

    浏览量

    90756
  • 编译器
    +关注

    关注

    1

    文章

    1577

    浏览量

    48614

原文标题:编译器优化那些事儿(5):寄存器分配

文章出处:【微信号:openEulercommunity,微信公众号:openEuler】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    解析CPU中的寄存器

    8位寄存器在16位寄存器中,而16位寄存器在32位寄存器中。
    发表于 09-19 10:10 2902次阅读

    线性汇编-寄存器分配疑问 请问为什么不同的变量分配了相同的寄存器

    上面是线性汇编函数,下图为寄存器分配,为什么不同的变量分配了相同的寄存器???如何使一个变量分配一个寄存
    发表于 08-07 09:06

    分配16位状态/控制寄存器的典型方法是什么?

    应该被放置到连续的UDB中。对于数据通路,你可以通过链接它们之间的链接信号来实现,但是在这种情况下,没有什么可以链接。标准组件似乎也不支持更广泛的寄存器分配。是否有类似于“CypPoS3x控件16
    发表于 10-18 06:04

    编译优化那些事儿(5):寄存器分配

    色。如果着色失败,可以选择某个变量溢出到内存中。之后在RIG的基础上介绍了寄存器合并这一变换。然后我们简单介绍了不同思路寄存器分配算法:l
    发表于 08-24 14:41

    寄存器与移位寄存器

    寄存器与移位寄存器 寄存器是用来寄存数码的逻辑部件,所以必须具备接收和寄存数码的功能。任何一种触发器都可以构成
    发表于 03-12 15:19 59次下载

    寄存器应用举例

    寄存器应用举例   在9.2.3寄存器的应用一节中,曾介绍利用寄存器集成芯片74LS194构造的两种脉冲分配器:环形计数器和扭环形计数器。若需要更多路的顺序节拍脉冲,可以考虑在
    发表于 05-17 00:02 1416次阅读
    <b class='flag-5'>寄存器</b>应用举例

    什么是Register Pressure(寄存器不足) /

    什么是Register Pressure(寄存器不足) / Register Renaming(寄存器重命名)?   Register Pressure(寄存器不足) 软件算法执行时
    发表于 02-04 11:02 1251次阅读

    寄存器,寄存器是什么意思

    寄存器,寄存器是什么意思 寄存器定义  寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用
    发表于 03-08 14:26 2.1w次阅读

    数据寄存器,数据寄存器是什么意思

    数据寄存器,数据寄存器是什么意思 数据寄存器数据寄存器包括累加器AX、基址寄存器BX、计数寄存器
    发表于 03-08 14:38 1.2w次阅读

    32位寄存器,32位寄存器是什么意思

    32位寄存器,32位寄存器是什么意思  从X8086开始学了一年,第一个ASM的程序就是变32换16进制的程序,不过现在叫我从新开始写ASM程
    发表于 03-08 17:26 1.7w次阅读

    寄存器组网络处理器上的寄存器分配技术

    本内容提供了多寄存器组网络处理器上的寄存器分配技术
    发表于 06-28 15:26 28次下载
    多<b class='flag-5'>寄存器</b>组网络处理器上的<b class='flag-5'>寄存器</b><b class='flag-5'>分配</b>技术

    寄存器与移位寄存器

    寄存器与移位寄存器:介绍寄存器原理和移位寄存器的原理及实现。
    发表于 05-20 11:47 0次下载

    高效的C编程之寄存器分配

    14.7 寄存器分配 编译器一项很重要的优化功能就是对寄存器分配。与分配寄存器中的变量相比,
    发表于 10-17 17:17 4次下载

    反射内存卡编程的三个寄存器

    系统 BIOS 分配寄存器组和反射内存的基址。反射内存的寄存器组的基址和内存地址可以比较随意,。对于超出了基本的设置,如启用或禁用中断或 DMA 周期的操作,用户必须知道三个寄存器组内
    发表于 04-02 16:25 471次阅读

    什么是编译器算法寄存器分配

    寄存器是CPU中的稀有资源,如何高效的分配这一资源是一个至关重要的问题。本文介绍了基于图着色的寄存器分配算法
    的头像 发表于 03-02 16:11 670次阅读
    什么是编译器<b class='flag-5'>算法</b>之<b class='flag-5'>寄存器</b><b class='flag-5'>分配</b>