这取决于您的编译器和优化设置。在未优化的构建中,大多数 C++ 编译器可能会为两个对象分配堆栈内存,并根据采用的分支使用其中之一。在优化的构建中,事情变得更加有趣:
如果两个对象(int
和SuperLargeObject
没有被使用,编译器可以证明构造SuperLargeObject
没有副作用,两个分配都将被省略。
如果对象逃逸该函数,即它们的地址被传递给另一个函数,则编译器必须为它们提供内存。但由于它们的生命周期不重叠,因此它们可以存储在重叠的内存区域中。这是否真的发生取决于编译器。
正如你在这里看到的 https://godbolt.org/z/4Efn19s69,不同的编译器为这两个函数生成不同的程序集:(来自OP和参考的修改示例,全部针对x86-64编译)
void escape(void const*);
struct SuperLargeObject {
char data[104];
};
void f(bool cond) {
if (cond) {
int x;
escape(&x);
}
else {
SuperLargeObject y;
escape(&y);
}
}
void g() {
SuperLargeObject y;
escape(&y);
}
请注意,所有堆栈分配都是 8 的奇数倍,因为 x86-64 ABI 要求堆栈指针按 16 字节对齐,并且 8 字节由call
返回地址的说明(感谢@PeterCordes 在 上向我解释这一点另一个帖子 https://stackoverflow.com/questions/76291937/size-of-stack-allocations-produced-by-llvms-x86-64-backend?noredirect=1#comment134535786_76291937).
ICC
f(bool):
sub rsp, 120
test dil, dil
lea rax, QWORD PTR [104+rsp]
lea rdx, QWORD PTR [rsp]
cmovne rdx, rax
mov rdi, rdx
call escape(void const*)
add rsp, 120
ret
g():
sub rsp, 104
lea rdi, QWORD PTR [rsp]
call escape(void const*)
add rsp, 104
ret
ICC 似乎分配了足够的内存来存储两个对象,然后根据运行时条件在两个非重叠区域之间进行选择(使用cmov
) 并将选定的指针传递给转义函数。
在参考函数中g
它只分配 104 字节,正好是SuperBigObject
.
GCC
f(bool):
sub rsp, 120
mov rdi, rsp
call escape(void const*)
add rsp, 120
ret
g():
sub rsp, 120
mov rdi, rsp
call escape(void const*)
add rsp, 120
ret
GCC 也分配 120 字节,但它将两个对象放置在同一地址,因此不会发出任何信号cmov
操作说明。
Clang
f(bool):
sub rsp, 104
test edi, edi
mov rdi, rsp
call escape(void const*)@PLT
add rsp, 104
ret
g():
sub rsp, 104
mov rdi, rsp
call escape(void const*)@PLT
add rsp, 104
ret
Clang 还合并了两个分配,并将分配大小减少到必要的 104 字节。
不幸的是我不明白为什么它测试功能中的条件f
.
您还应该注意,当编译器可以将一个或两个变量放入寄存器中时,根本不会分配内存,即使它们在整个函数中使用和重新分配也是如此。为了int
's and long
和其他小对象是最常见的情况,如果它们的地址不转义该函数。