Presentation is loading. Please wait.

Presentation is loading. Please wait.

寄存器分配 影响程序速度的因素 cpu, register, cache, memory, disk

Similar presentations


Presentation on theme: "寄存器分配 影响程序速度的因素 cpu, register, cache, memory, disk"— Presentation transcript:

1 寄存器分配 影响程序速度的因素 cpu, register, cache, memory, disk
寄存器分配是“后端”的三个重要任务之一。 getreg 书上的办法。

2 寄存器分配的概念 register allocation decides which program values shall reside in registers register assignment picks the specific register in which these values will reside. (eax? edx?)

3 Memory Model Register-to-register model
Assume IR program variable names are virtual registers. The allocator’s task is to map the set of virtual registers onto the registers provided by the target machine, called physical registers. In this scheme, the allocator inserts additional loads and stores for those virtual registers that cannot be allocated a phisical register. Memory-to-memory model Assume IR program variable names are memory references. The allocator determines when it is both safe and profitable to promote values into registers.

4 决定什么变量可以放在寄存器是tricky
main() { int *A, *B, i, j; int C[100], D[100]; if (fee()) { A = C; B = D; }else { B = C; } j = fie(); for (i=0; i<100; i++) A[i] = A[i] * B[j]; B[j] 是循环不变量且可以放在寄存器吗?

5 决定什么变量可以放在寄存器是tricky
subroutine fum integer x, y ... call foe(x,x) call foe(x,y) end subroutine foe(a,b) integer a, b a 和 b 可以放在寄存器里面吗?

6 决定什么变量可以放在寄存器是tricky
var i: integer; p: ^integer; begin i:=8; p for i:=0 to 9 do writeln(p^); end i 可以放在寄存器里面吗?

7 决定什么变量可以放在寄存器是tricky
c 的 memory specifier: volatile register 因此,我们下面的讨论假设Register-to-register model ,而把决定什么变量可以放在寄存器交给分析器或中间代码生成器。

8 寄存器类 寄存器并不都是一样的,如IA-32 有6+2个通用寄存器: EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP 。 有若干个特殊寄存器,CS, DS, SS, ES, FS, GS… EFlags, EIP MMX FP XMM 调试、测试寄存器等。

9 寄存器分配很难 寄存器类之间有复杂的interaction,如 mul, imul指令不能与浮点数指令并行执行;
MMX, SIMD指令不能和浮点指令同时执行 即使是单类,最优算法也是NP完全问题。不存在多项式算法

10 寄存器分配约定 k个机器寄存器,n个虚拟寄存器,n>k
spill: 某些虚拟寄存器不能分配到物理寄存器,其值在使用时用load指令装入,定义时用store指令保存。

11 简单的寄存器分配策略 原则:用得最多的变量应该放在寄存器里 算法: 1) 计算频率(使用计数usage counts) 2) 排序
3)分配 (注意不能把k个物理寄存器用光,应留几个用于处理内存变量的装入和存储) 4)重写代码

12 例子 频率: X 5 Z 5 Y 5 D 3 E 1 如果只有三个寄存器,可以分配两个给 X, Z,留下一个作临时寄存器 LOOP:
X = 2 * E Z = Y + X + 1 IF some condition THEN Y = Z + Y D = Y - 1 ELSE Z = X - Y D = 2 ENDIF X = Z + D Z = X ENDLOOP

13 问题 使用计数计算很难。特别是在非基本块,如循环中。 即使一个变量的使用计数高,其使用也可能很不均匀,没有必要一直占用一个寄存器
推而广之,由于优化的关键在循环,任何基本块的分配策略都很难是高效的。

14 全局寄存器分配 全局一般指一个过程(函数)以上。我们仅讨论过程。
IBM的Chaitin在Yorktown Heights的Watson研究中心的工作。第一个基于图着色的全局寄存器分配器 。 Rice大学的Briggs的改进。 参见我ftp上的有关论文。

15 几个概念 生存期(live range/lifetime): 变量的定义和使用的期限。可以用def-use链来表示。如果两个def-use链有共同的use,则视为同一个生存期。 如果两个变量的生存期交叠(overlap),则不能使用同一个寄存器,称为冲突(interfere)。 一个生存期应该用同一个虚拟寄存器命名,可以分配一个机器寄存器。在分配之后,任意一个机器寄存器通常对应几个生存期。

16 图着色问题 生存期:顶点 冲突:边 着色:给每个顶点一种颜色,相邻的顶点不能用同样的颜色
k-着色:能用k种颜色着色。一个冲突图有k-着色表示最多需k个寄存器。 So I regard the success of this approach, which has been the basis for much future work, as a triumph of the power of a simple mathematical idea over ad hoc hacking. --Gregory Chaitin

17 Yorktown 分配器 Chaitin和他的同事在IBM的Yorktown Heights研究中心为PL8 编译器实现的分配器的流程

18 说明 Renumber 找到函数中的所有生存期,并给它们以唯一的编号。
Build 这一步要建立冲突图G。为了保证效率,G同时有两种表示形式:一个三角位矩阵(triangular bit matrix)和一个邻接向量表。 Coalesce 在这个阶段分配器删去不要的复制赋值,从代码中去掉对应的复制语句,合并源生存期和目标生存期。删去一个复制的前提是源和目标互相不冲突。我们把结点l i 和 l j 的合并记为 l ij 。 删去复制指令必然会改变冲突图,我们要重复build 和 coalesce直到再没有复制赋值。

19 说明 Spill Costs 在着色之前,对每个生存期 l 都要计算它的spill开销。l 的spill开销是对spill l 后插入的load 和store指令造成的开销的评估。每条指令的开销用10d加权,而d是这条指令的循环嵌套深度。 Simplify 这个阶段,还有select ,一起对冲突图着色。simplify 反复地检查G中的结点,删去所有度数小于k的结点。当一个结点被删去时,与它关联的边也被删去,然后这个点被压入栈s。 如果遇到G中剩下的每个结点的度数都大于k,就要选择其中一个spill。但不会立刻把结点对应的生存期spill(也就是立刻更新代码和冲突图),只是把那个结点从G中删去,并标记为spilling。

20 说明 最后G会成为空图。如果有些结点被标记spilling,它们在spill code 阶段会被spilled,整个分配过程要重新来。否则,栈s被传递到select阶段。 Select 按照在simplify阶段确定的顺序,把颜色分配给每一个结点。按顺序,每个结点从栈s中弹出来,重新插入到G中,并分配一个与它的邻居不同的颜色。 Spill Code 在每个被spilled的生存期的引用前面插入一条load指令,在每个被spilled的生存期的赋值后面插入一条store指令。

21 识别生存期(Discovering Live Ranges)
每个分离的使用就是一个唯一的生存期,被分配器来着色。因此,分配器的首要任务是识别函数中的生存期。在实现中,每个生存期被给予一个唯一的索引,按照生存期索引重写中间代码,代替原来的虚拟寄存器号。 识别生存期首先要找到可以合并的def-use链。一根def-use链把一个虚拟寄存器的赋值和它所有的使用串联起来。如果几根def-use链共享同一个使用(换句话说,几个赋值可以到达一个使用),这几根def-use链是可以合并的。当然,那些从一个给定的赋值出发的链都是可以合并的。

22

23

24 冲突(Interference) 理解冲突的概念是理解图着色法分配器的关键。Chaitin给出了判断冲突的条件:如果两个生存期互相冲突,那么在函数中存在某些点以及某次运行满足: 两个生存期都被定义(赋值), 两个生存期都会被使用,而且 两个生存期有不同的值。 这些条件通常是不可判定的。Chaitin给出了一种近似条件。

25 Chaitin的近似 在每个运算时刻,那些生存期是活跃的且可用的?
我们称一个关于变量v的生存期l在某条语句s是活跃的,如果存在一条路径从s到v的某个使用,而且在这条路径上不存在对v的赋值。类似的,称l在s是可用的如果存在一条路径从v的赋值到s。事实上,可用性和活跃性对应了上面的条件1和条件2,是原来的不可判定条件的保守的近似。

26 Chaitin的近似 对第三个条件,Chaitin 通过对复写操作特殊处理来做了近似。显然,复写使源和目的生存期具有相同的值,所以不一定冲突。因此有可能合并。

27 冲突图(Interference Graph)
在Yorktown 分配器中一个核心数据结构就是冲突图。冲突图要提供5个操作。 new(n) 返回一个n个结点的图,但是没有边。 add(g,x,y) 返回图g,并在结点x和y之间添加一条边。 interfere(g,x,y) 如果图g中结点x和y之间存在一条边就返回真。 degree(g,x) 返回图g中结点x的度数。 neighbors(g,x,f) 对图g中结点x的各个邻居应用函数f。

28 在实现中,冲突图有两种表现形式:三角位矩阵和邻接向量表。位矩阵可以支持常数时间的add和interfere操作,而邻接向量表是neighbors的高效表示,存储在一个连续的数组里。
构造冲突图: 1)为位矩阵分配空间和清零。对整个代码做一遍扫描,填充位矩阵并计算每个结点的度数。 2)知道每个结点的度数以后,为邻接表分配空间,并从位矩阵直接生成。

29 实现的时候,每一遍对流图中的每个基本块逆向扫描,动态维护一个当前活跃并且可用的生存期的集合。每遇到一处赋值,为被赋值的生存期和s中的所有元素在图中添加边。
彻底完成build—coalesce循环后,位矩阵占据的空间可以被释放。邻接表在以后的阶段simplify 和select中还需要。

30 合并(Coalescing) 建立冲突图以后,就要执行合并。对每个复制指令,检查它的源生存期和目的生存期是否冲突;若不冲突,它们可以被合并,复制指令也被删去。为了合并两个生存期l x和l y 成为l xy,在用到l x、l y每个地方用l xy代替。当两个生存期l x和l y被合并,必须更新冲突图,使得l xy和l x、l y的邻居冲突。 合并阶段对中间代码做一遍完全的扫描,尽可能地合并,更新冲突图。 任一个复制语句被删去,都会导致重建冲突图,并引起更多的合并。这个周期一直重复到没有更多的复制语句被删去才停止。

31 为什么删去复制语句后必须重建冲突图

32 Spilling 最粗糙的对spilling的处理方案就是:spill一个生存期l,然后在l的每处赋值后面插入store指令,在l的每处使用前面插入load指令。Chaitin 描述了几种情况下可以改进: 如果被spilled的生存期的两个使用是紧相邻的,就不必要为第二次使用从内存中重新读入;为这两个使用分配同一个寄存器就行了。 如果一个使用紧跟在被spilled的生存期的赋值的后面,也就不必要在使用前重新读入了。 类似的,如果一个生存期的所有使用都紧跟着对它的赋值,这个生存期也不必被spilled。

33 着色(Coloring) Yorktown 分配器的核心就是它的着色算法。Chaitin的算法分为两个部分simplify和select。
Simplify不断地把结点从图中删去,压入栈中。在select阶段,结点被从栈中弹出来,重新加到图中去。当结点l i的度数小于k时,它被从图中移入栈中。以后l i从栈中移回图中时,它的邻居数目仍然小于k。显然l i 在图中是可以着色的。

34 insight: 设G中有一个结点n的度数小于k, 且称G删去n后成为G’. G’可k着色 则 G可k着色。
在simplify阶段,只有确认一个结点在当前图中可以着色,才把它删去。当每一个生存期被删去时,它的邻居的度数就会降低。在select阶段,结点按照被删去的逆序分配颜色。

35

36 如果在simplify阶段遇到一个图中所有的结点的度数都大于k的情形,有一个结点将被选中spill。这个spill结点被从图中删去,并被标记为spilling。一个处理方案是在程序中的这一点立刻插入代码,重建冲突图,寻找新的着色方案。这个方案虽然精确但开销巨大。Chaitin提到一种不很精确的处理方案:在选择spill结点后,继续简化(simplification)直到走完这一遍,标记出所有的spill结点。

37 选择spill结点 选择使Mn最小的结点n去spill。其中, degreen是指结点n当前的度数。 这个公式的直观意义:
2)希望能尽量化简图(分母)

38 Briggs的改进 Briggs发现了Chaitin的算法的缺陷,产生了不必要的spill。两个例子,一小一大,可以展示算法的缺陷。
假设我们要为右图找到一个2着色方案。显然存在这样的方案;比如, x和y着红色,w和z着绿色。

39 如果运用Chaitin的算法,因为图中没有度数小于2的结点,必然有一个结点要被spill。
再看下一个例子

40

41 Briggs用上图的例程做测试的时候,发现分配器把循环计数变量和循环计数的上界(M,N,I,J)spill出去了,尽管当时还有可分配的寄存器。
原因:当必须spill时,这些小而短的生存期的spill开销小。 但这并不解决问题。最后的结果就是:那段复制数组的循环代码几乎就没有利用寄存器。

42 问题的进一步分析 Chaitin把 “n得到一个颜色”近似为“n的度数<k”. 这是充分条件,而不是必要条件,因为n的邻居得着色可能相同(第一个例子) 分配器不知道有些spill不减轻负担。导致过多的spill.

43 Briggs的改进 Briggs对Chaitin的算法做了两处修正:
在simplify阶段只要发现剩下的结点的度数都不小于k,就从中间选择一个spill。这个结点被从图中删去,但是并没有被标记为spilling。它被压入栈,以后有可能获得一个颜色。 在select阶段发现不能为某些结点着色时,就放弃那个结点,继续对下面的结点着色。那些被放弃的结点在Chaitin的算法中一定被spill。

44 spill决定现在由select而不是simplify作出
Briggs改进后的分配器的流程 spill决定现在由select而不是simplify作出

45 推迟spill产生两个结果 首先,它消除了一些无效的spill。在Chaitin的方法中,spill是在着色以前的simplify阶段就决定了。一旦一个结点被spilled,它对应的生存期就被spilled。在Briggs的方法中,那些结点放在栈里只是spill的候选者,由select阶段才决定它们是否真的被spilled。

46 其次,它提供了一个更强大的着色算法。在为结点x选择颜色的时候,它检查x的当前所有邻居的颜色。如果有两个或者多个x的邻居收到同样的颜色,即使x的度数不小于k,它也可以被染色。这就比Chaitin简单地用“x的度数是否小于k”作为判据要准确。这样实现的分配器可以为上面的菱形图产生一个2着色方案。

47 再看SVD例程 生存期I,J,M和N较早成为spill的候选者,因为它们的spill开销小。然而,spill它们并不会减轻循环内部的寄存器分配的压力。分配器不得不spill那些大而长的生存期。等到这些小的生存期从栈中弹出来,已经有些大的生存期被spilled了。通过了解小生存期的邻居的颜色,分配器很容易地为它们在复制循环中的分配颜色。

48 结果 Chaintin的算法称为悲观(pessimistic)着色算法;Briggs的算法称为乐观(optimistic)着色算法
Briggs的算法比Chaintin的算法有较大的改进:使spill开销平均降低20%(在IBM RS/6000的XL编译器进行SPEC基准测试时)

49 其他 Briggs还作了很多其他的改进,参见其博士论文。包括部分源程序。
寄存器分配是一个比较热门的编译课题,我们讲的这些也只是点皮毛而已。但这已经比书上的要复杂很多。可见书的更新太慢。 大家在以后的学习工作中要善于从其他地方获取知识(当然web是一个不错的选择)


Download ppt "寄存器分配 影响程序速度的因素 cpu, register, cache, memory, disk"

Similar presentations


Ads by Google