为什么被调用者不首先使用调用者保存的寄存器?

amj*_*jad 0 c assembly x86-64 calling-convention compiler-optimization

我们知道,根据 x86-64 约定,寄存器%rbx%rbp%r12%r15被归类为被调用者保存的寄存器。While%r10%r111是调用者保存的寄存器。但是当我在大多数情况下编译 C 代码时,例如函数P调用Q,我看到以下函数的汇编代码Q

Q:
   push %rbx
   movq %rdx, %rbx
   ...
   popq %rbx
   ret
Run Code Online (Sandbox Code Playgroud)

我们知道,由于%rbx是一个被调用者保存的寄存器,我们必须将它存储在堆栈中,并在P以后为调用者恢复它。

但它不会更简洁并通过使用调用者保存的寄存器来保存堆栈操作%r10

Q:
   movq %rdx, %r10
   ...
   ret
Run Code Online (Sandbox Code Playgroud)

所以被调用者不需要担心保存和恢复调用者的寄存器,因为调用者在调用被调用者之前已经把它推到了堆栈?

Pet*_*des 5

您似乎对“来电者已保存”的含义感到困惑。我认为这个错误的术语选择让你误以为编译器实际上会将它们保存在函数调用的调用者中。这通常会更慢(为什么编译器坚持在这里使用被调用者保存的寄存器?),尤其是在进行多次调用或循环调用的函数中。

更好的术语是call-clobbered 与 call-preserved,它反映了编译器实际如何使用它们,以及人们应该如何看待它们:在函数调用中死掉的寄存器,或者不死的寄存器。 编译器不会在每个call.

但是,如果您要围绕单个函数调用推送/弹出一个值,则只需使用%rdx. 将它复制到 R10 只会浪费指令。所以mov %r10没用。稍后推送它只是低效,没有它是不正确的。


复制到调用保留寄存器的原因是函数 arg 将在函数稍后进行的函数调用中幸存下来。显然,您必须为此使用保留调用的寄存器;调用破坏的寄存器无法在函数调用中存活。

当不需要调用保留寄存器时,是的编译器会选择调用破坏寄存器。

如果您将示例扩展到实际的 MCVE 而不是仅显示没有源的 asm,这应该更清楚。如果您编写了一个需要 amov来计算表达式的叶函数,或者在第一次函数调用后不需要其任何参数的非叶函数,您将不会看到它浪费指令保存和使用调用保留注册。例如

int foo(int a) {
    return (a>>2) + (a>>3) + (a>>4);
}
Run Code Online (Sandbox Code Playgroud)

https://godbolt.org/z/ceM4dP带有 GCC 和 clang -O3:

# gcc10.2
foo(int):
        mov     eax, edi
        mov     edx, edi      # using EDX, a call-clobbered register
        sar     edi, 4
        sar     eax, 2
        sar     edx, 3
        add     eax, edx
        add     eax, edi
        ret
Run Code Online (Sandbox Code Playgroud)

使用 LEA 无法右移来复制和操作,并且将相同的输入移 3 种不同的方式来说服 GCCmov用于复制输入。(而不是做一连串的右移:编译器喜欢以牺牲更多指令为代价来最小化延迟,因为这通常最适合广泛的 OoO exec。)

  • @amjad:因为你的“...”包含一个函数调用,而重点是在该函数调用中保留值。 (3认同)
  • 我同意彼得的观点,“调用者保存”是一个非常糟糕的术语。正确的术语是“call-saved”(又名“callee-saved”)和“call-clobbered”,这些名称只是描述了围绕调用的 ABI 契约,而不是调用者或被调用者必须执行的操作。 (2认同)