码迷,mamicode.com
首页 > 其他好文 > 详细

从汇编看尾递归的优化

时间:2014-09-13 18:37:45      阅读:374      评论:0      收藏:0      [点我收藏+]

标签:style   http   color   io   使用   ar   div   问题   sp   

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

尾调用

要说尾递归,得先说尾调用。我理解的尾调用大概是这么一种情况:k7娱乐城

  1. 函数A里面调用了函数B。
  2. 函数B执行后,函数A马上返回。
  3. 也就是说调用函数B(并返回执行结果)是函数A所做的最后一件事。
  4. 相当于执行完函数B后,函数A也就执行完。

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

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

尾递归

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

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

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

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

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

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

测试

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

先看看普通递归:

01 // 递归
02 int factorial(int n)
03 {
04     if(n <= 2)
05     {
06         return 1;
07     }
08     else
09     {
10         return factorial(n-1) + factorial(n-2);
11     }
12 }

其汇编代码:

01 00401371    push   %ebp
02 00401372    mov    %esp,%ebp
03 00401374    push   %ebx
04 00401375    sub    $0x14,%esp
05 00401378    cmpl   $0x2,0x8(%ebp)
06 0040137C    jg     0x401385 <factorial+20>
07 0040137E    mov    $0x1,%eax
08 00401383    jmp    0x4013a4 <factorial+51>
09 00401385    mov    0x8(%ebp),%eax
10 00401388    dec    %eax
11 00401389    mov    %eax,(%esp)
12 0040138C    call   0x401371 <factorial>
13 00401391    mov    %eax,%ebx
14 00401393    mov    0x8(%ebp),%eax
15 00401396    sub    $0x2,%eax
16 00401399    mov    %eax,(%esp)
17 0040139C    call   0x401371 <factorial>
18 004013A1    lea    (%ebx,%eax,1),%eax
19 004013A4    add    $0x14,%esp
20 004013A7    pop    %ebx
21 004013A8    leave
22 004013A9    ret

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

尾递归:

01 int factorial_tail(int n,int acc1,int acc2)
02 {
03     if (n < 2)
04     {
05         return acc1;
06     }
07     else
08     {
09         return factorial_tail(n-1,acc2,acc1+acc2);
10     }
11 }

其汇编代码:

01 00401381    push   %ebp
02 00401382    mov    %esp,%ebp
03 00401384    sub    $0x18,%esp
04 00401387    cmpl   $0x1,0x8(%ebp)
05 0040138B    jg     0x401392 <factorial_tail+17>
06 0040138D    mov    0xc(%ebp),%eax
07 00401390    jmp    0x4013b2 <factorial_tail+49>
08 00401392    mov    0x10(%ebp),%eax
09 00401395    mov    0xc(%ebp),%edx
10 00401398    lea    (%edx,%eax,1),%eax
11 0040139B    mov    0x8(%ebp),%edx
12 0040139E    dec    %edx
13 0040139F    mov    %eax,0x8(%esp)
14 004013A3    mov    0x10(%ebp),%eax
15 004013A6    mov    %eax,0x4(%esp)
16 004013AA    mov    %edx,(%esp)
17 004013AD    call   0x401381 <factorial_tail>
18 004013B2    leave
19 004013B3    ret

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

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

从汇编看尾递归的优化

标签:style   http   color   io   使用   ar   div   问题   sp   

原文地址:http://www.cnblogs.com/laoyangman/p/3970065.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!