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

时间:2015-07-21 11:12:03

标签: c gcc assembly llvm 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作为prolog函数,clang使用push rax / pop rdx

为什么编译器使用不同的函数序言?哪种变体更好? pushpop肯定会编码为较短的说明,但它们是否比addsub更快或更慢?

首先出现堆栈的原因似乎是abi要求rsp与非叶子过程对齐16字节。我无法找到任何删除它们的编译器标志。

从你的答案来看,它似乎是推动和放大流行更好。 push rax + pop rdx = 1 + 1 = 2sub rsp, 8 + add rsp, 8 = 4 + 4 = 8。所以前一对节省了6个字节。

2 个答案:

答案 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密度很好。

顺便说一下,我认为这对insn的目的是在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相同。我想现在所有的现代计算机都应该如此。

无论如何,差异(如果有的话)实际上是微观的,所以我建议你安全地假设它们是等价的......