为什么 llvm 和 gcc 在 x86 64 上使用不同的函数序言?

Why does llvm and gcc use different function prologs on x86 64?

我用 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 作为函数序言,而 clang 使用 push rax/pop rdx

为什么编译器使用不同的函数序言?哪个变体更好? pushpop 当然编码为更短的指令,但它们比 addsub 快还是慢?

堆栈摆弄的第一个原因似乎是 abi 要求 rsp 为非叶程序对齐 16 字节。我还没有找到任何删除它们的编译器标志。

从你的回答来看,push & pop 似乎更好。 push rax + pop rdx = 1 + 1 = 2 对比 sub rsp, 8 + add rsp, 8 = 4 + 4 = 8。所以前一对可以免费节省 6 个字节。

根据我在我的机器上做的实验,push/popadd/sub的速度是一样的。我想所有现代计算机都应该如此。

无论如何,差异(如果有的话)真的是微观的,所以我建议你放心地假设它们是等价的......

在 Intel 上,sub / add 将触发堆栈引擎插入一个额外的 uop 以同步 %rsp 流水线的乱序执行部分。 (请参阅 Agner Fog's microarch doc,特别是第 91 页,关于堆栈引擎。据我所知,就何时需要插入额外的微处理器而言,它在 Haswell 上的工作方式与在 Pentium M 上的工作方式相同。

push / pop 将采用更少的融合域 uops,因此即使它们使用 store/load 端口也可能更高效。它们介于 call/ret 对之间。

所以,push / pop 至少不会变慢,但占用的指令字节更少。更好的 I-cache 密度是好的。

顺便说一句,我认为这对 insns 的要点是在 call 压入 8B return 地址后保持堆栈 16B 对齐。这是 ABI 最终需要半无用指令的一种情况。更复杂的函数需要一些堆栈 space 来溢出局部变量,然后在函数调用后重新加载它们,通常会 sub $something, %rsp 保留 space.

SystemV (Linux) amd64 ABI 保证在函数入口处,(%rsp + 8),其中堆栈上的参数(如果有的话)将按 16B 对齐。 (http://x86-64.org/documentation/abi.pdf)。你必须为你调用的任何函数安排这种情况,或者如果他们使用 SSE 对齐负载出现段错误,那是你的错。或者以其他方式因假设他们如何使用 AND 来掩盖地址或其他内容而崩溃。