Assembly 为什么编译器在寄存器分配中构造一个图?
我一直在研究寄存器分配,我想知道为什么他们都从live registers列表中构建图表,而有更好的方法可以做到这一点。我认为他们可以这样做的方式是,当活动寄存器超过可用寄存器的数量时,寄存器可能溢出。下面是一个示例(伪程序集):Assembly 为什么编译器在寄存器分配中构造一个图?,assembly,compiler-construction,graph-algorithm,cpu-registers,register-allocation,Assembly,Compiler Construction,Graph Algorithm,Cpu Registers,Register Allocation,我一直在研究寄存器分配,我想知道为什么他们都从live registers列表中构建图表,而有更好的方法可以做到这一点。我认为他们可以这样做的方式是,当活动寄存器超过可用寄存器的数量时,寄存器可能溢出。下面是一个示例(伪程序集): 我已经在汇编代码中列出了活动寄存器。现在,所有的教程和文本都从这里构建了干涉图,等等。但是,与之相反(正如我上面提到的),他们可以查看活动寄存器。例如,如果这是一个11寄存器机器,那么当活动寄存器是{t0,t1}时,我们必须选择一个寄存器来溢出。我觉得这比构造一个图和
我已经在汇编代码中列出了活动寄存器。现在,所有的教程和文本都从这里构建了干涉图,等等。但是,与之相反(正如我上面提到的),他们可以查看活动寄存器。例如,如果这是一个1
1
寄存器机器,那么当活动寄存器是{t0,t1}
时,我们必须选择一个寄存器来溢出。我觉得这比构造一个图和做所有其他的事情来检查是否需要溢出寄存器要简单得多。我知道无知不是全球性的(一定有人想到了这一点,认为它不合适),那么我在这里没有看到什么呢?仅仅从寄存器溢出的角度思考可能对直线代码来说是好的,但许多程序都包含循环。虽然登记效率在循环中往往比直线代码更重要,但登记溢出模型却难以处理一个值,因为在接近结束时,该值需要为部分环活,并且保持生存,直到执行在开始附近到达某个点,但不需要保持居中。在寄存器溢出模型下,一个值可能最终保存在循环开始附近的寄存器中,而在循环结束附近的另一个寄存器中。图形着色将确保两个图形都被分配了相同的“颜色”[即放置在同一寄存器中]。无需构建图形,例如,该算法避免构建图形。显然,它被诸如V8和HotSpot之类的JIT编译器使用,因为它速度快,折衷的办法是做出不太理想的决策
线性扫描比寄存器用完时一次扫描和溢出更复杂。相反,您可以找到活动范围并检查它们何时重叠。即使有一些分支和循环,这也可以做得不错
我可以想象,如果你不善于让分支的任意一边使用相同的临时寄存器,以及线性扫描所做的那种分析,那么你过于简单的算法可能会在branchy代码中严重退化。正如@supercat所说,并非所有代码都是直线的即使如此,LRU关于泄漏内容的决定也不是最优的。您是一个编译器,您可以向前看,看看接下来要使用的寄存器是什么
此外,您还需要向前看,看看是否/如何使用结果,除非您计划根本不进行优化。e、 g.x++;x++
应将与x+=2
相同的代码编译为add指令,而不是两个单独的add-1操作。因此,您需要某种数据结构来表示程序逻辑,而不仅仅是在一个过程中将其动态转换为asm。(除非您正在编写一个真正的一次通过编译器,如。)
注意,许多编译器的目标都是好代码,而不仅仅是正确的代码,这意味着最小化溢出/重新加载,尤其是在循环携带的依赖链上。即使在branchy代码中也能很好地处理分配。这就是静态单一赋值(SSA)图的用途所在,同时还需要知道何时从循环中提升或接收计算或内存访问
相关的:
有一些关于寄存器分配算法的详细信息,也有一些到论文的链接。如何选择要溢出的寄存器?t0还是t1?假设我们的活动寄存器集是{t0},然后是{t0,t1},然后是{t1},然后是{t1,t2},然后是{t2}
## ldi: load immediate
## addr: add registers and store in arg 2
## store: store memory at offset from stack pointer
.text
main:
# live registers: {}
ldi %t0, 12 # t0 = 12
# live registers: {t0}
ldi %t1, 8 # t1 = 8
# live registers: {t0, t1}
addr %t0, %t1 # t1 = t0 + t1
# live registers: {t1}
store -4(%sp), %t1 # -4(%sp) = t1
# live registers: {}
exit