在第一个答案here中,提到了C ++中的堆栈内存:
调用函数时,会在堆栈顶部为区域变量和一些簿记数据保留一个块。
这在顶层是非常有意义的,并且让我对在this question的上下文中使用自身分配这个内存时的智能编译器感到好奇:由于大括号本身不是堆栈帧C(我假设这也适用于C ++),我想检查编译器是否在单个函数内基于变量作用域优化保留内存。
在下面我假设在函数调用之前堆栈看起来像这样:
--------
|main()|
-------- <- stack pointer: space above it is used for current scope
| |
| |
| |
| |
--------
然后在调用函数f()
之后执行以下操作:
--------
|main()|
-------- <- old stack pointer (osp)
| f() |
-------- <- stack pointer, variables will now be placed between here and osp upon reaching their declarations
| |
| |
| |
| |
--------
例如,给定此功能
void f() {
int x = 0;
int y = 5;
int z = x + y;
}
据推测,这只会为记账分配3*sizeof(int)
+一些额外的开销。
然而,这个功能怎么样:
void g() {
for (int i = 0; i < 100000; i++) {
int x = 0;
}
{
MyObject myObject[1000];
}
{
MyObject myObject[1000];
}
}
忽略编译器优化可能会忽略上面的很多内容,因为它们实际上什么都不做,我对第二个例子中的以下内容感到好奇:
for
循环:堆栈空间是否足够大以适应所有100000个整数?1000*sizeof(MyObject)
或2000*sizeof(MyObject)
?一般情况下:在调用某个函数之前,在确定新堆栈帧需要多少内存时,编译器是否会考虑变量范围?如果这是特定于编译器的,那么一些着名的编译器如何做呢?
答案 0 :(得分:4)
编译器将根据需要分配空间(通常用于函数开头的所有项),但不会为循环中的每次迭代分配空间。
例如,Clang生成的内容为LLVM-IR
define void @_Z1gv() #0 {
%i = alloca i32, align 4
%x = alloca i32, align 4
%myObject = alloca [1000 x %class.MyObject], align 16
%myObject1 = alloca [1000 x %class.MyObject], align 16
store i32 0, i32* %i, align 4
br label %1
; <label>:1: ; preds = %5, %0
%2 = load i32, i32* %i, align 4
%3 = icmp slt i32 %2, 100000
br i1 %3, label %4, label %8
; <label>:4: ; preds = %1
store i32 0, i32* %x, align 4
br label %5
; <label>:5: ; preds = %4
%6 = load i32, i32* %i, align 4
%7 = add nsw i32 %6, 1
store i32 %7, i32* %i, align 4
br label %1
; <label>:8: ; preds = %1
ret void
}
这是以下结果:
class MyObject
{
public:
int x, y;
};
void g() {
for (int i = 0; i < 100000; i++)
{
int x = 0;
}
{
MyObject myObject[1000];
}
{
MyObject myObject[1000];
}
}
因此,正如您所看到的,x
仅分配一次,而不是100000次。因为在任何给定时间只存在其中一个变量。
(编译器可以为myObject[1000]
和第二个x
重用myObject[1000]
的空间 - 并且可能会为优化的构建重用该空间,但在这种情况下它也会完全删除这些变量因为它们没有被使用,所以它不会很好地显示出来)
答案 1 :(得分:2)
在现代编译器中,该函数首先转换为流程图。在流程的每个弧中,编译器知道有多少变量 live - 也就是说保持可见值。其中一些将存在于寄存器中,而对于其他寄存器,编译器将需要保留堆栈空间。
随着优化器的进一步参与,事情会变得更复杂,因为它可能不希望移动堆栈变量。这不是免费的。
最后,编译器最终准备好了所有的汇编操作,并且可以计算使用了多少个唯一的堆栈地址。