GCC 的堆栈框架太大过度对齐?但不是 Clang

问题描述

考虑这个简单的代码

class X {
  int i_;
public:
  X();
};

void f() {
  X x;
}

f 的栈帧是 32 字节长的 GCC,这是不必要的长。返回地址和 x 只需要 12 字节,根据 Linux/x86_64 ABI 应该需要 16 字节对齐。使用 Clang,只分配了 16 个字节。为什么 GCC 需要这么多堆栈空间?

GCC 程序集:

f():
  sub   rsp,24
  lea   rdi,[rsp+12]
  call  X::X()
  add   rsp,24
  ret

Clang 程序集:

f():       
  push  rax
  mov   rdi,rsp
  call  X::X()
  pop   rax
  ret

两者都带有 -O2。现场演示:https://godbolt.org/z/bcrWW36on

解决方法

迷人的兔子洞,我已经改变了三遍分析。

看来这确实是一个遗漏的优化。在玩了一会儿之后,我发现了另一个错过的优化,这次是在 clang 中:

如果您实际上是 use the x object,那么 Clang 使用 rbx 来缓存 x 的地址而不是重新计算它,这意味着它需要在整个函数中保存 rbx,它将堆栈帧中的已用空间扩展了 8(从 12 到 20),将对齐的堆栈帧碰撞到 32,与 gcc 相同。

从调试的角度来看,我更喜欢 clang 使用 sub rsp,8 而不是 push raxx 分配内存,因此内存在 valgrind 中没有标记为已初始化。

GCC 程序集:

f():
    sub     rsp,24
    lea     rdi,[rsp+12]
    call    X::X() [complete object constructor]
    lea     rdi,[rsp+12]
    call    g(X&)
    add     rsp,24
    ret

Clang 程序集:

f():
    push    rbx
    sub     rsp,16
    lea     rbx,[rsp + 8]
    mov     rdi,rbx
    call    X::X() [complete object constructor]
    mov     rdi,rbx
    call    g(X&)
    add     rsp,16
    pop     rbx
    ret

我通过using a 32 byte vector as a data member检查了gcc是否可能使用32字节堆栈对齐,gcc和clang都生成代码来对齐这里的堆栈指针,并使用基指针来实现可变长度堆栈帧。不过,我不知道为什么 Clang 会为这里的对象分配 64 个字节。

GCC 程序集:

f():
    push    rbp
    mov     rbp,rsp
    and     rsp,-32
    sub     rsp,32
    mov     rdi,rsp
    call    X::X() [complete object constructor]
    leave
    ret

Clang 程序集:

f():                                  # @f()
    push    rbp
    mov     rbp,64
    mov     rdi,rsp
    call    X::X() [complete object constructor]
    mov     rsp,rbp
    pop     rbp
    ret

如果不实际测量性能,就很难判断哪个更好——-O2 将针对运行时而不是堆栈帧大小进行优化,因此所有这些选择都可能有充分的理由。