为什么不能在基于JVM的Lisps中优化尾调用?

Mar*_*ars 21 lisp jvm clojure jvm-languages abcl

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

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

(对不起 - 这无疑是一个常见问题解答,我确信上面的评论显示了我的无知,但我找不到早期的问题是不成功的.)

Mic*_*zyk 9

Real TCO works for arbitrary calls in tail position, not just self calls, so that code like the following does not cause a stack overflow:

(letfn [(e? [x] (or (zero? x) (o? (dec x))))
        (o? [x] (e? (dec x)))]
  (e? 10))
Run Code Online (Sandbox Code Playgroud)

Clearly you'd need JVM support for this, since programs running on the JVM cannot manipulate the call stack. (Unless you were willing to establish your own calling convention and impose the associated overhead on function calls; Clojure aims to use regular JVM method calls.)

至于消除尾部位置的自调用,这是一个更简单的问题,只要整个函数体被编译为单个JVM方法就可以解决.然而,这是一个有限的承诺.此外,recur它的显性非常受欢迎.

  • 我从Google clojure组线程中看到@Jared314引用(其中包括您自己的有用评论)JVM"goto"保留在方法中,而方法是JVM代码的基本结构.我现在明白了.只要基于JVM的语言将函数表示为方法,这无疑是可取的,将多功能尾递归转换为循环是不容易的.可以这么说,'goto`不能走那么远.通常,真正的CPU机器语言没有这样的限制. (3认同)
  • @Mars实现真正TCO的最有效方法是用适合尾部调用的最终堆栈帧替换最终堆栈帧,但是使用当前调用的返回地址(取自正在替换的帧;另一种查看方法的方法是最终帧被部分重写,但返回地址保持不变).因此,没有全局转变为类似于一个巨大的命令循环的东西; 相反,结构仍然看起来像函数调用,但尾调用链直接返回到它们所启动的位置,没有中间返回. (2认同)