为什么TCO需要VM的支持?

时间:2014-04-19 20:57:15

标签: clojure lisp tail-recursion tail-call-optimization

据说一些虚拟机,尤其是JVM,不支持TCO。因此,像Clojure这样的语言要求用户改为使用loop recur

但是,我可以重写自尾调用以使用循环。例如,这是一个尾调用阶乘:

def factorial(x, accum):
    if x == 1:
        return accum
    else:
        return factorial(x - 1, accum * x)

这是一个等效的循环:

def factorial(x, accum):
    while True:
        if x == 1:
            return accum
        else:
            x = x - 1
            accum = accum * x

这可以由编译器完成(我编写的宏也是这样做的)。对于相互递归,您可以简单地内联您正在调用的函数。

因此,鉴于您可以在不需要任何VM的情况下实现TCO,为什么不使用语言(例如Clojure,Armed Bear Common Lisp)呢?我错过了什么?

3 个答案:

答案 0 :(得分:11)

由于多种原因,内联不是一般尾部调用消除问题的解决方案。以下列表并非详尽无遗。然而,它被排序 - 它以一种不便开始,并以一个完整的showstopper结束。

  1. 在尾部位置调用的函数可能很大,在这种情况下,从性能角度来看,它可能是不可取的。

  2. 假设g中的f有多个尾调用。在内联的常规定义下,您必须在每个呼叫站点内联g,这可能会使f变大。如果您选择goto g的开头然后再跳回来,那么你需要记住跳转到哪里,突然之间你维护自己的调用堆栈片段(几乎会与“真正的”调用堆栈相比,肯定表现出较差的性能。

  3. 对于相互递归的函数fg,您必须在fggf内嵌goto }。显然,根据通常的内联定义,这是不可能的。所以,你留下的是一个有效的自定义函数调用约定(如上面基于(defn say-foo-then-call [f] (println "Foo!") (f)) 的方法)。

  4. Real TCE在尾部位置使用任意调用,包括在高阶上下文中:

    {{1}}

    这可以在某些情况下发挥很大作用,但显然不能用内联来模拟。

答案 1 :(得分:1)

  1. 这可能会令人惊讶,并且可能会使调试变得更难,因为您无法看到调用堆栈。

  2. 它仅适用于非常特殊的情况,而不是VM支持TCO时处理的一般情况。

  3. 程序员通常不会以递归方式编写代码,除非语言激励他们这样做。例如。递归阶乘通常用递归步骤n * fact(n-1)编写,这不是尾递归。

答案 2 :(得分:1)

TCO 本身并不需要VM支持。也就是说,不适用于本地功能。跨越外部功能的尾部调用需要VM支持。理想情况下,尾递归的完整实现允许单独编译的程序单元中的函数在常量空间中相互递归,不仅是一个父函数本地的函数,也不是一次对编译器都可见的函数。

在没有尾调用支持的VM中,函数调用被封装,并在退出时分配新帧。尾调用需要一个绕过它的特殊入口点。函数可以参与尾调用以及非尾调用,因此它们都需要两个入口点。

可以在没有VM支持的情况下使用非本地返回和分派来模拟尾部呼叫消除。也就是说,当尾部调用在语法上发生时,它被转换为非本地返回,它通过动态控制传递放弃当前函数,将参数(可能打包为对象)传递给隐藏的调度循环,将控制权转移给目标函数,并将这些参数传递给它。这将实现递归发生在恒定空间中并且将具有“外观和感觉”的要求。喜欢尾巴打电话。但是,它很慢,也许并不完全透明。