【问题标题】:Simple register allocation scheme for x86x86 的简单寄存器分配方案
【发布时间】:2014-07-10 16:42:54
【问题描述】:

我正在写一个简单的玩具编译器,我来到生成机的部分 代码(本例中为 x86-32 程序集)。这就是我现在所拥有的:

给定赋值语句: d := (a-b)+(c-a)-(d+b)*(c+1)

我先生成如下中间代码(三元组形式的3个地址代码):

(0) sub, a, b
(1) sub, c, a
(2) add, (0), (1)
(3) add, d, b
(4) add, c, 1
(5) mul, (3), (4)
(6) sub, (2), (5)
(7) asn, d, (6)

我正在使用中间代码,希望稍后执行一些优化 在上面。目前,我不操纵 3AC 并直接从中生成程序集。

我的寄存器使用方案如下:我执行所有算术运算 使用 EAX 并将中间结果保存在其他寄存器 EBX、ECX 和 EDX 中。为了 例如,从之前的 3AC 我生成以下程序集:

mov     eax, a
sub     eax, b      ; eax = (0)
mov     ebx, eax    ; ebx = (0) & eax = free
mov     eax, c
sub     eax, a      ; eax = (1)
add     ebx, eax    ; ebx = (2) & eax = free
mov     eax, d
add     eax, b      ; eax = (3)
mov     ecx, eax    ; ecx = (3) & eax = free
mov     eax, c
add     eax, 1      ; eax = (4)
imul    ecx         ; eax = (5) & ecx = free
sub     ebx, eax    ; ebx = (6) & eax = free
mov     eax, ebx    ; eax = (6) & ebx = free
mov     d, eax

我的问题是:当我需要溢出 EAX 的结果但所有 寄存器忙(EBX、ECX 和 EDX 正在临时保存)。我应该保存 堆栈中 EAX 的值并稍后恢复?如果是这样,我应该预订吗 每个函数的堆栈框架中有一些额外的空间用于额外的临时?

我再说一遍,这只是我现在的想法。如果还有其他简单的 我想知道的分配寄存器方案(我知道存在 涉及图形着色等的更复杂的解决方案;但我只是在寻找一些东西 简单实用)。

【问题讨论】:

  • 由于这是一个玩具编译器,你不必太担心。您可以使用简单的pushpop,它们仅在必要时不应破坏堆栈。另外,也许这会有所帮助:en.wikipedia.org/wiki/Sethi%E2%80%93Ullman_algorithm.
  • 哦,还要记住,如果你不需要完整的 32 位,你可以将它们拆分,并使用 ah/albh/bl 等。
  • 反之亦然。您在堆栈帧中为每个变量分配一个插槽,然后您开始尝试不使用它们。显而易见的好处是,您总是有办法溢出寄存器。

标签: assembly compiler-construction x86 allocation cpu-registers


【解决方案1】:

与其总是将结果计算到 EAX 中,不如考虑将结果计算到目标位置,该目标位置可能是寄存器或内存位置。

在伪代码中:

for each 3AC instruction I
   Look up the set S of places that hold operands of I
   R = allocate_place(I)  // register or memory for the result
   Emit code that uses S and puts the result of I into R
      // code emitted differs depending on whether R, S are registers or memory
   free_places S

您将使用分配器,该分配器提供寄存器名称或临时内存位置,具体取决于可用的内容。分配器保留一个“反向映射”,允许在上面查找指令的每个操作数所在的位置。分配器可以使用多种策略。最简单的就是先用完所有的寄存器,然后再开始分配内存。

请注意,当整个函数的代码生成后,分配器将知道该函数需要多少个临时内存位置。函数前导代码必须在创建堆栈帧时设置这些。您需要一种机制来“回补”具有正确位置数量的序言。对此有多种可能性。询问您是否需要想法。然后,在继续编译下一个函数之前重置分配器。

上面的算法在使用它的值时立即释放相应的资源(寄存器或内存位置),因为您的简单代码生成器允许这个不变量。如果您消除常见的子表达式或进行其他优化,那么决定何时释放寄存器会变得更加复杂,因为它的值可能会被多次使用。

嵌入在表达式中的函数调用引发了其他有趣的案例来思考。如何保存寄存器?

【讨论】:

  • 谢谢,这正是我想要的。
【解决方案2】:

如果您的数据多于寄存器并推送多余的数据,那么您必须在使用它之前将其弹出。如果你最终没有使用它(由于分支),你仍然必须弹出它。

因此,您甚至没有使用数据就在推送和弹出。

您会将数据推送到堆栈中,并在需要时将其弹出。

您还丢弃了保存在寄存器中的数据,该弹出数据将被移动到。

您必须让编译器记住堆栈的深度,并确保从函数返回时它是正确的。

在函数之前或之后简单地使用本地存储并不容易。您可以 mov(e)(可能带有偏移)而不是将您推到 bpxchg(可能带有偏移)进出这个本地存储区。

您可以随时从函数中Ret(urn),而无需弹回您推送的相同数量的数据,只需将其丢弃在本地存储中即可。

显然,这很容易支持几乎无限数量的“寄存器”,而推动和弹出则成为“3x3(或更多)滑块拼图”的游戏。

使用螺丝刀解决这些滑块拼图(以及魔方)更快(世界纪录冠军除外)。只需撕入您想要的东西(访问任何内存位置)并按照您的意愿将其重新组合在一起(不要在Ret(urning)之前弹出东西) - 没有所有的滑回和第四款“河内塔”风格。

使用局部变量而不是堆栈(除非您只有一个或两个寄存器短,那么推送和弹出的智能例程可能比内存访问更快;就像一个几乎解决的立方体可以比弹出用螺丝刀打开东西)。

在 2 或 3 处会有一个屈服点(寄存器少于您想要的),其中局部变量比推送和弹出更快,具体取决于代码以及如何对其进行洗牌(优化)。

【讨论】:

    猜你喜欢
    • 2010-12-29
    • 2011-02-12
    • 2012-02-17
    • 1970-01-01
    • 2019-01-02
    • 1970-01-01
    • 1970-01-01
    • 2014-02-04
    • 1970-01-01
    相关资源
    最近更新 更多