主要问题:我将尾调用优化(TCO)的最重要应用视为递归调用到循环的转换(在递归调用具有某种形式的情况下)。更准确地说,当翻译成机器语言时,这通常会转换成某种跳跃系列。编译为本机代码(例如SBCL)的一些Common Lisp和Scheme编译器可以识别尾递归代码并执行此转换。基于JVM的Lisp(例如Clojure和ABCL)在执行此操作时遇到了麻烦。 JVM作为一台可以防止或使其变得困难的机器是什么?我不明白。 JVM显然没有循环问题。编译器必须弄清楚如何进行TCO,而不是它编译的机器。
相关问题:Clojure 可以将看似递归的代码转换为循环:如果程序员用关键字recur
替换对函数的尾调用,它就好像正在执行TCO一样。但是,如果有可能让编译器识别尾调用 - 例如SBCL和CCL那么做 - 那么为什么Clojure编译器不会发现它应该以它对待{{1}的方式处理尾调用}?
(对不起 - 这无疑是一个常见问题解答,我确信上面的评论显示了我的无知,但我找不到先前的问题是不成功的。)
答案 0 :(得分:9)
Real TCO适用于尾部位置的任意调用,而不仅仅是自调用,因此以下代码不会导致堆栈溢出:
(letfn [(e? [x] (or (zero? x) (o? (dec x))))
(o? [x] (e? (dec x)))]
(e? 10))
显然,您需要JVM支持,因为在JVM上运行的程序无法操纵调用堆栈。 (除非你愿意建立自己的调用约定并在函数调用上强加相关的开销; Clojure的目的是使用常规的JVM方法调用。)
至于消除尾部位置的自调用,这是一个更简单的问题,只要整个函数体被编译为单个JVM方法就可以解决。然而,这是一个有限的承诺。此外,recur
因其明确性而备受青睐。
答案 1 :(得分:4)
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使用trampoline
或loop-recur
构造进行优化的情况。这里已经解释了这个决定:https://groups.google.com/d/msg/clojure/4bSdsbperNE/tXdcmbiv4g0J
答案 2 :(得分:2)
在ClojureConj 2014的最后一次演示中,Brian Goetz指出JVM中有一个安全功能可以防止堆栈帧崩溃(因为这对于那些希望在返回时将某个函数转移到其他地方的人来说是一个攻击向量)。
https://www.youtube.com/watch?v=2y5Pv4yN0b0&index=21&list=PLZdCLR02grLoc322bYirANEso3mmzvCiI