迷人的兔子洞,我的分析已经改了三遍了。
看来这确实是一个错过的优化。玩了一会儿,我发现了另一个错过的优化,这次是 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, rsp
and rsp, -32
sub rsp, 64
mov rdi, rsp
call X::X() [complete object constructor]
mov rsp, rbp
pop rbp
ret
如果不实际测量性能,很难判断哪个更好 -- -O2 将针对运行时进行优化,而不是针对堆栈帧大小进行优化,因此所有这些选择都可能有充分的理由。