什么时候堆栈分配

即使在C(不仅仅是C ++)中,您也可以在代码块的开头声明变量,该代码块用大括号括起来。

例:

#include  void use_stack(int cnt) { if (cnt<=16) { int a[16]; int i; a[0]=3; a[1]=5; for (i=2;i<cnt;i++) { a[i]=a[i-1]+a[i-2]; } printf("a[%d] == %d\n",cnt-1,a[cnt-1]); } else { printf("cnt is too big\n"); } } 

现在我知道像数组a[16]这样的变量在这种情况下被分配在堆栈上。

我想知道这个数组的空间是在函数的开头(第一个打开花括号)还是在声明它的块的开头分配(在if之后打开花括号)。

从检查汇编代码开始,编译器似乎直接在函数入口处为a[16]分配空间。

我实际上期望在a[16]的声明处分配堆栈(堆栈指针减少),并且堆栈将在相应的if代码块的末尾被解除分配(堆栈指针增加)。

但是这似乎没有发生(即使在a[16]分支中没有使用a[16]a[16]堆栈直接在函数入口处分配)。

有没有人解释为什么会这样?

那么是否有C语言标准的任何部分,它解释了这种行为,或者是否与“longjmp”或信号处理有关,这可能要求堆栈指针在函数内部是“常量”?

注意:我假设堆栈的原因是在代码块的开头/结尾分配/解除分配,因为在C ++中,在堆栈上分配的对象的构造函数/析构函数将在代码块的开头/结尾处调用。 因此,如果您检查C ++程序的汇编代码,您会注意到堆栈仍然在函数入口处分配; 只是构造函数/析构函数调用将在代码块的开始/结束时完成。

我明确感兴趣的是为什么堆栈没有使用花括号在代码块的开头/结尾分配/解除分配。

问题: 在什么时刻是本地变量分配存储? 仅涉及在函数开始时分配的局部变量。 我很惊讶稍后在代码块内分配的变量的堆栈分配也在函数入口处完成。

到目前为止,答案是:

  • 与优化有关
  • C,C ++可能有所不同
  • 在C语言规范中甚至没有提到堆栈

所以:我对C的答案感兴趣…(我坚信答案也适用于C ++,但我不是在问C ++ :-))。

优化:这是一个例子,它将直接说明为什么我如此惊讶,以及为什么我很确定这不是一个优化:

 #include  static char *stackA; static char *stackB; static void calc(int c,int *array) { int result; if (c<=0) { // base case c0 if (array==NULL) { // no array allocated, so allocate it now int i; int a[2500]; // calculate array entries recursively calc(c-1,a); // calculate current array entry a[c] a[c]=a[c-1]+3; // print full array for(i=0;i<=c;i++) { printf("a[%d] = %d\n",i,a[i]); } } else { // array already allocated calc(c-1,array); // calculate current array entry a[c] array[c]=array[c-1]+3; } } int main() { int a; stackA=(char *)(&a); printf("stack ptr main() = %p\n",stackA); calc(9,NULL); printf("used stack = %d\n",(int)(stackA-stackB)); } 

我知道这是一个丑陋的程序:-)。

函数calc以递归方式计算所有0<=n<=c n*3 + 1

如果你看一下calc的代码,你会注意到只有当函数的输入参数arrayNULL时才会声明数组a[2500]

现在这只发生在对main进行的calc调用中。

stackAstackB指针用于计算该程序使用多少堆栈的粗略估计。

现在: int a[2500]应该消耗大约10000个字节(每个整数4个字节,2500个条目)。 所以你可以期望整个程序消耗大约10000字节的堆栈+额外的东西(对于递归调用calc开销)。

但是:事实certificate,这个程序消耗大约100000字节的堆栈(是预期的10倍)。 原因是,对于每次调用calc ,分配数组a[2500] ,即使它仅用于第一次调用。 有10次调用calc0<=c<=9 ),所以你消耗了100000字节的堆栈。

  • 无论是否使用优化编译程序都没关系
  • GCC-4.8.4和clang for x64,MS Visual C ++ 2010,Windriver for DIAB(for PowerPC) 表现出这种行为

甚至更奇怪:C99引入了可变长度arrays。 如果我替换int a[2500]; 在上面的代码中用int a[2500+c]; 然后程序使用更少的堆栈空间(减少大约90000字节)。

注意:如果我main中的calc(1000,NULL);调用更改为calc(1000,NULL); 程序将崩溃(堆栈溢出==分段错误)。 如果我另外改为int a[2500+c]; 该程序工作,使用不到100KB的堆栈。 我仍然希望看到一个答案,这解释了为什么可变长度数组不会导致堆栈溢出,而固定长度数组确实导致堆栈溢出,特别是因为这个固定长度数组超出了范围(除了第一次调用calc )。

那么在C中出现这种行为的原因是什么?

我不相信GCC / clang都不能做得更好; 我坚信必须有技术上的理由。 有任何想法吗 ?

由Google回答

经过更多的谷歌搜索:我坚信这与“setjmp / longjmp”行为有关。 谷歌为“可变长度arrayslongjmp”,并亲自看看。 如果你没有在函数入口分配所有数组,似乎很难实现longjmp。

自动存储的语言规则仅保证最后分配的是第一个被释放的。

编译器可以以它认为合适的方式实现此逻辑堆栈

如果它可以certificate函数不是递归的,它甚至可以在程序启动时分配存储。


我相信以上内容适用于C和C ++,但我不是C专家。

当您询问编程语言的详细信息时,请一次将问题限制为一种语言。

除了编译器制造商的选择之外,没有其他技术原因。 它生成的代码更少,执行代码更快,始终保留函数开头所需的所有堆栈空间。 所以所有的编译器都做出了相同的合理性能权衡。

尝试使用可变长度数组,您将看到编译器完全能够生成仅为块“分配”堆栈的代码。 像这样的东西:

 void foo(int sz, int x) { extern void bar(char *); if (x) { char a[sz]; bar(a); } else { char a[10]; bar(a); } } 

我的编译器生成的代码始终为x为false部分保留堆栈空间,但只有x为真时才保留true部分的空间。

如何做到这一点不受任何标准的约束。 C和C ++标准根本没有提到堆栈,理论上这些语言甚至可以在没有堆栈的计算机上使用。

在具有堆栈的计算机上,如何完成此操作由给定系统的ABI指定。 通常,在程序进入函数时保留堆栈空间。 但编译器可以自由地优化代码,这样只有在使用某个变量时才会保留堆栈空间。

无论如何,声明变量的点与分配时间无关。 在您的示例中, int a[16]在输入函数时分配,或者在使用函数的第一个位置之前分配。 如果在if语句中或函数顶部声明a并不重要。

但是,在C ++中,有构造函数的概念。 如果您的变量是带有构造函数的对象,那么该构造函数将在声明变量的位置执行。 这意味着必须在该变量发生之前分配变量。

Alf解释了标准规定的限制和自由(或者更确切地说,它未指定的内容)。

我会建议回答这个问题

为什么堆栈没有使用花括号在代码块的开头/结尾分配/解除分配。

你测试的编译器选择这样做(我实际上只是猜测,我没有写任何一个),因为更好的运行时性能和简单性(当然,因为它是允许的)。

分配96个字节(任意示例)一次所花费的时间大约是分配48个字节两次的一半。 第三次分配32个字节三次。

将循环视为极端情况:

 for(int i = 0; i < 1000000; i++) { int j; } 

如果在函数的开头分配j ,则有一个分配。 如果j在循环体内分配,那么将有一百万个分配。 分配越少越好(更快)。

注意:我假设堆栈的原因是在代码块的开头/结尾分配/解除分配,因为在C ++中,在堆栈上分配的对象的构造函数/析构函数将在代码块的开头/结尾处调用。

现在你知道假装你错了。 如链接问题中的精细答案所述,分配/处理不一定与构造/破坏一致。


想象一下,我会使用a[100000]

这接近总堆栈空间的非常大的一部分。 您应该动态分配大块内存。