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

6
此处的第一个答案中,提到了关于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个整数?
  • 除此之外,堆栈空间是否包含1000*sizeof(MyObject)或2000*sizeof(MyObject)?

总的来说,编译器在调用某个函数之前,会考虑变量作用域以确定需要多少内存来创建新的堆栈帧吗? 如果这是与编译器有关的,请问一些知名的编译器是如何处理的?


3
一对 {} 是一个作用域。 循环重复使用相同的内存来存储 x,同时两个 myObject 数组不会同时存在。 - LogicStuff
1
当可以重复使用相同的空间时,为什么需要为 100000 个整数分配空间?数组也是同样的道理。 - Algirdas Preidžius
1
编译器检查函数的每个作用域,并保留的空间是所有同时存在的作用域中占用空间最大的。 - Jabberwocky
堆栈空间是预先分配的,编译器只是在使用它直到用完并发生溢出。 - Jonathan Potter
2
同时,并不是所有关于C++的问题都只需要询问语言本身。询问编译器的实现细节,或者通常情况下编译器如何处理语言特性的一般原则,也是可以的。 - Angew is no longer proud of SO
显示剩余4条评论
2个回答

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] 的空间来存储第二个 myObject[1000] 以及 x。对于优化版本,编译器可能会这样做,但在这种情况下,编译器也会将这些未使用的变量完全删除,因此不会显示得很清楚。)

关于堆栈指针的问题:当达到g()时,它会增加max(2*sizeof(int), 1000*sizeof(MyObject))吗?因为只有这些变量可以同时存在。我认为从汇编代码中并不清楚。 - Jimmy
很可能是这样,但它可能是所有本地变量的总和 - 在非优化构建中几乎肯定是这样的[这就是我的代码所显示的]。 - Mats Petersson
当然,在优化的构建中,ix很可能存储在寄存器中而不是堆栈上。 - Mats Petersson

2
在现代编译器中,函数首先被转换为流程图。在流程的每个弧线中,编译器知道有多少变量是“活跃”的,也就是说它们保存着可见的值。其中一些变量将保存在寄存器中,对于其他变量,编译器将需要保留堆栈空间。
随着优化器的进一步介入,情况会变得更加复杂,因为它可能不希望移动堆栈变量。这不是免费的。
最终,编译器拥有所有汇编操作,并且可以计算使用了多少个唯一的堆栈地址。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接