为什么被调用者不首先使用调用者保存的寄存器?
why callees don't use caller saved registers first?
我们知道,按照x86-64的约定,寄存器%rbx
、%rbp
和%r12
–%r15
被归类为callee-saved寄存器。而 %r10
和 %r111
是调用者保存的寄存器。
但是在大多数情况下,当我编译 C 代码时,例如函数 P
调用 Q
,我看到函数 Q
的以下汇编代码:
Q:
push %rbx
movq %rdx, %rbx
...
popq %rbx
ret
我们知道,由于%rbx
是一个被调用者保存的寄存器,我们必须将它存储在堆栈中,并在稍后为调用者P
恢复它。
但使用调用者保存的寄存器 %r10
是否会更简洁并保存堆栈操作:
Q:
movq %rdx, %r10
...
ret
所以被调用者不需要担心为调用者保存和恢复寄存器,因为调用者在调用被调用者之前已经将它压入堆栈?
您似乎对“呼叫者已保存”的含义感到困惑。我认为这种错误的术语选择让您误以为编译器实际上 会 将它们保存在函数调用周围的调用者中。这通常会更慢 (),尤其是在进行多次调用或循环调用的函数中。
更好的术语是 call-clobbered vs. call-preserved,它反映了编译器实际如何使用它们,以及人们应该如何看待它们:在函数调用时死掉的寄存器,或者没有。 编译器不会 push/pop 在每个 call
.
周围注册调用破坏(又名调用者保存)
但是,如果您打算 push/pop 一个围绕单个函数调用的值,您只需使用 %rdx
即可。将其复制到 R10 只会浪费指令。所以 mov %r10
是没用的。再往后推,效率低下,不正确。
复制到调用保留寄存器的原因是函数 arg 将在函数稍后进行的函数调用中继续存在。显然,您必须为此使用保留调用的寄存器;被调用破坏的寄存器无法在函数调用后继续存在。
当不需要调用保留寄存器时,是的,编译器会选择调用破坏寄存器。
如果您将示例扩展到实际的 MCVE 而不是只显示没有源代码的 asm,这应该会更清楚。如果您编写一个需要 mov
来评估表达式的叶函数,或者在第一次函数调用后不需要其任何参数的非叶函数,您将不会看到它浪费指令保存和使用保留呼叫的注册。例如
int foo(int a) {
return (a>>2) + (a>>3) + (a>>4);
}
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
右移不能用 LEA 完成复制和操作,并且将相同的输入以 3 种不同的方式移动说服 GCC 使用 mov
来复制输入。 (而不是做一系列右移:编译器喜欢以更多指令为代价来最小化延迟,因为这通常最适合宽 OoO exec。)
我们知道,按照x86-64的约定,寄存器%rbx
、%rbp
和%r12
–%r15
被归类为callee-saved寄存器。而 %r10
和 %r111
是调用者保存的寄存器。
但是在大多数情况下,当我编译 C 代码时,例如函数 P
调用 Q
,我看到函数 Q
的以下汇编代码:
Q:
push %rbx
movq %rdx, %rbx
...
popq %rbx
ret
我们知道,由于%rbx
是一个被调用者保存的寄存器,我们必须将它存储在堆栈中,并在稍后为调用者P
恢复它。
但使用调用者保存的寄存器 %r10
是否会更简洁并保存堆栈操作:
Q:
movq %rdx, %r10
...
ret
所以被调用者不需要担心为调用者保存和恢复寄存器,因为调用者在调用被调用者之前已经将它压入堆栈?
您似乎对“呼叫者已保存”的含义感到困惑。我认为这种错误的术语选择让您误以为编译器实际上 会 将它们保存在函数调用周围的调用者中。这通常会更慢 (
更好的术语是 call-clobbered vs. call-preserved,它反映了编译器实际如何使用它们,以及人们应该如何看待它们:在函数调用时死掉的寄存器,或者没有。 编译器不会 push/pop 在每个 call
.
但是,如果您打算 push/pop 一个围绕单个函数调用的值,您只需使用 %rdx
即可。将其复制到 R10 只会浪费指令。所以 mov %r10
是没用的。再往后推,效率低下,不正确。
复制到调用保留寄存器的原因是函数 arg 将在函数稍后进行的函数调用中继续存在。显然,您必须为此使用保留调用的寄存器;被调用破坏的寄存器无法在函数调用后继续存在。
当不需要调用保留寄存器时,是的,编译器会选择调用破坏寄存器。
如果您将示例扩展到实际的 MCVE 而不是只显示没有源代码的 asm,这应该会更清楚。如果您编写一个需要 mov
来评估表达式的叶函数,或者在第一次函数调用后不需要其任何参数的非叶函数,您将不会看到它浪费指令保存和使用保留呼叫的注册。例如
int foo(int a) {
return (a>>2) + (a>>3) + (a>>4);
}
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
右移不能用 LEA 完成复制和操作,并且将相同的输入以 3 种不同的方式移动说服 GCC 使用 mov
来复制输入。 (而不是做一系列右移:编译器喜欢以更多指令为代价来最小化延迟,因为这通常最适合宽 OoO exec。)