在这里的第一个答案中,提到了以下关于C中的堆栈内存的内容:
调用函数时,在堆栈顶部为局部变量和一些簿记数据保留一个块。
这在顶层是完全有意义的,并且让我好奇编译器在分配内存时有多聪明,考虑到这个问题的上下文:由于大括号本身不是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 int?1000*sizeof(MyObject)
或2000*sizeof(MyObject)
?一般来说:在调用某个函数之前,编译器在确定新堆栈帧需要多少内存时是否考虑了变量范围?如果这是编译器特定的,那么一些知名的编译器是如何做到的?
编译器将根据需要分配空间(通常为函数开头的所有项目),但不会为循环中的每次迭代分配空间。
例如,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次。因为在任何给定时间,这些变量中只有一个会存在。
(编译器可以为x
和第二个myObject[1000]
重用myObject[1000]
的空间-并且可能会为优化的构建这样做,但在这种情况下,它也会完全删除这些变量,因为它们没有被使用,所以它不会显示得很好)
在现代编译器中,函数首先被转换为流图。在流的每个弧中,编译器知道有多少变量是活动的——也就是说持有一个可见的值。其中一些将存在于寄存器中,对于其他变量,编译器需要保留堆栈空间。
随着优化器的进一步参与,事情变得更加复杂,因为它可能更喜欢不移动堆栈变量。这不是免费的。
尽管如此,编译器最终还是准备好了所有汇编操作,并且可以计算使用了多少唯一堆栈地址。