寄存器分配是编译器最后阶段的一个重要方法。寄存器的访问速度比高速缓存快。寄存器可以小到几百 Kb。因此有必要使用最少数量的寄存器进行变量分配。有三种流行的寄存器分配算法。
- 初始寄存器分配
- 线性扫描算法
- 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 个寄存器。
脚步 :
- 选择度数小于 k 的任意节点。
- 将该节点推入堆栈并删除它的所有传出边。
- 检查剩余边的度数是否小于 k,如果是,则转到 5 否则转到 #
- 如果任何剩余顶点的度数小于 k,则将其推入堆栈。
- 如果没有更多的边可供推送,并且如果堆栈中存在所有边,则弹出每个节点并为它们着色,以便没有两个相邻的节点具有相同的颜色。
- 分配给节点的颜色数是所需的最少寄存器数。
# 根据它们的生存范围溢出一些节点,然后使用相同的 k 值重试。如果问题仍然存在,则意味着假定的 k 值不能是最小寄存器数。尝试将 k 值增加 1 并再次尝试整个过程。
对于上面提到的相同说明,图形着色如下:
假设 k=4
进行图形着色后,得到最终图形如下
注意:任何颜色(寄存器)都可以分配给“i”,因为它没有任何其他节点的边缘。