美文网首页
递归与循环

递归与循环

作者: Teech | 来源:发表于2019-12-18 17:12 被阅读0次

一直听说“递归的效率不如循环”,“递归会爆栈”等等说法。想这这里深入分析下递归与非递归的区别。
先看看下面的例子代码以及对应的汇编代码。

int f(int n)
{   
    if(n>0)return 1+f(n-1);
    return 0;
}

int main()
{
    return f(8192*1024);
}
   0x0000000100000f50 <+0>: push   %rbp
   0x0000000100000f51 <+1>: mov    %rsp,%rbp
   0x0000000100000f54 <+4>: sub    $0x10,%rsp
   0x0000000100000f58 <+8>: mov    %edi,-0x8(%rbp)
   0x0000000100000f5b <+11>:    cmpl   $0x0,-0x8(%rbp)
   0x0000000100000f5f <+15>:    jle    0x100000f7d <f+45>
   0x0000000100000f65 <+21>:    mov    -0x8(%rbp),%eax
   0x0000000100000f68 <+24>:    sub    $0x1,%eax
   0x0000000100000f6b <+27>:    mov    %eax,%edi
   0x0000000100000f6d <+29>:    callq  0x100000f50 <f>
   0x0000000100000f72 <+34>:    add    $0x1,%eax
   0x0000000100000f75 <+37>:    mov    %eax,-0x4(%rbp)
   0x0000000100000f78 <+40>:    jmpq   0x100000f84 <f+52>
   0x0000000100000f7d <+45>:    movl   $0x0,-0x4(%rbp)
   0x0000000100000f84 <+52>:    mov    -0x4(%rbp),%eax
   0x0000000100000f87 <+55>:    add    $0x10,%rsp
   0x0000000100000f8b <+59>:    pop    %rbp
   0x0000000100000f8c <+60>:    retq 

我机器本身的默认栈大小为8192kB(可以通过ulimit -a查看),通过gdb调试,然后查看n为多少的时候崩溃。
Thread 2 received signal SIGSEGV, Segmentation fault.
0x0000000100000f6d in f (n=8126506) at t1.c:4
4 return 1+f(n-1);
在n为8126506时收到段错误的信号,系统杀掉进程。81921024 - 8126506 = 262102,就是说当递归进行到262102次时,堆栈溢出。
在来看看每次调用函数的开销,push %rbp,以及sub $0x10,%rsp,还有隐含的call指令会把pc指针压入栈中,这3个部分的栈增大的大小为32(8+16+8)。递归的次数262102
32 = 8387264。这个就是几乎等于系统栈限制的大小了。
这个例子中,可以简单通过一个循环来解决递归的问题。有些需要用用户栈来模拟递归调用的情况,从这个例子中也可以看出,用户栈模拟也会好于递归的调用结果,因为rbp等寄存器不用保存,以及call指令对应的pc指针压栈的操作也减少了,用户栈占用大小比递归时栈的大小也要小很多。
当然递归时,如果优化形成尾递归,也会对缓解栈溢出的风险。上述代码实际就是尾递归,当使用O1优化时,形成的汇编如下

100000f90:  55  pushq   %rbp
100000f91:  48 89 e5    movq    %rsp, %rbp
100000f94:  31 c0   xorl    %eax, %eax
100000f96:  85 ff   testl   %edi, %edi
100000f98:  0f 49 c7    cmovnsl %edi, %eax
100000f9b:  5d  popq    %rbp
100000f9c:  c3  retq
100000f9d:  0f 1f 00    nopl    (%rax)

可以看出编译器优化掉了递归调用,这里直接改成一个简单的条件赋值操作。

相关文章

  • 领扣算法12:整数转换为罗马数字

    题目描述: 递归实现: 循环实现: 递归与循环的比较:

  • 递归与循环

    一.递归与循环 递归,说白了就是自己调用自己。理论上,任何的循环都可以重写为递归形式,所有的递归也可以被表述成循环...

  • 递归与循环

    理论上,任何循环都可以重写为递归形式。有些语言没有循环语句,只能使用递归。 循环改递归 改为递归的关键是发现逻辑“...

  • 递归与循环

    一 题目描述: 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序...

  • 递归与循环

    一直听说“递归的效率不如循环”,“递归会爆栈”等等说法。想这这里深入分析下递归与非递归的区别。先看看下面的例子代码...

  • 递归入门

    1.递归求前n项和 所有循环都可以转化为递归,而递归大多数可以转换为循环 2.递归求最大值 数组第一个下标与最后一...

  • 循环与递归对比

    大学学习递归的时候有一句话印象深刻:所有的递归都可以改写为循环。这句话我是同意的,因为递归其实本质上就是栈的操作。...

  • 谈谈递归与循环

    谈谈递归与循环很久没有写技术文章了,重新提笔希望这次能坚持下去。端午小长假随手给游戏的公会写了一个抽奖程序,在写抽...

  • 6.模块、函数与变量作用域

    循环与使用场景 while 解决问题的基本思维模式多用于递归,其他场景,推荐使用for循环。 for 与 for ...

  • 递归

    递归不用循环,调用自身循环,上诉代码为递归,它的普通形式如下:

网友评论

      本文标题:递归与循环

      本文链接:https://www.haomeiwen.com/subject/vjyunctx.html