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

标签 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 & pop 似乎更好。 push rax + pop rdx = 1 + 1 = 2 对比 sub rsp, 8 + add rsp, 8 = 4 + 4 = 8。所以前一对可以免费节省 6 个字节。

最佳答案

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

push/pop 将采用更少的融合域微指令,因此即使它们使用存储/加载端口也可能更高效。它们位于调用/返回对之间。

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

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

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

关于c - 为什么 llvm 和 gcc 在 x86 64 上使用不同的函数序言?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31537448/

相关文章:

c++ - 如何消除针对 c++17 功能的 gcc 警告?

c - 断言调用段错误

c++ - 关闭 AF_PACKET 与 AF_INET 的时间差?

c++ - 在 RaspberryPi 上使用 gcc 编译蓝牙问题

c++ - 为什么 std atomic 将 5 插入堆栈

linux - 使用 x64 汇编代码的基本输入

c - 从汇编逆向工程 C 源代码

c - 具有不同运行时间的几乎相同的代码 - 为什么?

c - 如何将指针数组引用到多个常量数组

GCC 中的 C++14 支持是实验性的