【问题标题】:C++: How does the compiler know how much memory to allocate for each stack frame?C++:编译器如何知道为每个堆栈帧分配多少内存?
【发布时间】:2016-07-07 20:35:41
【问题描述】:

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

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

【问题讨论】:

  • 一对{} 是一个作用域。循环对x重复使用相同的内存,两个myObject数组不会同时存在。
  • 为什么要为100000 ints分配空间,当它可以重用相同的空间时?数组也是如此。
  • 编译器检查函数的每个作用域,保留的空间是所有作用域中可以同时存在的最大空间。
  • 堆栈空间是预先分配的,编译器只是使用它,直到它用完并且你有溢出。
  • @n.m.同时,并非所有关于 C++ 的问题都只需要询问语言。询问编译器的实现细节,或者只是询问编译器通常如何处理语言特性的一般原则,也可以。

标签: c++ memory memory-management


【解决方案1】:

编译器将根据需要分配空间(通常为函数开头的所有项),但不会为循环中的每次迭代分配空间。

例如,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] - 并且可能会为优化构建这样做,但在这种情况下,它也会完全删除这些变量,因为它们不是用过,所以显示不太好)

【讨论】:

  • 就堆栈指针而言:在达到g() 时它会增加max(2*sizeof(int), 1000*sizeof(MyObject)) 吗?因为只有那些变量可以同时存在。我认为大会并不清楚这一点。
  • 很可能,是的,但它可能是所有局部变量的总和 - 几乎可以肯定是在未优化的构建中[这是我的代码显示的]
  • 当然,在优化的构建中,ix 很可能驻留在寄存器中而不是堆栈中。
【解决方案2】:

在现代编译器中,函数首先被转换为流程图。在流程的每一条弧线中,编译器都知道有多少变量是live——也就是说,它拥有一个可见的值。其中一些将存在于寄存器中,而对于其他一些,编译器将需要保留堆栈空间。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-26
    • 1970-01-01
    • 2018-04-10
    • 2015-05-01
    • 2010-11-25
    • 1970-01-01
    • 2019-12-07
    • 2015-12-18
    相关资源
    最近更新 更多