我用gcc和clang编译的一个简单的函数:
void test() {
printf("hm");
printf("hum");
}
$ gcc test.c -fomit-frame-pointer -masm=intel -O3 -S
sub rsp, 8
.cfi_def_cfa_offset 16
mov esi, OFFSET FLAT:.LC0
mov edi, 1
xor eax, eax
call __printf_chk
mov esi, OFFSET FLAT:.LC1
mov edi, 1
xor eax, eax
add rsp, 8
.cfi_def_cfa_offset 8
jmp __printf_chk
和
$ clang test.c -mllvm --x86-asm-syntax=intel -fomit-frame-pointer -O3 -S
# BB#0:
push rax
.Ltmp1:
.cfi_def_cfa_offset 16
mov edi, .L.str
xor eax, eax
call printf
mov edi, .L.str1
xor eax, eax
pop rdx
jmp printf # TAILCALL
我感兴趣的区别在于gcc使用sub rsp, 8
/ add rsp, 8
作为prolog函数,clang使用push rax
/ pop rdx
。
为什么编译器使用不同的函数序言?哪种变体更好? push
和pop
肯定会编码为较短的说明,但它们是否比add
和sub
更快或更慢?
首先出现堆栈的原因似乎是abi要求rsp与非叶子过程对齐16字节。我无法找到任何删除它们的编译器标志。
从你的答案来看,它似乎是推动和放大流行更好。 push rax + pop rdx = 1 + 1 = 2
与sub rsp, 8 + add rsp, 8 = 4 + 4 = 8
。所以前一对节省了6个字节。
答案 0 :(得分:9)
在Intel上,sub
/ add
将触发堆栈引擎插入额外的uop,以便为管道的无序执行部分同步%rsp
。 (参见Agner Fog's microarch doc,特别是第91页,关于堆栈引擎.AFAIK,它在Haswell和Pentium M上的工作方式相同,只要它需要插入额外的uops。
push
/ pop
将占用更少的融合域uop,因此即使它们使用存储/加载端口也可能更有效。它们介于呼叫/转换对之间。
因此,push
/ pop
至少不会慢,但占用的指令字节更少。更好的I-cache密度很好。
call
推送8B返回地址后保持堆栈16B对齐。这是ABI最终需要半无用指令的一种情况。更复杂的函数需要一些堆栈空间来溢出本地,然后在函数调用后重新加载它们,通常会sub $something, %rsp
来保留空间。
SystemV(Linux)amd64 ABI保证在函数入口(%rsp + 8)
,堆栈中的args(如果有的话)将是16B对齐的。 (http://x86-64.org/documentation/abi.pdf)。你必须安排你所使用的任何功能的情况,或者如果他们使用SSE对齐的负载进行分段,则是你的错。或者因为假设他们如何使用AND
掩盖地址或其他东西而崩溃。
答案 1 :(得分:1)
根据我在计算机上进行的实验,push/pop
的速度与add/sub
相同。我想现在所有的现代计算机都应该如此。
无论如何,差异(如果有的话)实际上是微观的,所以我建议你安全地假设它们是等价的......