递归的风险
实际开发中应避免使用递归,原因主要两点:
1. 递归调用在深度上不可预测,层数过多不断压栈,可能会引起栈溢出的崩溃;
2. 不容易理解;
栈溢出
stack overflow异常是程序中常常会碰到的,原因是调用线程栈不够用。windows默认栈大小是1M,使用栈空间超过了1M就会报出stack overflow异常。
产生原因
1、死循环
出现了死循环,例如:递归函数没有出口,不管栈空间多大必定溢出。
long func(int n)
{
return n*func(n-1);
}
这是个没有出口的递归函数, 必然引起栈溢出。纠正:
long func(int n)
{
if(n==1) return 1;
return n*func(n-1);
}
2、栈确实不够用
存到栈上的主要内容是:局部变量和函数调用的函数环境,包括函数参数等。
局部变量,例如:
char buff[1024*1024]
buff为1M,如果你的栈默认也是1M大小,这就会发生栈溢出,因为其他东西会占掉少量栈空间,局部变量能用的空间肯定小于1M,程序在执行到main函数之前,就会跳出stack overflow异常。
3、函数调用层数太深
这种情况一般发生在递归调用中
过多的递归调用为什么会引起栈溢出呢?
在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当程序执行进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,就会导致栈溢出。函数的参数是通过stack栈来传递的,在调用中会占用线程的栈资源。递归调用在到达最后的结束点后,函数才能依次退栈清栈,如果递归调用层数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,程序异常退出。
不得不使用递归的场景:遍历一个目录下的所有文件,包括其子目录的文件。
对于解决一些包含重复类似逻辑的问题,递归对于开发人员来说是清晰的选择。
在不得不使用递归时,如何评估栈空间是否足够,避免栈溢出。
评估思路:
1. 确认当前线程栈空间限制是多少?
2. 递归调用n次,分析n次压栈后栈空间的损耗是多少?
3. 结合业务预估最大可能的递归调用次数,如果大于或已经接近栈大小, 就存在栈越界风险,需要放大栈空间或者做功能规格约束。
优化
当栈不够使用时,一种办法是修改程序:
主要还是要注意递归调用引起的栈溢出,多数情况可以通过算法优化来解决:
1、控制递归深度。例如,使用动态规划来代替递归算法等。
2、修改栈的大小。
尾递归优化
尾递归是指,在函数返回的时候,调用函数本身,并且return语句不能包含表达式。如果递归调用,都出现在函数的末尾,这个递归函数就是尾递归的函数。
尾递归函数的特点是在回归过程中,不用做操作,这个特性很重要,因为大多数现代编译器会利用这一特点,自动生成优化的代码。
有些语言极力提倡尾递归,因为它们的编译器会对代码进行优化,不会因为递归次数的增加,给函数栈带来巨大的开销。
尾递归优化,使无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
递归的优点是逻辑清晰。
循环替代递归
所有的递归都可以改写成循环的方式。
斐波那契数列: 1,1,2,3,5,……
求斐波那契数列的第N项的值
尾递归和循环的执行效率都非常高。但是尾递归的递归层数大到一定程度会出现段错误。尾递归的函数栈开销比普通递归要小的多,执行效率大很多。 但是尾递归仍然有函数栈开销。
正因为尾递归具有函数栈开销,其调用次数比循环小很多。
实现一个功能,能不用递归就别用递归,能用尾递归就用尾递归。
//一般递归
int fib_normal(int n)
{
if (n <= 2)
return 1;
else
return fib_normal(n-1) + fib_normal(n-2);
}
//尾递归
int fib_rail(int n, int first, int second)
{
if (n == 1) return first;
if (n == 2) return second;
return fib_rail(n-1, second, second+first);
}
unsigned int fib_rail_rec(unsigned int n)
{
return fib_rail_rec(n, 1, 1);
}
// 循环取代递归
int fib_no(int n)
{
if (n <= 2)
return 1;
int x=1, y=1, y_tmp=0;
for (int i=0; i<n-2; i++)
{
y_tmp = y;
y = x+y;
x = y_tmp;
}
return y;
}
栈溢出windbg显示Stack overflow
0:072> !analyze -v
CONTEXT: (.ecxr)
eax=0000000e ebx=76fbceb8 ecx=00413fb8 edx=00417b98 esi=00413fb8 edi=00000000
eip=76ed83eb esp=2f652ffc ebp=2f653040 iopl=0 nv up ei pl nz ac pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010216
ntdll!RtlAcquireSRWLockExclusive+0xb:
76ed83eb 53 push ebx
Resetting default scope
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 76ed83eb (ntdll!RtlAcquireSRWLockExclusive+0x0000000b)
ExceptionCode: c00000fd (Stack overflow)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 2f652ff8