【问题标题】:F# has tail call elimination?F# 有尾调用消除吗?
【发布时间】:2014-03-31 09:32:39
【问题描述】:

talk 中,前 8 分钟,Runar 解释说 Scala 在尾调用消除方面存在问题,这让我想知道 F# 是否也有类似的问题?如果没有,为什么不呢?

【问题讨论】:

  • CLR 通过 tail 操作码 (.NET 4 had some improvements) 支持尾调用优化,据我所知,JVM 不支持尾调用
  • @PatrykĆwiek 我想不通,这只是尾递归调用吗?或者各种尾调用,例如相互递归函数中的尾调用,如odd/even
  • @IonuțG.Stan From this blog post 似乎相互递归函数也使用tail 操作码。
  • @PatrykĆwiek 谢谢。然后它比JVM有更好的支持。

标签: scala f# tail-recursion tail-call-optimization


【解决方案1】:

Scala 中正确尾调用的问题是工程权衡之一。将 PTC 添加到 Scala 很容易:只需在 SLS 中添加一个句子。瞧:Scala 中的 PTC。从语言设计的角度来看,我们已经完成了。

现在可怜的编译器编写者需要实施该规范。好吧,用 PTC 将编译成语言很容易……但不幸的是,JVM 字节码不是这样的语言。好的,那么GOTO 呢?没有。续集?没有。异常(已知等同于延续)?啊,现在我们到了某个地方!因此,我们可以使用异常来实现 PTC。或者,我们也可以根本不使用 JVM 调用堆栈,而是实现我们自己的堆栈。

毕竟 JVM 上有多个 Scheme 实现,它们都支持 PTC 就好了。您不能在 JVM 上安装 PTC,只是因为 JVM 不支持它们,这是一个神话。毕竟,x86 也没有它们,但是,在 x86 上运行的语言有它们。

那么,如果在 JVM 上实现 PTC 是可能的,那么为什么 Scala 没有它们呢?就像我上面说的,您可以使用异常或您自己的堆栈来实现它们。但是对控制流使用异常或实现自己的堆栈意味着期望 JVM 调用堆栈以某种方式显示的所有内容都将不再有效。

特别是,您将失去与 Java 工具生态系统(调试器、可视化器、静态分析器)的几乎所有互操作性。您还必须建立与 Java 库互操作的桥梁,这会很慢,因此您也失去了与 Java 库生态系统的互操作性。

但这是 Scala 的主要设计目标!这就是 Scala 没有 PTC 的原因。

我将其称为“Hickey 定理”,以 Clojure 的设计师 Rich Hickey 的名字命名,他曾在一次演讲中说过“尾调用、互操作、性能 – 选择两个”。

您还会向 JIT 编译器展示一些非常不寻常的字节码模式,它可能不知道如何优化。

如果您要将 F# 移植到 JVM,您基本上必须做出正确的选择:您是否放弃尾调用(您不能,因为语言规范要求它们),您是否放弃 Interop还是你放弃性能?在 .NET 上,您可以同时拥有这三个,因为 F# 中的尾调用可以简单地编译为 MSIL 中的尾调用。 (虽然实际的翻译比这更复杂,而且在 MSIL 中尾调用的实现在某些极端情况下是错误的。)

这就提出了一个问题:为什么不向 JVM 添加尾调用?好吧,由于 JVM 字节码中的设计缺陷,这非常困难。设计者希望 JVM 字节码具有一定的安全属性。但是,不要以这样一种方式设计 JVM 字节码语言,即您一开始就不能编写不安全的程序(例如,在 Java 中,您不能编写违反指针安全的程序,因为该语言只是首先不让你访问指针),JVM 字节码本身是不安全的,需要一个单独的字节码验证器来保证它的安全。

该字节码验证器基于堆栈检查,并且尾调用更改堆栈。因此,两者非常难以协调,但 JVM 根本无法在没有字节码验证器的情况下工作。花了很长时间和一些非常聪明的人终于弄清楚如何在不丢失字节码验证器的情况下在 JVM 上实现尾调用(参见 A Tail-Recursive Machine with Stack Inspection by Clements and Felleisentail calls in the VM by John Rose (JVM lead designer)),所以我们现在已经从原来的阶段转移了一个开放的研究问题到它“只是”一个开放的工程问题的阶段。

请注意,Scala 和其他一些语言确实具有方法内直接尾递归。然而,这在实现方面非常无聊:它只是一个while 循环。大多数目标都有while 循环或类似的东西,例如JVM 有内部方法GOTO。 Scala 也有scala.util.control.TailCalls object,这是一种重新定义的蹦床。 (请参阅Stackless Scala With Free Monads by Rúnar Óli Bjarnason 了解这个想法的更通用版本,它可以消除 all 对堆栈的使用,而不仅仅是在尾调用中。)这可以用于实现尾调用算法Scala,但这与 JVM 堆栈不兼容,即它看起来不像是对其他语言或调试器的递归方法调用:

import scala.util.control.TailCalls._

def isEven(xs: List[Int]): TailRec[Boolean] =
  if (xs.isEmpty) done(true) else tailcall(isOdd(xs.tail))

def isOdd(xs: List[Int]): TailRec[Boolean] =
 if (xs.isEmpty) done(false) else tailcall(isEven(xs.tail))

isEven((1 to 100000).toList).result

def fib(n: Int): TailRec[Int] =
  if (n < 2) done(n) else for {
    x <- tailcall(fib(n - 1))
    y <- tailcall(fib(n - 2))
  } yield (x + y)

fib(40).result

Clojure 有 recur special form,它也是一个显式蹦床。

【讨论】:

  • +1 有趣的文章,谢谢。您能否为“一些非常聪明的人最终弄清楚如何在不丢失字节码验证器的情况下在 JVM 上实现尾调用”提供任何参考?我会对细节感兴趣。
  • Scala 的@tailrec 可能值得一提
  • @bbarker:方法内直接尾递归很无聊(从实现的角度来看)。它只是一个while 循环,大多数目标都支持。 (或者更准确地说,JVM有intra-methodGOTO,是一回事。)
【解决方案2】:

F# 没有尾调用的问题。它的作用如下:

  • 如果您有一个尾递归函数,编译器会生成一个带有突变的循环,因为这比生成 .tail 指令要快

  • 在其他尾调用位置(例如,当使用延续或两个相互递归的函数时),它会生成 .tail 指令,因此尾调用由 CLR 处理

  • 默认情况下,尾调用优化在 Visual Studio 的调试模式下关闭,因为这使调试更容易(您可以检查堆栈),但您可以在项目属性(如果需要)。

在过去,.tail 指令在某些运行时(CLR x64 和 Mono)上曾经存在问题,但现在所有这些问题都已修复,一切正常。

【讨论】:

【解决方案3】:

事实证明,对于正确的尾调用,您必须在“发布模式”而不是默认的“调试模式”下编译,或者打开项目属性,然后在“构建”菜单中,向下滚动并选中“生成尾调用”。感谢 IRC.freenode.net 上的 Arnavion #fsharp 的提示。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-13
    相关资源
    最近更新 更多