尾递归以及编译器优化
来源:互联网 发布:淘宝店铺上新微淘描述 编辑:程序博客网 时间: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 是相同的。
- 尾递归以及编译器优化
- 尾递归和编译器优化
- 浅谈递归过程以及递归的优化
- C++编译器的递归深度与程序优化思考
- 被忽略的C编译器递归内存优化
- 递归优化之尾递归
- 递归及尾递归优化
- 递归及尾递归优化
- 递归优化之尾递归
- 编译器的尾调用优化
- auto_inline,inline以及编译器优化之间的关系
- auto_inline,inline以及编译器优化之间的关系
- Python 尾递归优化
- 尾递归优化
- 尾递归优化【-O2】
- 【Scala】尾递归优化
- 尾递归优化
- 尾递归优化
- 全是老古董:俄罗斯程序员收藏的8080处理器
- linux下解压windows下的rar文件
- ROS探索总结(十二)——坐标系统
- Poj 3660 Cow Contest (传递闭包 Floyd算法变形)
- Android cursor
- 尾递归以及编译器优化
- Ubuntu 使用
- hdu 1558 Segment set(并查集+线段相交)
- 经典SQL语句大全
- 测试
- android 创建快捷方式
- 真正的小说 真正的生活 真正的蜕变 真正的品味
- 黑马程序员_二十四 【交通灯管理系统】
- IOS消息推送相关介绍