📜  编译器设计中的寄存器分配算法

📅  最后修改于: 2021-09-27 14:50:23             🧑  作者: Mango

寄存器分配是编译器最后阶段的一个重要方法。寄存器的访问速度比高速缓存快。寄存器可以小到几百 Kb。因此有必要使用最少数量的寄存器进行变量分配。有三种流行的寄存器分配算法。

  1. 初始寄存器分配
  2. 线性扫描算法
  3. Chaitin 算法

这些解释如下。

1. 简单的寄存器分配:

  • 朴素(无)寄存器分配基于变量存储在 Main Memory 中的假设。
  • 我们不能直接对存储在 Main Memory 中的变量进行操作。
  • 变量被移动到允许使用 ALU 执行各种操作的寄存器。
  • ALU 包含一个临时寄存器,其中变量在执行算术和逻辑运算之前被移动。
  • 一旦操作完成,我们需要在此方法中将结果存储回主内存。
  • 从主内存来回传输变量会降低整体执行速度。
a = b + c
d = a
c = a + d

存储在主内存中的变量:

a b c d
2 fp 4 fp 6 fp 8 fp

机器级说明:

LOAD  R1, _4fp
LOAD  R2, _6fp
ADD   R1, R2 
STORE R1, _2fp
LOAD  R1, _2fp
STORE R1, _8fp
LOAD  R1, _2fp
LOAD  R2, _8fp
ADD   R1, R2
STORE R1, _6fp

优点 :

  • 易于理解的操作和变量从主内存到寄存器的流程,反之亦然。
  • 只需 2 个寄存器就足以执行任何操作。
  • 设计复杂度较低。

缺点:

  • 随着变量从主存储器移到寄存器,时间复杂度增加。
  • 太多的 LOAD 和 STORE 指令。
  • 要第二次访问变量,我们需要将它存储到主内存以记录所做的任何更改并再次加载它。
  • 这种方法不适用于现代编译器。

2. 线性扫描算法:

  • 线性扫描算法是一种全局寄存器分配机制。
  • 这是一种自下而上的方法。
  • 如果 n 个变量在任何时间点都处于活动状态,那么我们需要 ‘n’ 个寄存器。
  • 在该算法中,变量被线性扫描,以确定变量的有效范围,根据该范围分配寄存器。
  • 该算法背后的主要思想是分配最少数量的寄存器,以便这些寄存器可以再次使用,这完全取决于变量的有效范围。
  • 对于这个算法,我们需要实现代码优化的实时变量分析。
a = b + c
d = e + f
d = d + e
IFZ a goto L0
b = a + d
goto L1
L0 : b = a - d 
L1 : i = b

控制流图:

  • 在任何时间点,在本例中,活动变量的最大数量都是 4。因此,我们最多需要 4 个寄存器来进行寄存器分配。

如果我们在上图中的任何一点画一条水平线,我们可以看到我们正好需要 4 个寄存器来执行程序中的操作。

拆分:

  • 有时可能无法获得所需数量的寄存器。在这种情况下,我们可能需要将一些变量移入和移出 RAM。这称为溢出。
  • 通过移动程序中使用次数较少的变量,可以有效地进行溢出。

缺点:

  • 线性扫描算法没有考虑变量的“生命周期漏洞”。
  • 变量在整个程序中都不是活动的,并且该算法无法记录变量活动范围内的漏洞。

3.Graph Coloring (Chaitin’s Algorithm) :

  • 寄存器分配被解释为图形着色问题。
  • 节点代表变量的有效范围。
  • 边代表两个生命周期之间的连接。
  • 为节点分配颜色,使得没有两个相邻节点具有相同的颜色。
  • 颜色数表示所需的最少寄存器数。

图形的 k 着色映射到 k 个寄存器。

脚步 :

  1. 选择度数小于 k 的任意节点。
  2. 将该节点推入堆栈并删除它的所有传出边。
  3. 检查剩余边的度数是否小于 k,如果是,则转到 5 否则转到 #
  4. 如果任何剩余顶点的度数小于 k,则将其推入堆栈。
  5. 如果没有更多的边可供推送,并且如果堆栈中存在所有边,则弹出每个节点并为它们着色,以便没有两个相邻的节点具有相同的颜色。
  6. 分配给节点的颜色数是所需的最少寄存器数。

# 根据它们的生存范围溢出一些节点,然后使用相同的 k 值重试。如果问题仍然存在,则意味着假定的 k 值不能是最小寄存器数。尝试将 k 值增加 1 并再次尝试整个过程。

对于上面提到的相同说明,图形着色如下:

假设 k=4

上色前

进行图形着色后,得到最终图形如下

具有 k(4) 种颜色的最终图

注意:任何颜色(寄存器)都可以分配给“i”,因为它没有任何其他节点的边缘。