Optimization 在JVM中运行时在Scala中使用递归

Optimization 在JVM中运行时在Scala中使用递归,optimization,scala,jvm,tail-recursion,jvm-languages,Optimization,Scala,Jvm,Tail Recursion,Jvm Languages,从该站点和web上的其他地方搜索,JVM不支持尾部调用优化。因此,这是否意味着,如果要在JVM上运行,就不应该编写如下可能在非常大的输入列表上运行的尾部递归Scala代码 // Get the nth element in a list def nth[T](n : Int, list : List[T]) : T = list match { case Nil => throw new IllegalArgumentException

从该站点和web上的其他地方搜索,JVM不支持尾部调用优化。因此,这是否意味着,如果要在JVM上运行,就不应该编写如下可能在非常大的输入列表上运行的尾部递归Scala代码

// Get the nth element in a list    
def nth[T](n : Int, list : List[T]) : T = list match {
            case Nil => throw new IllegalArgumentException
            case _ if n == 0 => throw new IllegalArgumentException
            case _ :: tail if n == 1 => list.head
            case _ :: tail  => nth(n - 1, tail)
}
Martin Odersky的Scala by Example包含以下段落,这似乎表明在某些情况下或其他环境中递归是合适的:

原则上,尾部调用始终可以重用调用的堆栈帧 功能。但是,一些运行时环境(如JavaVM)缺少 原语使堆栈帧在尾部调用中的重用高效。生产质量 因此,Scala实现只需要重新使用di的堆栈框架- 其最后一个操作是对自身的调用的直尾递归函数。其他尾部调用可能会 也可以进行优化,但不应该跨实现依赖于此

谁能解释一下这段中间的两句话是什么意思


谢谢大家!

由于直尾递归相当于while循环,因此您的示例将在JVM上高效运行,因为Scala编译器只需使用一个跳转,就可以将其编译为隐藏的循环。但是,JVM不支持常规TCO,尽管有tailcall()方法,它支持使用编译器生成的蹦床进行尾部调用

为了确保编译器能够正确地将尾部递归函数优化为循环,可以使用scala.annotation.tailrec注释,如果编译器无法进行所需的优化,则会导致编译器错误:

import scala.annotation.tailrec

@tailrec def nth[T](n : Int, list : List[T]) : Option[T] = list match {
            case Nil => None
            case _ if n == 0 => None
            case _ :: tail if n == 1 => list.headOption
            case _ :: tail  => nth(n - 1, tail)
}

(该死的IllegalArgmentException!)

Scala编译器将尝试通过将尾部调用“展平”到不会导致堆栈不断扩展的循环中来优化尾部调用


当然,要做到这一点,您的代码必须是可优化的。但是,如果在方法之前使用注释@tailrec(scala.annotation.tailrec),编译器将要求该方法可优化或无法编译。

Martin的评论是,只有直接自递归调用才是TCO优化的候选对象(满足其他标准)。间接的、相互递归的方法对(或更大的递归方法集)不能如此优化

原则上,尾部调用始终可以重用调用的堆栈帧 功能。但是,一些运行时环境(如JavaVM)缺少 原语使堆栈帧在尾部调用中的重用高效。生产质量 因此,Scala实现只需要重新使用di的堆栈框架 其最后一个操作是对自身的调用的直尾递归函数。其他尾部调用可能会 也可以进行优化,但不应该跨实现依赖于此

谁能解释一下这段中间的两句话是什么意思

尾部递归是尾部调用的特例。直接尾部递归是尾部递归的一种特殊情况。只有直尾递归才能保证得到优化。其他的也可以进行优化,但这基本上只是一个编译器优化。作为一种语言特性,Scala只保证直接尾部递归消除

那么,有什么区别呢

嗯,tail调用只是子例程中的最后一个调用:

def a = {
  b
  c
}
在这种情况下,对
c
的调用是尾部调用,对
b
的调用不是尾部调用

尾部递归是指尾部调用调用之前已调用的子例程:

def a = {
  b
}

def b = {
  a
}
这是尾部递归:
a
调用
b
(尾部调用),后者反过来又调用
a
。(与下面描述的直接尾部递归不同,这有时被称为间接尾部递归。)

但是,这两个示例都不会通过Scala进行优化。或者,更准确地说:Scala实现可以优化它们,但不需要这样做。这与Scheme不同,例如Scheme,其中语言规范保证上述所有情况都将占用
O(1)
堆栈空间

Scala语言规范仅保证直接尾部递归得到优化,即当子例程直接调用自身时,中间没有其他调用:

def a = {
  b
  a
}
在这种情况下,对
a
的调用是一个尾部调用(因为它是子例程中的最后一个调用),它是尾部递归(因为它再次调用自己),最重要的是它是直接尾部递归,因为
a
直接调用自己,而不首先进行另一个调用

请注意,有许多微妙的事情可能导致方法不是直接尾部递归的。例如,如果
a
被重载,那么递归实际上可能会经历不同的重载,因此不再是直接的

实际上,这意味着两件事:

  • 您不能对尾部递归方法执行提取方法重构,至少不包括尾部调用,因为这会将直接尾部递归方法(将得到优化)变成间接尾部递归方法(将不会得到优化)
  • 您只能使用直尾递归。一个尾部递归下降解析器,或者一个状态机,可以使用间接尾部递归非常优雅地表示出来
  • 这样做的主要原因是,当您的底层执行引擎缺少强大的控制流操纵功能时,例如
    GOTO
    、continuations、一级可变堆栈或适当的尾部调用,那么您需要在其上实现自己的堆栈,使用蹦床,进行全局CPS转换或类似的恶劣行为,以便提供通用的正确尾部调用。所有这些都会严重影响性能或与同一平台上的其他代码的互操作性

    或者,正如Clojure的创始人里奇·希基(Rich Hickey)在面对同样的问题时所说的那样