【问题标题】:Why does llvm and gcc use different function prologs on x86 64?为什么 llvm 和 gcc 在 x86 64 上使用不同的函数序言?
【发布时间】:2015-07-21 11:12:03
【问题描述】:

我用 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 = 2sub rsp, 8 + add rsp, 8 = 4 + 4 = 8。所以前一对可以免费节省 6 个字节。

【问题讨论】:

  • 这是一个选择问题。很难说哪个变种更好。可能这两种变体在性能方面都非常相似。
  • 回复:您的编辑。是的,ABI 保证在函数入口处,(%rsp + 8) 是 16B 对齐的。 (我将大部分评论编辑到我的答案中)。

标签: c gcc assembly llvm x86-64


【解决方案1】:

在 Intel 上,sub / add 将触发堆栈引擎插入一个额外的 uop 以同步 %rsp 用于管道的乱序执行部分。 (请参阅Agner Fog's microarch doc,特别是第 91 页,关于堆栈引擎。AFAIK,它在 Haswell 上的工作方式与在 Pentium M 上的工作方式相同,只要它需要插入额外的微指令。

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

所以,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 来掩盖地址或其他内容而崩溃。

【讨论】:

  • 是的,这只是为了保持堆栈对齐。
  • 还要注意,大多数时候函数会为局部变量分配一些空间,在这种情况下sub 变体更有效。据推测,编译器编写者只是没有针对不需要本地人的情况进行优化。
  • 是的,只有很少本地人的非叶函数是一种罕见的情况。我认为clang对push/pop不关心的数据的使用是一种巧妙的优化。
【解决方案2】:

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

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

【讨论】:

  • 什么样的实验?您是否正在测试 uop 吞吐量的瓶颈?我同意大多数时候可能没有区别。
  • 我做了最幼稚的事情:复制一条指令几千次(实际上是使用宏),把整个循环,然后运行。我不确定这是否是 uop 的瓶颈。你能确认一下吗?
  • add 使用相同的寄存器每次都需要前一个的输出作为输入,从而使延迟成为限制器。 add 在 SnB/IvB 上每个周期的吞吐量为 3,在 Haswell 上每个周期为 4,如果它们是独立的。 push 可以维持 1 个/周期,pop 2 个/周期。与现代 CPU 一样,重要的是上下文(其他 insn 正在竞争执行资源,以及它如何适应依赖链)。
猜你喜欢
  • 2020-11-11
  • 2011-04-16
  • 2011-11-19
  • 2019-04-26
  • 1970-01-01
  • 2017-03-05
  • 1970-01-01
  • 2012-12-13
  • 2014-10-07
相关资源
最近更新 更多