C ++:编译器如何知道为每个堆栈帧分配多少内存?

时间:2016-03-21 11:01:31

标签: c++ memory memory-management

在第一个答案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)

一般情况下:在调用某个函数之前,在确定新堆栈帧需要多少内存时,编译器是否会考虑变量范围?如果这是特定于编译器的,那么一些着名的编译器如何做呢?

2 个答案:

答案 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 - 也就是说保持可见值。其中一些将存在于寄存器中,而对于其他寄存器,编译器将需要保留堆栈空间。

随着优化器的进一步参与,事情会变得更复杂,因为它可能不希望移动堆栈变量。这不是免费的。

最后,编译器最终准备好了所有的汇编操作,并且可以计算使用了多少个唯一的堆栈地址。