欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-11-21 15:49:58
递归运用 一个函数直接或间接的调用自身,这个函数即可叫做递归函数。 递归主要功能是把问题转换成较小规模的子问题,以子问题的解去逐渐逼近最终结果。 递归最重要的是边界条...

递归运用

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

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

复制代码 代码如下:

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调用堆栈:

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

常见使用场景:

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#中尾递归的使用、优化及编译器优化

c#/32位或c#/debug模式中jit是不进行优化的。

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

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) 

优化成:

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

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

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

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

总结

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