Clojure 为什么可以';在基于JVM的Lisps中是否可以优化尾部调用?

Clojure 为什么可以';在基于JVM的Lisps中是否可以优化尾部调用?,clojure,jvm,lisp,jvm-languages,abcl,Clojure,Jvm,Lisp,Jvm Languages,Abcl,主要问题:我认为尾部调用优化(TCO)最重要的应用是将递归调用转换为循环(在递归调用具有某种形式的情况下)。更准确地说,当翻译成机器语言时,这通常是翻译成某种跳跃。一些编译为本机代码的常见Lisp和Scheme编译器(例如SBCL)可以识别尾部递归代码并执行此转换。基于JVM的Lisp(如Clojure和ABCL)很难做到这一点。JVM作为一台机器,是什么阻止或使这变得困难?我不明白。JVM显然没有循环问题。编译器必须弄清楚如何进行TCO,而不是它编译到的机器 相关问题:Clojure可以将看似

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

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


(抱歉——这无疑是一个常见问题,我相信上面的评论表明了我的无知,但我没有成功地找到前面的问题。)

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

(letfn [(e? [x] (or (zero? x) (o? (dec x))))
        (o? [x] (e? (dec x)))]
  (e? 10))
显然,这需要JVM支持,因为JVM上运行的程序无法操作调用堆栈。(除非您愿意建立自己的调用约定并对函数调用施加相关开销;Clojure的目标是使用常规JVM方法调用。)


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

JVM不支持TCO有一个原因:

然而,有一种方法可以通过滥用堆内存和中解释的一些技巧来解决这个问题;它由Chris Frisz和Daniel P.Friedman在Clojure中实现(参见)


现在Rich Hickey可以选择在默认情况下进行这样的优化,Scala会在某些时候这样做。相反,他选择依靠最终用户指定可通过Clojure使用
蹦床
循环重现
构造进行优化的情况。在这里已经解释了:

< P>在CuljurCurj2014的最后呈现中,Brian Goetz指出JVM中有一个安全特性,它防止堆栈帧崩溃(因为这将是一个攻击向量,用于让人们在返回时在其他地方运行一个函数)。

有关里奇·希基对这一主题的评论,请看:谢谢@Jared314。该线程的其余部分也很有用。我应该搜索Google group.TCO对所有尾部调用都有效,而不仅仅是递归调用;它并不适用于所有递归调用,只适用于尾部调用。如果你要编辑你的第一句话,这样就不会传播对TCO的误解,这可能会很好。好的,说得好@Rörd。我已经编辑了第一句话。谢谢Michal。当尾部递归在返回调用方之前通过一个或多个其他函数时,仅通过分析源代码(或一些相对简单的源代码转换)很难实现TCO。这需要以某种方式操纵调用堆栈吗?(顺便说一句,这个例子在奇数上破坏了堆栈,但在偶数上调用时仍然说明了您的观点。)我从@Jared314引用的Google clojure组线程(其中包括您自己的有用注释)中看到,JVM
goto
s保留在方法中,方法是JVM代码的基本结构。我现在明白了。只要基于JVM的语言将函数表示为方法(这无疑是可取的),那么将多函数尾部递归转换为循环就没有简单的方法。可以说,
goto
走不了那么远。真正的CPU机器语言通常没有这样的限制。另外,看看这个和这个,看看与自动TCO相关的工作。@Mars实现真正TCO最有效的方法是用一个适合尾部调用的堆栈帧替换最后的堆栈帧,但要使用当前调用的返回地址(取自被替换的帧;另一种查看方式是最终帧被部分重写,但返回地址保持不变)。因此,没有全局转换成类似于单个大型命令循环的东西;相反,结构看起来仍然像函数调用,但尾部调用链直接返回到启动它们的位置,没有中间返回。@Mars因此,即使是常规的
goto
也不够,您还需要能够操纵调用堆栈上的最后一帧。在任何情况下,JVM上执行的代码的结构限制都是问题所在。为了避免这些限制,人们可以采取蹦床的方式,但会降低性能。有关详细讨论,请参阅Wikipedia的文章。必须指出,引用的“原因”为什么JVM不支持TCO是站不住脚的。(请注意JVM!=Java,尽管它也在那里会很好。)“JVM不支持TCO有一个原因:为什么JVM仍然不支持尾部调用优化?”那么scala是如何实现的?