尾递归以及编译器优化

来源:互联网 发布:淘宝店铺上新微淘描述 编辑:程序博客网 时间:2024/06/10 17:27

      大家对递归应该都不陌生,我相信大家应该都写过递归函数,提起递归你脑海里肯定会出现 n阶层,斐波那契数列,汉诺塔,树的遍历等等。是的这些都可以用递归实现,那么尾递归是什么呢?我们先来看看维基百科上的解释:尾部递归是一种编程技巧。递归函数是指一些会在函数内调用自己的函数,如果在递归函数中,递归调用返回的结果总被直接返回,则称为尾部递归。尾部递归的函数有助将算法转化成函数编程语言,而且从编译器角度来说,亦容易优化成为普通循环。这是因为从电脑的基本面来说,所有的循环都是利用重复移跳到代码的开头来实现的。如果有尾部归递,就只需要叠套一个堆栈,因为电脑只需要将函数的参数改变再重新调用一次。利用尾部递归最主要的目的是要优化

    我想维基百科已经介绍的很明白啦,我这里主要用c++实现一个尾递归,并探讨一下编译器的优化,我们以n的阶层为例。

首先是普通的递归算法:

int fun(int n){if (n == 0)return 1;return fun(n - 1) * n;}


我们现在想办法把这个函数变成尾递归,尾递归要求最后return语句返回这个函数本身不能有运算,所以我要将最后的乘法去掉,可以引进一个参数用来计算

尾递归算法:

int fun1(int n, int acc){if (n == 0)return acc;return fun1(n - 1, n * acc);}

简单说明一下,可以看到整个计算任务都交给啦第二个参数,而第一个参数是负责计数的。很容易理解,下面我们来看看编译器的优化。

首先我在vs2010 debug下看反汇编

int fun(int n){002913B0  push        ebp 002913B1  mov         ebp,esp 002913B3  sub         esp,0C0h 002913B9  push        ebx 002913BA  push        esi 002913BB  push        edi 002913BC  lea         edi,[ebp-0C0h] 002913C2  mov         ecx,30h 002913C7  mov         eax,0CCCCCCCCh 002913CC  rep stos    dword ptr es:[edi]                 if (n == 0)002913CE  cmp         dword ptr [n],0 002913D2  jne         fun+2Bh (2913DBh)                                 return 1;002913D4  mov         eax,1 002913D9  jmp         fun+3Eh (2913EEh)                 return fun(n - 1) * n;002913DB  mov         eax,dword ptr [n] 002913DE  sub         eax,1 002913E1  push        eax 002913E2  call        fun (2910F0h) 002913E7  add         esp,4 002913EA  imul        eax,dword ptr [n] }


 

int fun1(int n, int acc){01291420  push        ebp 01291421  mov         ebp,esp 01291423  sub         esp,0C0h 01291429  push        ebx 0129142A  push        esi 0129142B  push        edi 0129142C  lea         edi,[ebp-0C0h] 01291432  mov         ecx,30h 01291437  mov         eax,0CCCCCCCCh 0129143C  rep stos    dword ptr es:[edi]                 if (n == 0)0129143E  cmp         dword ptr [n],0 01291442  jne         fun1+29h (1291449h)                                 return acc;01291444  mov         eax,dword ptr [acc] 01291447  jmp         fun1+40h (1291460h)                 return fun1(n - 1, n * acc);01291449  mov         eax,dword ptr [n] 0129144C  imul        eax,dword ptr [acc] 01291450  push        eax 01291451  mov         ecx,dword ptr [n] 01291454  sub         ecx,1 01291457  push        ecx 01291458  call        fun1 (12911D6h) 0129145D  add         esp,8 }


大家可以看到这种情况下基本没有什么优化,汇编也是一步一步执行的,汇编不同的地方也只是普通递归是先call 函数,返回后在做乘法运算;而尾递归是先计算call 之后不会再做用算。

我们再看看release和/0x优化之后的结果:

普通递归:

int fun(int n){01351000  push        ebp 01351001  mov         ebp,esp 01351003  push        esi                 if (n == 0)                                return 1;                return fun(n - 1) * n;01351004  mov         esi,dword ptr [n] 01351007  lea         eax,[esi-1] 0135100A  test        eax,eax 0135100C  jne         fun+19h (1351019h) 0135100E  mov         eax,1 01351013  imul        eax,esi 01351016  pop         esi }01351017  pop         ebp 01351018  ret                 if (n == 0)                                return 1;                return fun(n - 1) * n;01351019  push        eax 0135101A  call        fun (1351000h) 0135101F  add         esp,4 01351022  imul        eax,esi 01351025  pop         esi }


尾递归:

int fun1(int n, int acc){00F31030  push        ebp 00F31031  mov         ebp,esp                 if (n == 0)                                return acc;                return fun1(n - 1, n * acc);00F31033  mov         ecx,dword ptr [n] 00F31036  mov         eax,ecx 00F31038  imul        eax,dword ptr [acc] 00F3103C  dec         ecx 00F3103D  je          fun1+19h (0F31049h) 00F3103F  push        eax 00F31040  push        ecx 00F31041  call        fun1 (0F31030h) 00F31046  add         esp,8 }

 

总结:大家可以简单的从汇编的代码数量就可以看出优化的不同,明显尾递归的汇编语句少很多,还有不同的是由于普通递归在函数返回时还要做乘法运算,所以必须在栈上保留n的信息,等函数返回时好做乘法运算,而尾递归不需要这个信息,所以尾递归的效率的确要高,但是我并没有看到维基百科上说的优化成循环的汇编,过两天我在linux下gcc试试。

疑问:是不是所有的递归都能变成尾递归呢?(后续研究一下)

ps:如果有人对汇编看不懂我有空加上详细的注释(有人提出的话)。

再附上vs的优化选项:

  • /O1 为获得最小大小而优化代码。

  • /O2 为获得最高速度而优化代码。

  • /Ob 控制内联函数展开。

  • /Od 禁用优化,从而加快编译并简化调试。

  • /Og 启用全局优化。

  • /Oi 为适当的函数调用生成内部函数。

  • /Os 通知编译器优选大小优化而非速度优化。

  • /Ot(默认设置)通知编译器优选速度优化而非大小优化。

  • /Ox 选择完全优化。

  • /Oy 取消在调用堆栈上创建框架指针,以更快地进行函数调用。

备注

还可以将多个 /O 选项组合到一个选项语句。例如,/Odi 与 /Od /Oi 是相同的。


 

原创粉丝点击