提问者:小点点

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


在这里的第一个答案中,提到了以下关于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)

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


共2个答案

匿名用户

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

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

匿名用户

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

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

尽管如此,编译器最终还是准备好了所有汇编操作,并且可以计算使用了多少唯一堆栈地址。

相关问题