【问题标题】:Does java support and optimize away tail-recursive calls?java是否支持和优化尾递归调用?
【发布时间】:2021-07-11 20:18:05
【问题描述】:

假设我有一个递归函数,它是尾递归。不知道这个函数会是递归实现,在栈上增长,还是改成循环(因为是尾递归函数)?

我刚刚读到 Scala 会检测到此类调用并对其进行优化,但这是仅限 Scala 的东西还是一般的 JVM?

【问题讨论】:

  • 我很确定这个函数不是尾递归的。在对 sum() 的嵌套调用之后,仍然需要在该调用的返回值中添加一些内容。
  • 不是尾声。
  • 不管怎样,HotSpot 不支持 TCO:stackoverflow.com/q/3616483/139010
  • 创建一个 subList 对象比递归更昂贵(假设你已经用完堆栈)当然最有效的选择是简单的迭代。
  • 鉴于您对这种编程风格感兴趣,您是否有理由不能只使用 Scala?以上(整个)的等价物将是println((0 to 5).sum)。如果您考虑使用 sum() 方法作弊,您可以使用折叠生成总和:(0 /: (0 to 5))(_+_)。作为一种方法,上面的一个更通用的版本是def sum( xs:Seq[Int] ):Int = if (xs.isEmpty) 0 else xs.head + sum(xs.tail)。或者您可以编写一个尾递归版本,它实际上会得到优化(通过 scalac)。

标签: java scala optimization recursion jvm


【解决方案1】:

Java 支持尾递归调用,但 AFAIK 并没有优化它们。我认为是 Scala 编译器能够做到这一点,而不是 JVM 本身。查看 Scala 中的 @tailrec 注释,了解编译器的更多功能:)

但无论 Java/JVM 是否优化了尾递归,您的函数都会比必要的更难优化。

看看这个:

int sum(List<Integer> integers) {
    return sum(integers, 0);
}

int sum(List<Integer> integers, int sumSoFar) {
    if (integers.isEmpty())
        return sumSoFar;
    else
        return sum(
                integers.subList(1, integers.size()),
                sumSoFar + integers.get(0)
        );
}

看,我已经添加了一个重载的sum 和一个到目前为止计算的总和参数。这样,当您在 else 分支中递归时,您就不再需要实际的堆栈帧了 - 您在递归调用中获得了作为函数参数所需的一切。

在您的 sn-p 中,只要递归调用,堆栈框架就可能必须存在..

【讨论】:

  • 你可能知道这一点,但无论如何,@tailrec 仅用于强制优化代码——如果编译器不能这样做,它会抛出编译错误。但是,如果可能的话,无论是否提供此类注释,代码都会被优化。
  • @om-nom-nom 是的,很高兴你在这里指出了这一点。 Scala 中的@tailrec 类似于Java 中的@Override(在存在和编译器操作方面)。
  • 我相信这叫做“共同递归”
【解决方案2】:

Java 和 JVM 目前不支持尾调用

需要进行的基本工作是在 JVM 级别,而不是 Java。解决这个问题的工作进展缓慢(最初是MLVM 的一部分,现在在Project Loom 下)。尽管相对较旧,但这个2007 blog from John Rose 是对如何更改 JVM 字节码的简短概述。我认为尾部呼叫工作被搁置一旁,有利于首先完成invokedynamic(这已经发现了more tricky design considerations for tail calls)。这是the latest Project Loom proposal的摘录:

由于毫无疑问需要向 JVM 添加操作调用堆栈的能力,因此该项目的目标也是添加一个更轻量级的结构,允许将堆栈展开到某个点,然后调用一个方法给定参数(基本上是有效尾调用的概括)。我们将该功能称为 unwind-and-invoke 或 UAI。


其他一些细节:

  • Java 作为一种语言不太可能自动优化尾调用。这是因为尾调用消除的大多数实现对调用堆栈都有一些不直观的影响(例如,如果您在递归调用中抛出异常,则不会在堆栈跟踪中看到所有递归调用)。

    JavaScript 是一种尝试自动优化尾调用的语言示例(在 ES2015 中),here is a debrief from the V8 team 解释了其中的困难。他们已经删除了该功能并转而支持提案that supports tail-call optimization only when it is explicit

    如果 JVM 在字节码级别添加了对尾调用的支持,我推测 Java 也可能支持显式尾调用优化(即,以 return 上的注释或函数上的注释的形式或甚至可能是一个新关键字)。

  • Scala 尝试检测和优化尾递归到 JVM 字节码循环。 Here is a project 承诺对现有的 JVM 字节码执行这种优化。 Java 没有采用这个想法,因为它有些脆弱和有限:

    1. 相互递归的函数变得非常棘手(Scala 的 @tailrec 放弃了)
    2. 多态递归不起作用
    3. 广义的尾调用显然行不通

【讨论】:

  • 不清楚您链接到的某些页面的最新情况。
  • @Raedwald 在试图找出如何在 JVM 上最好地编译 WASM 尾调用的过程中,我花了一些时间研究这个问题。 AFAICT,真的什么都没有。即使是实际的 WIP 仍然非常简单:github.com/openjdk/loom。与 Amber 和 Valhalla 等其他优先项目不同,Loom 还没有文档库来讨论设计。它也没有任何 JEP 草案:openjdk.java.net/jeps/0。不过,我很高兴看到另一个答案证明我错了。
  • Scala 的@tailrec annotation 是对编译器优化函数的要求。它不限于仅对标有该注释的函数执行此操作。 Scala 非常乐意寻找任何可以展示 TCO 的 final 函数。
  • @SilvioMayolo 感谢您的参考!我不知道是这样的。你知道是否有办法告诉 Scala 编译器执行 TCO?
  • 据我所知,没有办法抑制它。不过,可能有一些我不知道的模糊注释或编译器设置
【解决方案3】:

据此article 2014 年 4 月:

请务必注意,这不是 JVM 中的错误。这是一种可以实现的优化,以帮助使用递归的函数式程序员,这在那些语言中更为常见和正常。我最近与 Oracle 的 Brian Goetz 讨论了这种优化,他说它在要添加到 JVM 的内容列表中,但它并不是一个高优先级的项目。

【讨论】:

    【解决方案4】:

    java中Factorial的尾递归,空间复杂度为O(1)

      int tailFactorial(int x){
        return tailFactorial(x, 1);
    }
    
    int tailFactorial(int x, int totalFactor){
        if(x == 0){
            return totalFactor;
        }else{
            return tailFactorial(x-1, totalFactor * x);
        }
    
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-07-23
      • 2012-11-15
      • 2011-09-04
      • 1970-01-01
      • 1970-01-01
      • 2017-04-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多