编译器可以并将递归逻辑转换为等效的非递归逻辑吗?


15

我一直在学习F#,并且它开始影响我在编写C#时的想法。为此,当我感觉到结果提高了可读性并且无法想象它最终会导致堆栈溢出时,我一直在使用递归。

这使我问一个问题,编译器是否可以自动将递归函数转换为等效的非递归形式?


如果有基本示例,则尾部调用优化是一个很好的选择,但是只有在您需要return recursecall(args);递归的情况下,它才有效,通过创建显式堆栈并将其缩减下来,可以实现更复杂的操作,但是我怀疑它们是否会
棘手异常

@ratchet怪胎:递归并不意味着“正在使用堆栈的计算”。
Giorgio

1
@ Giorgio我知道,但是堆栈是将递归转换为循环的最简单方法
棘手怪胎

Answers:


21

是的,某些语言和编译器会将递归逻辑转换为非递归逻辑。这称为尾部调用优化 -请注意,并非所有递归调用都是尾部调用可优化的。在这种情况下,编译器会识别以下形式的函数:

int foo(n) {
  ...
  return bar(n);
}

在这里,该语言能够识别出返回的结果是来自另一个函数的结果,并将具有新堆栈框架的函数调用更改为跳转。

实现经典析因方法:

int factorial(n) {
  if(n == 0) return 1;
  if(n == 1) return 1;
  return n * factorial(n - 1);
}

不是因为回报必要的检查尾调用optimizatable。

为了使该尾调用可优化,

int _fact(int n, int acc) {
    if(n == 1) return acc;
    return _fact(n - 1, acc * n);
}

int factorial(int n) {
    if(n == 0) return 1;
    return _fact(n, 1);
}

使用以下代码进行编译gcc -O2 -S fact.c(-O2是启用编译器中的优化所必需的,但是,随着-O3的更多优化,人类难以阅读...)

_fact:
.LFB0:
        .cfi_startproc
        cmpl    $1, %edi
        movl    %esi, %eax
        je      .L2
        .p2align 4,,10
        .p2align 3
.L4:
        imull   %edi, %eax
        subl    $1, %edi
        cmpl    $1, %edi
        jne     .L4
.L2:
        rep
        ret
        .cfi_endproc

可以在segment中看到.L4jne而不是a call(使用新的堆栈框架进行子例程调用)。

请注意,这是使用C完成的。java中的尾调用优化非常困难,并且取决于JVM的实现-tail-recursion + javatail-recursion +优化是浏览的良好标记集。您可能会发现其他JVM语言可以优化尾递归更好(TRY的Clojure(这需要RECUR以尾调用优化),或斯卡拉)。


1
我不确定这就是OP的要求。仅仅因为运行时以某种方式占用或不占用堆栈空间,并不意味着该函数不是递归的。

1
@MattFenwick你是什么意思?“这使我问编译器是否可以自动将递归函数转换为等效的非递归形式”-答案是“在某些情况下是”。演示了条件,并且我提到了某些其他流行语言中带有尾部调用优化的陷阱。

9

小心踩一下。

答案是肯定的,但并非总是如此,并非全部。这项技术的名称有所不同,但是您可以在此处Wikipedia中找到一些非常确定的信息。

我更喜欢使用“尾部呼叫优化”这个名称,但还有其他名称,有些人会混淆该术语。

也就是说,有几件重要的事情要实现:

  • 为了优化尾部调用,尾部调用需要调用时已知的参数。这意味着,如果参数之一是对函数本身的调用,则无法将其转换为循环,因为这将需要所述循环的任意嵌套,而该嵌套在编译时无法扩展。

  • C#无法可靠地优化尾调用。IL具有F#编译器将发出的指令,但是C#编译器将不一致地发出它,并且根据JIT的情况,JIT可能会也可能不会这样做。所有迹象表明,您不应该依赖在C#中优化尾调用,这样做的风险很大,而且确实存在


1
您确定这是OP的要求吗?正如我在另一个答案下发布的那样,仅因为运行时以某种方式占用或不占用堆栈空间,并不意味着该函数不是递归的。

1
@MattFenwick实际上是一个好点,实际上,这取决于它,发出尾部调用指令的F#编译器正在完全维护递归逻辑,它只是指示JIT以替换堆栈空间的方式而不是堆栈空间的方式执行它。但是,其他编译器可能会逐字地编译为循环。(从技术上讲,如果将循环完全放在前面,则JIT会编译为循环,甚至可能是无循环的方式)
Jimmy Hoffa 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.