概述
寄存器位于CPU 或 GPU内部的少量高速存储器,通常用于保存机器指令的操作数
由于其价格昂贵,导致其数量有限,又由于存储速度快,使其不可或缺。因此,寄存器是计算机体系结构中的关键资源之一。在计算复杂表达式的过程中产生的中间结果都保存在寄存器中,更复杂的编译器会将经常使用的变量也保存在寄存器中,以避免反复存取
如果是优化的编译器,还会将公共子表达式消除或者将循环不变量移动以后的重用值保存在寄存器中
开发者在编写高级语言程序时会用到变量定义,如 int x, string y 等。大部分程序不关心变量在计算机体系结构中用什么形式表示。从开发者的角度看,变量中的数据通常被认为保存在内存中,可以通过文件I/O接口在硬盘和内存之间转移,而编译器复杂在内存和寄存器之间转移数据,以便CPU 和 GPU 可以操作寄存器中的数据。从存储结构的角色,硬盘、内存、缓存(cache) 和寄存器特点不同
在编译过程中的代码生成阶段,程序中的变量会被编译器替换为寄存器
高级语言程序中使用的变量数量几乎无限,但CPU 或 GPU 中的寄存器数量是有限的。寄存器分配作为编译后端流程的一个阶段要解决这对矛盾,控制寄存器的分配和使用
因此,寄存器分配的目的是将程序中的数量无限的虚拟寄存器映射到数量有限的物理寄存器
。不同的目标设备有不同数量的物理寄存器
如果物理寄存器的数量不足以满足虚拟寄存器的需求,有些虚拟寄存器显然就只能映射到内存。这些虚拟寄存器称为溢出虚拟寄存器。好的寄存器分配算法应该努力在物理寄存器中分配尽可能多的变量,因为物理寄存器能提供更快的访问时间
寄存器分配原理
寄存器分配是编译器优化中最重要的问题之一,好的寄存器分配算法可以显著提高程序性能,因此,寄存器分配也是编译器理论研究中一个非常活跃的领域
寄存器分配可以工作在表达式、基本块、函数(也称全局)或整个程序等不同级别。历史上出现过多种寄存器分配算法,如图着色算法、线性扫描算法、整数线性规划算法、PBQP 算法、Multi-Flow Commodities 算法、基于SSA的寄存器分配
LLVM 没有支持上述全局寄存器分配算法,而是实现了基本寄存器分配(Basic Register Allocator)、快速寄存器分配(Fast Register Allocator)、PBQP 寄存器分配器(PBQP Register Allocator) 和 贪厌寄存器分配器(Greedy Register Allocator) 四种寄存器分配器
和指令选择、指令调度一样,寄存器分配是编译器后端的一个重要组成部分。在后端流程中,寄存器分配在机器指令调度(MI Scheduling) 之后执行
寄存器分配的基本目的是通过将程序变量尽可能地分配给物理寄存器,从而提供程序执行速度。以图着色算法为例,如果寄存器分配问题被抽象成图着色问题,那么图中的每个节点代表某个变量的活跃范围(live range)
活跃范围定义是从变量第一次被赋值(或称定值)开始,到该变量下一次被赋值前的最后一次被使用为止。两个节点之间的边表示这两个变量因为活跃范围重叠,导致互相冲突或干扰。一般说来,如果两个变量在函数的某一点是同时活跃(live) 的,那么这两个变量就相互冲突,不能占有同一个寄存器。假设有如下代码:
a = c - d
e = a + b
f = e + 3
在表达式 e = a - b 之后,变量a不再使用。同样,在表达式 f = e + 3之后,变量 e 也不再使用
因此,a、e、f 可以被分配给同一个寄存器,而寄存器中保存的值不会产生冲突。因此,可以只使用4个寄存器s1、s2、s3、s4 替换代码中的6个变量(寄存器使用还可以进一步简化)
s1 = s2 - s3
s1 = s1 - s4
s1 = s1 + 3
为了将寄存器分配给尽可能多的变量又不引起冲突,需要根据代码逻辑生成控制流图,并执行活跃性分析(liveness analysis) 得到寄存器干扰图
寄存器分配器基于寄存器干扰图执行寄存器分配算法,将寄存器分配给变量
控制流图是以基本块为节点的有向图,控制流图中每一节点代码一个程序基本块,节点之间的边代表基本块之间的控制转移。因此,控制流图中的每一节点代表一个程序基本块,节点之间的边代表基本块之间的控制转移
因此,控制流图可表示为 G = (N, E), N 是节点集合,每个节点对应程序中的一个基本块;E是边的集合。如果程序逻辑从基本块U的出口转向基本块V,则从U到V有一条有向边,表示从节点U到节点V存在一条可执行路径
这时,称U为V的前驱节点,V为U的后继节点,表示在执行完节点U中的指令后,可顺序执行节点V中的指令
L1: a = b + c
d -= a
e = d + f
if (e > 0)
f = 2 * 3
else {
b = d + e
e = e - 1
}
b = f + c
goto L1
调用寄存器分配算法为上述代码中的变量分配寄存器,首先要做的是活跃性分析
活跃性分析是指确定哪些变量在程序点保持活跃。结合控制流图,就是判断变量x在程序点p上的值是否会在流图中从点p出发的某条路径中使用
如果是,则x在p上活跃;否则,x在p上不活跃。活跃性分析是一个后向数据流分析问题,因为当前变量x是否在未来的某个地方被用到,只能通过后继节点的信息获知
活跃性分析的重要用途之一是为基本块进行寄存器分配,某个值被计算保存到一个寄存器后,很有可能在基本块中被使用。如果该值在基本块中不活跃,就不必保存这个值
活跃性分析结果显示在特定程序点哪些变量是活跃的,以及哪些寄存器分配时会互相冲突,并可以由此得知寄存器干扰图(Register Interference Graph, RIG)
如果在程序点p存在两个变量a和b同时活跃,那么就变量a和b在程序点p互相干扰,寄存器干扰图中的节点a、b之间有边相连,这时,变量a和b应分配同一寄存器