问题描述
考虑这个简单的代码:
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 rax
为 x
分配内存,因此内存在 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
将针对运行时而不是堆栈帧大小进行优化,因此所有这些选择都可能有充分的理由。