探索c#之尾递归编译器优化

递归运用

一个函数直接或间接的调用自身,这个函数即可叫做递归函数。

  • 递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果。
  • 递归最重要的是边界条件,这个边界是整个递归的终止条件。
static int RecFact(int x)
{
    if (x == 0)
        return 1;
    return x * RecFact(x - 1);
}
RecFact(10);

上面是个经典阶乘函数的实现。这里分2步:

  • 转换,把10的阶乘转化成10*9!,10(9*8!)....每次转换规模就变的更小。
  • 逼近,转换到最小规模时0!,求解1。开始逆向合并逐渐逼近到10,得出解。

这里的x==0就是我们的边界条件(即终止条件),也有的依赖外部变量为边界。

如果一个递归函数没有边界,也就无法停止(无限循环至内存溢出),当然这样也没什么意义。

RecFact调用堆栈:

常见使用场景:

  • 阶乘/斐波那契数列/汉诺塔
  • 遍历硬盘文件
  • InnerExceptions异常扑捉(exception.InnerException==null)

尾递归优化

当边界不明确的时候,递归就很容易出现溢出问题。

在阶乘过程中,堆栈需要保存每次(RecFact)调用的返回地址及当时所有的局部变量状态,期间堆栈空间是无法释放的(即容易出现溢出)。

为了优化堆栈占用问题,从而提出尾递归优化的办法。

    static void TailRecursion(int x)
    {
        Console.WriteLine(x);
        if (x == 10)
            return;
        TailRecursion(x + 1);
    }
    TailRecursion(0);

使用尾递归堆栈可以不用保存上次的函数返回地址/各种状态值,而方法遗留在堆栈上的数据完全可以释放掉,这是尾递归优化的核心思想。

由于尾递归期间,堆栈是可以释放/再利用的,也就解决递归过深而引起的溢出问题,这也是尾递归的优势所在。

编译器优化

尾递归优化,看起来是蛮美好的,但在net中却有点乱糟糟的感觉。

  • Net在C#语言中是JIT编译成汇编时进行优化的。
  • Net在IL上,有个特殊指令tail去实现尾递归优化的(F#中)。

我们执行 TailRecursion(0)(x==1000000) 得出如下结论:

C#/64位/Release是有JIT编译器进行尾递归优化的(非C#编译器优化)。

C#/32位或C#/Debug模式中JIT是不进行优化的。

F#在优化尾递归也分2种情况:

1、 简单的尾递归优化成while循环,如下:

 let rec TailRecursion(x) =
  if (x = 1000) then true
  else TailRecursion(x + 1)

(方便演示C#呈现)编译器优化成:

    public static bool TailRecursion(int x)
    {
        while (x != 0x3e8)
        {
            x++;
        }
        return true;
    }

2、 复杂的尾递归,F#编译器会生成IL指令Tail进行优化,如下:

let TailRecursion2 a cont = cont (a + 1)  

优化成:

如何定义复杂的尾递归呢?通常是后继传递模式(CPS)。

F#中在debug模式下,需要在编译时配置:

总结

在C#语言(过程式/面向对象编程思想)中,优先考虑的是循环,而不是递归/尾递归。 但在函数式编程思想当中,递归/尾递归使用则是主流用法,就像在C#使用循环一样。

参考资料

http://volgarev.me/blog/62412678347

http://stackoverflow.com/questions/15864670/generate-tail-call-opcode

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏申龙斌的程序人生

Eclipse油藏模型解析程序

2010年的时候,三维可视化项目中要读取eclipse建模软件产生的三维模型网格数据,经过连续多天的奋战,终于搞明白eclipse数模软件输出的egrid、in...

34270
来自专栏JAVA高级架构

java面试需要掌握知识点

重点知识 由于我面试的JAVA开发工程师,针对于JAVA,需要理解的重点内容有: JVM内存管理机制和垃圾回收机制(基本每次面试都会问,一定要搞得透彻) JVM...

38250
来自专栏李海辰的专栏

Unity引擎资源管理代码分析 ( 2 )

上一篇《Unity 引擎资源管理代码分析 ( 1 ) 》讲解了 Unity 引擎资源管理代码的类型设计架构和 Resources.Load 接口的实现,本文将...

96740
来自专栏逍遥剑客的游戏开发

Nebula3学习笔记(3): 核心库

204110
来自专栏mathor

LeetCode55. 跳跃游戏

 首先创建一个index数组,存储当前位置最大可达的数组下标,就以样例1来举例,输入是[2,3,1,1,4],那么对应的这个index数组就是[2,4,3...

23920
来自专栏大史住在大前端

野生前端的数据结构基础练习(2)——队列

循环队列书中并没有提及,它是一种特殊的队列。简单理解就是将基本队列只当做存储结构,而使用front和rear两个指针分别代表队列的头和尾,实际对外表现的队列是f...

22230
来自专栏JetpropelledSnake

Python面试题之回调函数

编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;...

17320
来自专栏灯塔大数据

干货 | 数据科学入门必读:如何使用正则表达式?

有时候,这些数据中会包含大量文本语料。比如,假如我们需要搞清楚「xxx文件 」中谁给谁发送过邮件,那么我们就要筛查 1150 万份文档!我们可以采用人工方式,亲...

13520
来自专栏iOS技术杂谈

iOS runtime探究(五): 从runtime开始深入weak实现机理你要知道的runtime都在这里

你要知道的runtime都在这里 转载请注明出处 https://cloud.tencent.com/developer/user/1605429 本文主要讲解...

36260
来自专栏IT大咖说

面向前端开发者的V8性能优化

摘要 V8是一个由丹麦Google使用C++开发的开源JavaScript引擎,用于Google Chrome中,目前该JavaScript引擎已用于其它项目的...

399100

扫码关注云+社区

领取腾讯云代金券