如果函数在同时定义变量时调用自身 会导致堆栈溢出吗? gcc中是否有任何选项可以重用相同的堆栈。
void funcnew(void)
{
int a=10;
int b=20;
funcnew();
return ;
}
函数可以重用之前使用的堆栈帧吗? gcc中有什么选项可以在尾递归中重用相同的帧?
答案 0 :(得分:5)
是。参见
-foptimize同胞通话
优化兄弟和尾部递归调用。
在-O2,-O3,-Os等级启用。
您的功能编译为:
funcstack:
.LFB0:
.cfi_startproc
xorl %eax, %eax
jmp func
.cfi_endproc
(注意跳转到func)
当函数通过调用结束时重用堆栈帧 - 这包括完全通用性操纵堆栈以将参数放在正确的位置并通过跳转到函数的开头来替换函数调用 - 是众所周知的优化称为[i]尾部呼叫删除[/ i]。一些语言(例如方案)强制要求递归调用(递归调用是在这些语言中表达循环的自然方式)。如上所述,gcc已经为C实现了优化,但是我不确定哪个编译器有它,我不会依赖它来获取可移植代码。请注意,我不知道它有哪些限制 - 我不确定gcc是否会在参数类型不同的情况下操纵堆栈。
答案 1 :(得分:3)
即使没有定义参数,您也会得到堆栈溢出。由于返回地址也被压入堆栈。
最近我学到了这一点可能是编译器将你的循环优化为尾递归(这使得堆栈根本不会增长)。 Link to tail recursion question on SO
答案 2 :(得分:0)
不,每次递归都是一个新的堆栈帧。如果递归是无限深的,那么所需的堆栈也是无限的,所以你得到堆栈溢出。
答案 3 :(得分:0)
是的,在某些情况下,编译器可能能够执行称为尾部调用优化的操作。您应该查看编译器手册。 (AProgrammer似乎在他的回答中引用了GCC手册。)
当实现例如频繁出现此类代码的函数式语言时,这是一个必不可少的优化。
答案 4 :(得分:0)
您可以完全取消堆栈帧,因为返回地址需要它。除非您使用尾递归,并且您的编译器已将其优化为循环。但是要在技术上完全诚实,你可以通过使它们静态来消除框架中的所有变量。然而,这几乎肯定是不你想做什么,如果你不确切知道自己在做什么,就不应该这样做,因为你不得不问这个问题,你没有。
答案 5 :(得分:0)
正如其他人所指出的那样,只有(1)您的编译器支持尾调用优化,以及(2)您的函数是否有资格进行此类优化时,才有可能。优化是重用现有堆栈并执行JMP(即汇编中的GOTO)而不是CALL。
实际上,您的示例函数确实有资格进行此类优化。原因是你的函数在返回之前做的最后一件事就是调用它自己; 最后一次调用funcnew()
后,它不需要做任何事情。但是,只有某些编译器才会执行此类优化。例如,海湾合作委员会将这样做。有关详细信息,请参阅Which, if any, C++ compilers do tail-recursion optimization?
关于此的经典材料是阶乘函数。让我们做一个递归因子函数,不符合尾调用优化(TCO)。
int fact(int n)
{
if ( n == 1 ) return 1;
return n*fact(n-1);
}
它做的最后一件事是将n
乘以fact(n-1)
的结果。通过某种方式消除最后的操作,我们将能够重用堆栈。让我们介绍一个累加器变量,它将为我们计算答案:
int fact_helper(int n, int acc)
{
if ( n == 1 ) return acc;
return fact_helper(n-1, n*acc);
}
int fact_acc(int n)
{
return fact_helper(n, 1);
}
函数fact_helper
完成工作,而fact_acc
只是初始化累加器变量的便利函数。
注意 last 事物fact_helper
的作用是如何调用自身。通过重用现有的变量堆栈,可以将此CALL转换为JMP。
使用GCC,您可以通过查看生成的程序集来验证它是否已针对跳转进行了优化,例如gcc -c -O3 -Wa,-a,-ad fact.c
:
...
37 L12:
38 0040 0FAFC2 imull %edx, %eax
39 0043 83EA01 subl $1, %edx
40 0046 83FA01 cmpl $1, %edx
41 0049 75F5 jne L12
...
某些编程语言(例如Scheme)实际上将保证正确的实现将执行此类优化。他们甚至会为非递归尾调用。