【问题标题】:Why can't tail calls be optimized in JVM-based Lisps?为什么不能在基于 JVM 的 Lisps 中优化尾调用?
【发布时间】:2013-10-28 01:03:29
【问题描述】:

主要问题:我认为尾调用优化 (TCO) 最重要的应用是将递归调用转换为循环(在递归调用具有特定形式的情况下)。更准确地说,当翻译成机器语言时,这通常会翻译成某种系列的跳跃。一些编译为本机代码(例如 SBCL)的 Common Lisp 和 Scheme 编译器可以识别尾递归代码并执行此翻译。 Clojure 和 ABCL 等基于 JVM 的 Lisps 很难做到这一点。 JVM 是什么机器可以防止或使这变得困难?我不明白。 JVM 显然没有循环问题。必须弄清楚如何进行 TCO 的是编译器,而不是它编译到的机器。

相关问题:Clojure 可以将看似递归的代码转换为循环:如果程序员用关键字recur 替换函数的尾调用,它就好像在执行TCO。但是,如果有可能让编译器识别尾调用(例如 SBCL 和 CCL 所做的),那么为什么 Clojure 编译器不能确定它应该以处理 recur 的方式处理尾调用?

(对不起——这无疑是一个FAQ,我确信上面的评论表明我的无知,但我没有成功找到之前的问题。)

【问题讨论】:

  • 关于 Rich Hickey 对该主题的评论,请看:groups.google.com/d/msg/clojure/4bSdsbperNE/tXdcmbiv4g0J
  • 谢谢@Jared314。该线程的其余部分也很有帮助。我应该搜索一下 Google 群组。
  • TCO 适用于所有尾调用,而不仅仅是递归调用;它不适用于所有递归调用,仅适用于尾调用。如果您要编辑您的第一句话可能会很好,这样它就不会传播对 TCO 的误解。
  • 好的,好点@Rörd。我已经编辑了第一句话。

标签: clojure jvm lisp jvm-languages abcl


【解决方案1】:

真正的 TCO 适用于尾部位置的任意调用,而不仅仅是自调用,因此如下代码不会导致堆栈溢出:

(letfn [(e? [x] (or (zero? x) (o? (dec x))))
        (o? [x] (e? (dec x)))]
  (e? 10))

显然,您需要 JVM 支持,因为在 JVM 上运行的程序无法操纵调用堆栈。 (除非您愿意建立自己的调用约定并将相关的开销强加于函数调用;Clojure 旨在使用常规 JVM 方法调用。)

至于消除尾部位置的self调用,这是一个更简单的问题,只要将整个函数体编译为单个JVM方法就可以解决。然而,这是一个有限的承诺。此外,recur 因其明确性而颇受欢迎。

【讨论】:

  • 谢谢米哈尔。当尾递归在返回给调用者之前经过一个或多个其他函数时,纯粹通过分析源代码(或一些相对简单的源代码转换)很难实现 TCO 的想法。这需要以某种方式操作调用堆栈? (顺便说一下,这个例子在奇数上破坏了堆栈,但是当它在偶数上调用时仍然说明了你的观点。)
  • 我从@Jared314 引用的Google clojure 组线程中看到(其中包括您自己有用的cmets),JVM gotos 保留在方法中,方法是JVM 代码的基本结构。我现在明白了。只要基于 JVM 的语言将函数表示为方法,这无疑是可取的,就没有简单的方法将多功能尾递归转换为循环。 goto 不能走那么远,可以这么说。真正的 CPU 机器语言通常没有这样的限制。
  • 另外,请查看Conj 2012 presentationrepo,了解与自动 TCO 相关的工作。
  • @Mars 实现真正 TCO 的最有效方法是将最终堆栈帧替换为适合尾调用的堆栈帧,但使用当前调用的返回地址(取自被替换的帧;另一种看法是最后一帧被部分重写,但返回地址保持不变)。因此,没有全局转换为类似于单个巨大的命令循环的东西;相反,结构看起来仍然像函数调用,但尾调用链直接返回到它们开始的位置,没有中间返回。
  • @Mars 所以,即使是一般的goto 也不够用,您还需要能够操作调用堆栈上的最后一帧。无论如何,在 JVM 上执行代码的结构限制确实是问题所在。为了绕过它们,可以求助于蹦床,但会降低性能。另请参阅 Wikipedia 的 Tail call 文章以获得很好的讨论。
【解决方案2】:

JVM不支持TCO是有原因的:Why does the JVM still not support tail-call optimization?

但是有一种方法可以通过滥用堆内存和A First-Order One-Pass CPS Transformation paper 中解释的一些技巧来解决此问题;它由 Chris Frisz 和 Daniel P. Friedman 在 Clojure 中实现(参见 clojure-tco)。

现在 Rich Hickey 可以选择默认进行这样的优化,Scala 在某些时候会这样做。相反,他选择依靠最终用户来指定可以通过 Clojure 使用 trampolineloop-recur 构造优化的情况。该决定已在此处解释:https://groups.google.com/d/msg/clojure/4bSdsbperNE/tXdcmbiv4g0J

【讨论】:

  • 必须说,JVM不支持TCO的“理由”是站不住脚的。 (注意 JVM != Java,虽然它也在那里当然很好。)
  • "JVM不支持TCO是有原因的:为什么JVM还不支持tail-call优化?"那么scala是如何实现的呢?
【解决方案3】:

在 ClojureConj 2014 的最后一次演示中,Brian Goetz 指出 JVM 中有一个安全功能可以防止堆栈帧崩溃(因为对于希望将函数返回到其他地方的人来说,这将是一个攻击向量)。

https://www.youtube.com/watch?v=2y5Pv4yN0b0&index=21&list=PLZdCLR02grLoc322bYirANEso3mmzvCiI

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-05-08
    • 2010-09-11
    • 2012-01-04
    • 2015-01-18
    • 1970-01-01
    • 1970-01-01
    • 2019-12-26
    相关资源
    最近更新 更多