void foo() {
if (runtimeCondition) {
int x = 0;
} else {
SuperLargeObject y;
}
}
当编译器为此函数分配堆栈帧的内存时,它是否只会分配足够存储SuperLargeObject
的内存,如果导致int
的条件为真,则多余的内存将不会被使用?还是会以其他方式分配内存?
void foo() {
if (runtimeCondition) {
int x = 0;
} else {
SuperLargeObject y;
}
}
当编译器为此函数分配堆栈帧的内存时,它是否只会分配足够存储SuperLargeObject
的内存,如果导致int
的条件为真,则多余的内存将不会被使用?还是会以其他方式分配内存?
这取决于您的编译器和优化设置。在未经优化的构建中,大多数C++编译器可能会为两个对象分配堆栈内存,并根据采取哪个分支来使用其中一个。在经过优化的构建中,情况变得更加有趣:
如果两个对象(int
和SuperLargeObject
)都没有被使用,并且编译器可以证明构造SuperLargeObject
没有副作用,则两个分配都将被省略。
如果对象逃逸函数,即它们的地址被传递到另一个函数中,编译器必须为它们提供内存。但由于它们的生命周期不重叠,它们可以存储在重叠的内存区域中。实际上是否发生这种情况取决于编译器。
正如您在这里看到的那样, 不同的编译器为这两个函数生成不同的汇编代码:(修改自原始帖子和参考文献,均编译为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);
}
call
指令会为返回地址推送8个字节(感谢@PeterCordes在另一篇文章中向我解释了这一点)。
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
的大小。
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
cmov
指令。
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
中测试条件。
在函数中声明的所有内存都可以假定在函数进入时一次性分配完毕。良好的 C 编译器会合并具有非重叠生命周期的对象的存储。
如果您在某些特定代码路径中有一个大型对象,而希望避免在不采用该路径时分配它,则必须执行以下操作之一:
在该代码路径中动态分配并释放它。
使用 C99 变长数组进行分配。
使用 alloca
函数/运算符进行分配,这是许多编译器中存在的传统扩展。
将代码移动到单独的辅助函数中。但是,如果该函数被内联,则不会有任何区别!由内联函数产生的栈分配会合并为一个大栈帧,就像代码是内联编写的一样。请确保使用特定于编译器的魔术代码声明此函数不被内联。
#ifdef __GNUC__
#define NOINLINE __attribute__((noinline))
#else
#error port me
#endif
NOINLINE void foo_LargeObjectCase()
{
SuperLargeObject y;
}
void foo() {
if (runtimeCondition) {
int x = 0;
} else {
foo_SuperLargeObjectCase();
}
}
我在TXR Lisp虚拟机中使用了上述最后一种方法。该虚拟机为各种指令分派函数。其中一些函数具有更多的堆栈存储,而另一些则较少。我已将其中许多函数声明为notinline
,这对观察到的堆栈帧大小产生了巨大影响。
当然,将代码移入函数可能会不方便;您可能需要传递所有参数,甚至还需要传递一些额外的参数,如果代码需要访问foo
的某些局部变量。
如果您关心函数的堆栈使用情况,gcc
提供了有用的诊断工具。您可以使用-fstack-usage
获取有关函数堆栈使用情况的信息,或者使用-Wstack-usage=N
警告,如果某个函数的堆栈使用超过N
字节,则会发出此警告。
真实故事:-fstack-usage
帮助我发现,使用GNU C库中的crypt_r
函数的一个函数具有超过128千字节的堆栈帧。这就是struct crypt_data
上下文缓冲区的大小!我将该代码切换到malloc
和free
结构。
extern void consume(void*);
并使用任何你不想被优化掉的东西的地址来调用它。https://godbolt.org/z/9Gr5549vq - Raymond Chen