尾递归优化 c语言,漫谈递归:从汇编看尾递归的优化

对于尾递归,很多人的理解仅局限于它是递归和尾调用的一个合体,比普通递归效率高。至于效率为什么高,高在哪,可能没有深究过。

尾调用

要说尾递归,得先说尾调用。我理解的尾调用大概是这么一种情况:函数A里面调用了函数B。

函数B执行后,函数A马上返回。

也就是说调用函数B(并返回执行结果)是函数A所做的最后一件事。

相当于执行完函数B后,函数A也就执行完。

因此在执行函数B时,函数A的栈帧其实是已经大部分没用了,可以被修改或覆盖。编译器可以利用这一点进行优化,函数B执行后直接返回到函数A的调用者。

这里有一点需要注意:它是来自于编译器的优化。 这一点点的优化对于普通的尾调用来说可能意义不大,但是对于尾递归来说就很重要了。

尾递归

尾递归是一种基于尾调用形式的递归,相当于前面说的函数B就是函数A本身。

普通递归会存在的一些问题是,每递归一层就会消耗一定的栈空间,递归过深还可能导致栈溢出,同时又是函数调用,免不了push来pop去的,消耗时间。

采用尾调用的形式来实现递归,即尾递归,理论上可以解决普通递归的存在的问题。因为下一层递归所用的栈帧可以与上一层有重叠(利用jmp来实现),局部变量可重复利用,不需要额外消耗栈空间,也没有push和pop。

再次提一下,它的实际效果是来自于编译器的优化(目前的理解)。在使用尾递归的情况下,编译器觉得合适就会将递归调用优化成循环。目前大多数编译器都是支持尾递归优化的。有一些语言甚至十分依赖于尾递归(尾调用),比如erlang或其他函数式语言(传说它们为了更好的处理continuation-passing style)。

假如不存在优化,大家真刀真枪进行函数调用,那尾递归是毫无优势可言的,甚至还有缺点——代码写起来不直观。

现代编译器的优化能力很强悍,很多情况下编译器优化起来毫不手软(于是有了volatile)。但有时编译器又很傲娇,你需要主动给它一点信号,它才肯优化。尾递归就相当于传递一个信号给编译器,尽情优化我吧!

测试

为了验证尾递归优化,可以写个小程序进行测试。在VS2010下将使用/O1或以上的优化选项,一般就会尾递归优化。Gcc3.2.2(这版本好旧)下一般需要使用-O2优化选项。

先看看普通递归:// 递归

int factorial(int n)

{

if(n <= 2)

{

return 1;

}

else

{

return factorial(n-1) + factorial(n-2);

}

}

其汇编代码:00401371push %ebp

00401372mov %esp,%ebp

00401374push %ebx

00401375sub $0x14,%esp

00401378cmpl $0x2,0x8(%ebp)

0040137Cjg 0x401385

0040137Emov $0x1,%eax

00401383jmp 0x4013a4

00401385mov 0x8(%ebp),%eax

00401388dec %eax

00401389mov %eax,(%esp)

0040138Ccall 0x401371

00401391mov %eax,%ebx

00401393mov 0x8(%ebp),%eax

00401396sub $0x2,%eax

00401399mov %eax,(%esp)

0040139Ccall 0x401371

004013A1lea (%ebx,%eax,1),%eax

004013A4add $0x14,%esp

004013A7pop %ebx

004013A8leave

004013A9ret

在0040138C,0040139C这些位置,我们看到递归仍然是使用call指令,那么尾递归在汇编角度是怎么处理的呢?

尾递归:int factorial_tail(int n,int acc1,int acc2)

{

if (n < 2)

{

return acc1;

}

else

{

return factorial_tail(n-1,acc2,acc1+acc2);

}

}

其汇编代码:00401381push %ebp

00401382mov %esp,%ebp

00401384sub $0x18,%esp

00401387cmpl $0x1,0x8(%ebp)

0040138Bjg 0x401392

0040138Dmov 0xc(%ebp),%eax

00401390jmp 0x4013b2

00401392mov 0x10(%ebp),%eax

00401395mov 0xc(%ebp),%edx

00401398lea (%edx,%eax,1),%eax

0040139Bmov 0x8(%ebp),%edx

0040139Edec %edx

0040139Fmov %eax,0x8(%esp)

004013A3mov 0x10(%ebp),%eax

004013A6mov %eax,0x4(%esp)

004013AAmov %edx,(%esp)

004013ADcall 0x401381

004013B2leave

004013B3ret

在00401390位置上,尾递归是直接使用jmp实现循环跳转。

如何查看C语言程序的汇编?可以看看这篇单独的文章:如何在Code::Blocks下查看程序的汇编代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值