C#中尾递归的使用、优化及编译器优化

递归运用

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

递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果。
递归最重要的是边界条件,这个边界是整个递归的终止条件。

代码如下:

static int RecFact(int x)
{
    if (x == 0)
        return 1;
    return x * RecFact(x - 1);
}
RecFact(10);

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

1.转换,把10的阶乘转化成10*9!,10(9*8!)....每次转换规模就变的更小。
2.逼近,转换到最小规模时0!,求解1。开始逆向合并逐渐逼近到10,得出解。
这里的x==0就是我们的边界条件(即终止条件),也有的依赖外部变量为边界。

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

RecFact调用堆栈:

常见使用场景:

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

尾递归优化

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

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

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

代码如下:

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

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

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

编译器优化

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

1.Net在C#语言中是JIT编译成汇编时进行优化的。
2.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#使用循环一样。

(0)

相关推荐

  • C#函数式编程中的递归调用之尾递归详解

    关于递归相信大家已经熟悉的不能再熟悉了,所以笔者在这里就不多费口舌,不懂的读者们可以在博客园中找到很多与之相关的博客.下面我们直接切入正题,开始介绍尾递归. 尾递归 普通递归和尾递归如果仅仅只是从代码的角度出发来看,我们可能发现不了他的特点,所以笔者利用两张堆栈上的图来展示具体的差距在哪,首先我们来看看普通的递归调用的情况,如下图1.1所示: 假设这里执行的函数是Func1,并且Func1中通过递归调用了自己,那么我们可以看到栈上在每次调用Func1的时候都会重新将函数返回地址等其他参数放入栈中

  • C#中尾递归的使用、优化及编译器优化

    递归运用 一个函数直接或间接的调用自身,这个函数即可叫做递归函数. 递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果. 递归最重要的是边界条件,这个边界是整个递归的终止条件. 复制代码 代码如下: static int RecFact(int x) {     if (x == 0)         return 1;     return x * RecFact(x - 1); } RecFact(10); 上面是个经典阶乘函数的实现.这里分2步: 1.转换,把10的

  • python中尾递归用法实例详解

    本文实例讲述了python中尾递归用法.分享给大家供大家参考.具体分析如下: 如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的.当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归.尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码. 原理: 当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活跃记录而不是在栈中去创建一个新的.编译器可以做到这点,因

  • 浅谈C语言编程中程序的一些基本的编写优化技巧

    大概所有学习C语言的初学者,都被前辈说过,C语言是世界上接近最速的编程语言,当然这并不是吹牛,也并不是贬低其他语言,诚然非C语言能写出高速度的代码,但是C语言更容易写出高速的程序(高速不代表高效),然而再好的工具,在外行人手中也只能是黯淡没落. 对于现代编译器,现代CPU而言,我们要尽量迎合CPU的设计(比如架构和处理指令的方式等),虽然编译器是为程序员服务,并且在尽它最大的能力来优化程序员写出的代码,但是毕竟它还没有脱离电子的范畴,如果我们的代码不能让编译器理解,编译器无法帮我们优化代码,那么

  • 详解C++编译器优化技术

    前言 注1:vc6.vs没有提供编译选项来关闭该优化,无论是debug还是release都会进行RVO和复制省略优化 注2:vc6.vs2005以下及vs2005+ Debug上不支持NRVO优化,vs2005+ Release支持NRVO优化 注3:g++支持这三种优化,并且可通过编译选项:-fno-elide-constructors来关闭优化 RVO #include <stdio.h> class A { public: A() { printf("%p construct\

  • C/C++ 编译器优化介绍

    0. gcc -o gcc -o 的优化仍然是机械的,想当然的.只有做到深入理解计算机系统,加深对编程语言的理解,才能写出最优化的代码. Linux下gcc 优化级别的介绍  · gcc -o0 ⇒ 不提供任何优化:  · gcc -o1 ⇒ 最基本的优化,主要对代码的分支.表达式.常量等进行优化,编译器会在较短的时间下将代码变得更加短小,这样体积就会变得更小,会减少内存的占用率,在操作系统进行内存调度时就会更快.          · 但是事情没有绝对的优点,当一个庞大的程序被拆碎细分的话,内

  • 浅谈el-table中使用虚拟列表对对表格进行优化

    目录 前言 解决思路 具体实现 需要满足的必备条件 问题 前言 我们会经常使用表格,如果数据量大就直接可以分页,但是不可避免的会出现一些需要一页要展示很多条的情况,或者不用分页. 这个时候如果纯展示还好一点,列表里如果加上可编辑,就会变得卡的不行,点个下拉框,下拉框好久才会弹出来.这样是十分影响用户使用. 解决思路 首先我考虑是不是由于数据加载太慢导致的页面卡顿,于是我采用了滚动加载的方式,首先获取数据后,先加载100条数据.滚动的时候再加载到页面中,这样分批次加载,就会减轻首次加载数据量太大的

  • java性能优化之编译器版本与平台对应关系

    目录 JIT编译器版本 默认情况JVM如何选择编译器? 如何判断当前环境jvm使用的编译器? 小节 本章节更加具体化的学习编译器还有哪些可以优化的方便,让你的应用展现出更好的性能. JIT编译器版本 JIT编译器有不同的版本,而最终你使用哪种,取决于你所使用的系统平台.前面的文章我们说到编译器有-client和-server,具体划分应该是如下所示: -client 32位client编译器 -server 32位server编译器 -d64 64位server编译器 如果你的系统是32位,那么

  • vue项目中一定会用到的性能优化技巧

    目录 引言 性能优化标准 Lighthouse 通用常规优化手段 通用性能优化分析 FCP(First Contentful Paint) LCP(Largest Contentful Paint) SpeedIndex(速度指数) 排查性能瓶颈 分析包内容 利用chorme devtool 的代码覆盖率 针对vue 的特殊优化 图片懒加载 虚拟滚动 vue 中的函数式组件 利用v-show .KeepAlive 复用dom 分批渲染组件 最后 引言 提起性能优化 很多人眼前浮现的面试经验是不是

  • 浅谈Android性能优化之内存优化

    1.Android内存管理机制 1.1 Java内存分配模型 先上一张JVM将内存划分区域的图 程序计数器:存储当前线程执行目标方法执行到第几行. 栈内存:Java栈中存放的是一个个栈帧,每个栈帧对应一个被调用的方法.栈帧包括局部标量表, 操作数栈. 本地方法栈:本地方法栈主要是为执行本地方法服务的.而Java栈是为执行Java方法服务的. 方法区:该区域被线程共享.主要存储每个类的信息(类名,方法信息,字段信息等).静态变量,常量,以及编译器编译后的代码等. 堆:Java中的堆是被线程共享的,

  • 详解Android性能优化之启动优化

    1.为什么要进行启动优化 网上流行一种说法,就是8秒定律,意思是说,如果用户在打开一个页面,在8秒的时间内还没有打开,那么用户大概的会放弃掉,意味着一个用户的流失.从这里就可以看出,启动优化的重要性了. 2.启动的分类 2.1 冷启动 先来看看冷启动的流程图 从图中可以看出,APP启动的过程是:ActivityManagerProxy 通过IPC来调用AMS(ActivityManagerService),AMS通过IPC启动一个APP进程,ApplicationThread通过反射来创建App

随机推荐