为什么JVM仍然不支持尾调用优化?


95

在进行了-jvm-prevent-tail-call-optimizations之后的两年,似乎有一个原型 实现,并且MLVM一段时间以来将该功能列为“ proto 80%”。

Sun / Oracle方面对支持尾部调用是否没有积极的兴趣,或者仅仅是JVM提到尾部调用“ 注定要在每个功能优先级列表上排在第二位 ”。语言峰会

如果有人测试了MLVM构建并可以分享其运行效果的印象(如果有的话),我将非常感兴趣。

更新: 请注意,某些虚拟机(如Avian)支持正确的尾部调用,而没有任何问题。


14
随着太阳的人从甲骨文的报道出走,我不希望是任何当前项目的继续,除非所以从Oracle :(明确表示
托尔比约恩Ravn的安徒生

16
请注意,您接受的答案是完全错误的。尾部调用优化和OOP之间没有根本的冲突,当然,像OCaml和F#这样的几种语言都具有OOP和TCO。
JD

2
好吧,首先调用OCaml和F#OOP语言是一个不好的笑话。但是,是的,OOP和TCO没有太多共同点,只是运行时必须检查要优化的方法没有在其他地方重写/子类化。
soc

5
+1来自C背景,我一直认为TCO在任何现代JVM中都是给定的。我从来没有真正进行过检查,当我这样做时,结果令人惊讶……
thkala 2011年

2
@soc:“除了运行时必须检查要优化的方法未在其他地方重写/子类化这一事实之外”。您的“事实”完全是胡说八道。
JD

Answers:


32

诊断Java代码:提高Java代码的性能alt)解释了为什么JVM不支持尾调用优化。

但是,尽管众所周知如何将尾递归函数自动转换为简单循环,但是Java规范并不要求进行这种转换。大概没有原因的一个原因是,通常,不能以面向对象的语言静态地进行转换。相反,必须由JIT编译器动态完成从尾递归函数到简单循环的转换。

然后给出了一个不会转换的Java代码示例。

因此,如清单3中的示例所示,我们不能期望静态编译器在保留Java语言的语义的同时对Java代码执行尾部递归转换。相反,我们必须依靠JIT进行动态编译。根据JVM,JIT可能会或可能不会这样做。

然后它提供了一个测试,您可以用来确定您的JIT是否这样做。

自然,由于这是IBM论文,因此它包含一个插件:

我使用几个Java SDK运行了该程序,结果令人惊讶。在Sun的1.3版Hotspot JVM上运行后,发现Hotspot不执行转换。在默认设置下,计算机上的堆栈空间用完不到一秒钟。另一方面,IBM的1.3版JVM发出了毫无问题的提示,表明它确实以这种方式转换了代码。


62
FWIW,尾部调用不仅仅像他暗示的那样是自递归函数。尾部调用是出现在尾部位置的任何函数调用。它们不必是对自身的调用,也不必是对静态已知位置的调用(例如,它们可以是虚拟方法调用)。如果在一般情况下正确进行了尾部调用优化,那么他描述的问题就不成问题了,因此,他的示例在支持尾部调用的面向对象语言(例如OCaml和F#)中完美运行。
JD

3
“必须由JIT编译器动态完成”,这意味着必须由JVM本身而不是Java编译器完成。但是OP正在询问有关JVM的信息。
Raedwald,

11
“通常,不能以面向对象的语言静态地进行转换。” 当然,这是一句名言,但是每次我看到这样的借口时,我都会想问数字-因为如果在大多数情况下在实践中可以在编译时建立数字,我不会感到惊讶。
greenoldman '02

5
引用的文章的链接现已断开,尽管Google确实对其进行了缓存。更重要的是,作者的推理是错误的。给出的实例可以是尾部调用优化,使用静态和只是动态编译,如果仅编译器插入的instanceof检查,看看是否this是一个Example对象(而不是一个子类的Example)。
Alex D


30

过去我没有在Java中实现TCO(这被认为很困难)的原因之一是,JVM中的权限模型对堆栈敏感,因此尾调用必须处理安全性方面。

我相信Clements和Felleisen [1] [2]证明这并不是障碍,而且我很确定问题中提到的MLVM补丁也可以解决这个问题。

我意识到这并不能回答您的问题;只是添加有趣的信息。

  1. http://www.ccs.neu.edu/scheme/pubs/esop2003-cf.pdf
  2. http://www.ccs.neu.edu/scheme/pubs/cf-toplas04.pdf

1
+1。在Brian Brian演讲的结尾处聆听问题/答案youtube.com/watch?v=2y5Pv4yN0b0&t=3739
mcoolive

15

也许您已经知道这一点,但是该功能并不像听起来那么简单,因为Java语言实际上向程序员公开了堆栈跟踪。

考虑以下程序:

public class Test {

    public static String f() {
        String s = Math.random() > .5 ? f() : g();
        return s;
    }

    public static String g() {
        if (Math.random() > .9) {
            StackTraceElement[] ste = new Throwable().getStackTrace();
            return ste[ste.length / 2].getMethodName();
        }
        return f();
    }

    public static void main(String[] args) {
        System.out.println(f());
    }
}

即使它有一个“尾部调用”,它也可能没有被优化。(如果它优化,但仍需要自的程序依赖于它的语义整个调用堆栈的簿记。)

基本上,这意味着在仍然向后兼容的同时很难支持它。


17
在您的思想中发现了一个错误:“由于程序的语义依赖于此,因此需要对整个调用堆栈进行记账”。:-)就像新的“抑制的异常”。依赖于此类事物的程序必定会中断。在我看来,程序的行为是绝对正确的:丢弃堆栈帧是尾调用的全部目的。
soc 2010年

4
@Marco,但是几乎任何方法都可能引发异常,从该异常中整个调用堆栈必然可用,对吗?此外,g在这种情况下,您无法预先确定将间接调用哪些方法...例如,考虑多态性和反射。
aioobe 2010年

2
这是由Java 7中添加ARM引起的副作用。这是一个示例,您不能依赖上面显示的内容。
soc 2010年

6
“该语言公开了调用栈的事实使其难以实现”:该语言是否要求getStackTrace()x()源代码显示的方法返回的堆栈跟踪是从方法调用的,y()还显示x()从调用的y()?因为如果有一些自由就没有真正的问题。
Raedwald,

8
这仅是措辞单一方法规范的问题,从“给您所有堆栈框架”到“给您所有活动堆栈框架,省去了被尾部调用淘汰的堆栈”。此外,无论是否履行尾叫操作,都可以将其设为命令行开关或系统属性。
Ingo 2013年

12

Java是您可能想到的功能最少的语言(好吧,也许不是!),但这对于JVM语言(例如Scala)将是一个巨大的优势。

我的观察是,使JVM成为其他语言的平台似乎从来就不是Sun的优先事项,我想现在是Oracle。


16
@Thorbjørn-我编写了一个程序来预测任何给定的程序是否会在有限的时间内停止。我花了好长时间
oxbow_lakes

3
我使用的第一个BASIC没有功能,但是有GOSUB和RETURN。我也不认为LOLCODE的功能也很强(您可以从两种意义上讲)。
David Thornley'9

1
@David,功能性!=具有功能。
托尔比约恩Ravn的安徒生

2
@ThorbjørnRavn Andersen:不,但这是先决条件,您不是说吗?
David Thornley'9

3
“使JVM成为其他语言的平台似乎从未在Sun的优先级列表中居首位”。他们为使JVM成为动态语言的平台投入了比功能语言更多的精力。
JD
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.