为什么Java根本没有优化尾递归?


92

据我所读:原因是因为在继承时,要确定要实际调用的方法并不容易。

但是,为什么Java至少没有对静态方法进行尾递归优化并强制使用正确的方法来用编译器调用静态方法?

为什么Java根本不支持尾递归?

我不确定这里是否有任何困难。


关于建议的副本,如JörgW Mittag 1所述

  • 另一个问题是关于TCO的,这个是关于TRE的。TRE比TCO简单得多。
  • 此外,另一个问题询问JVM对希望编译为JVM的语言实现施加哪些限制,该问题询问Java,这是不受JVM限制的一种语言,因为JVM规范可以通过以下方式更改:设计Java的人
  • 最后,JVM中甚至没有关于TRE的限制,因为JVM确实具有方法内GOTO,这是TRE所需的全部

1 添加了格式以标注点。


26
引用埃里克·利珀特(Eric Lippert)的话“功能并不便宜;它们非常昂贵,而且它们不仅必须证明自己的成本是合理的,而且还必须证明不使用该预算可以完成的其他一百项功能所带来的机会成本。” Java的设计在某种程度上吸引了C / C ++开发人员,并且不能保证使用这些语言进行尾部递归优化。
2015年

5
如果我错了,请纠正我,但是Eric Lippert是C#的设计者吗?它具有尾递归优化功能?
InformedA 2015年

1
是的,他在C#编译器团队中。
2015年

10
据我了解,在JIT可能做到这一点如果某些条件得到满足,也许。因此在实践中,您不能依靠它。但是,无论C#是否拥有它,对于Eric而言都不重要。
2015年

4
@InstructedA如果深入研究,您会发现它从未在32位JIT编译器中完成。64位JIT在许多方面都更新,更智能。甚至更新的实验性编译器(适用于32位和64位)都更加智能,并且将支持IL中没有明确要求的尾递归优化。您还需要考虑另一点-JIT编译器没有很多时间。它们对速度进行了严格的优化-可能需要花费数小时才能在C ++中进行编译的应用程序,最多(至少部分)仍需要几百毫秒从IL到本机。
a安2015年

Answers:


132

正如本视频中的 Oracle Java语言架构师Brian Goetz所解释的:

在jdk类中,有许多对安全性敏感的方法,它们依赖于对jdk库代码和调用代码之间的堆栈帧进行计数,以确定谁在调用它们。

更改堆栈上的帧数的任何操作都会破坏此情况并导致错误。他承认这是一个愚蠢的原因,因此JDK开发人员已替换了此机制。

然后他进一步提到这不是优先事项,而是尾递归

最终会完成的。

注意:这适用于HotSpot和OpenJDK,其他VM可能会有所不同。


7
我很惊讶这样一个相当干脆的答案!但这似乎确实是个答案-由于过时的技术原因,现在有可能还没有完成,所以现在我们只等某人确定它足够重要就可以实施。
BrianH 2015年

1
为什么不实施解决方法?就像一个隐藏在标签下的标签,它只是跳转到方法调用的顶部,并带有新的参数值?这将是编译时的优化,并且不需要移动堆栈帧或引起安全冲突。
詹姆斯·沃特金斯

2
如果您或这里的其他人可以更深入地研究并提供那些“安全敏感方法”是什么,那对我们会更好。谢谢!
InformedA 2015年

3
@InstructedA -见securingjava.com/chapter-three/chapter-three-6.html其中包含的Java安全管理系统是如何工作的大约2的Java的发行的详细描述
朱尔斯

3
一种解决方法是使用另一种JVM语言。例如,Scala不在编译器中执行此操作。
詹姆斯·摩尔

24

Java没有尾调用优化,原因与大多数命令式语言没有的原因相同。命令式循环是该语言的首选样式,程序员可以用命令式循环代替尾部递归。对于不建议使用其样式的功能,复杂性是不值得的。

程序员有时希望用其他命令性语言以FP风格编写的东西仅在计算机开始在内核而不是在GHz范围内扩展后的最近十年左右才流行起来。即使到现在,它也不是很流行。如果我建议在工作中使用尾部递归来替换命令式循环,那么一半的代码审阅者会大笑,而另一半会给人以困惑的印象。即使在函数式编程中,您也通常避免尾部递归,除非其他构造(例如高阶函数)不能完全匹配。


36
这个答案似乎不正确。仅仅因为不严格要求尾递归并不意味着它就没有用。递归解决方案通常比迭代更容易理解,但是缺少尾部调用意味着递归算法对于大问题的规模变得不正确,这会破坏堆栈。这是关于正确性,而不是性能(为简单起见,交易速度通常是值得的)。正如正确答案所指出的,缺少尾部调用是由于依赖于堆栈跟踪而不是命令性偏执的怪异的安全模型所致。
阿蒙2015年

4
@amon James Gosling曾经说过,除非有多个人提出要求否则他不会向Java添加功能,只有这样他才会考虑。因此,如果答案的一部分确实是“您可以始终使用for循环”(对于一流的功能和对象同样如此),我也不会感到惊讶。我不会称其为“命令性偏执”,但我不认为它在1995年时有很高的需求,当时最主要的问题可能是Java的速度和缺少泛型。
2015年

7
@amon,一个安全的模型使我感到震惊,因为没有将TCO添加到现有的语言中是一个合理的理由,但是一个很糟糕的理由却是不首先将其设计为该语言。您不会为了包含次要的幕后功能而抛出主要的程序员可见功能。“为什么Java 8没有TCO”和“为什么Java 1.0没有TCO?”是一个非常不同的问题。我正在回答后者。
Karl Bielefeldt

3
@rwong,您可以将任何递归函数转换为迭代函数。(如果可以的话,我在这里写了一个例子)
alain

3
@ZanLynx取决于问题的类型。例如,访客模式可以从TCO(取决于结构和访客的细节)中受益,而与FP无关。尽管使用蹦床进行转换似乎更自然(取决于问题),但通过状态机还是一样。同样,尽管从技术上讲,您可以使用循环来实现树,但我相信共识是​​递归更为自然(对于DFS来说,尽管在这种情况下,即使使用TCO,由于堆栈限制,您也可以避免递归)。
Maciej Piechotka

5

Java没有严格的调用优化,因为JVM没有用于尾部调用的字节码(针对某些静态未知的函数指针,例如某些vtable中的方法)。

似乎出于社会(也许是技术上)的原因,在JVM规范的拥有者中很难在JVM中添加新的字节码操作(这将使其与该JVM的早期版本不兼容)。

不在JVM规范中添加新字节码的技术原因包括现实生活中的JVM实现是非常复杂的软件(例如,由于它正在执行许多JIT优化)。

尾部调用某些未知函数需要用新的栈帧替换当前的栈帧,并且该操作应位于JVM中(这不仅仅是更改生成字节码的编译器)。


17
问题不是关于尾调用,而是关于尾递归。问题不是关于JVM字节码语言,而是关于Java编程语言,这是一种完全不同的语言。Scala编译为JVM字节码(以及其他),并消除了尾递归。JVM上的方案实现具有完整的正确尾调用。Java 7(JVM Spec,第三版)添加了新的字节码。即使不需要特殊的字节码,IBM的J9 JVM也可以执行TCO。
约尔格W¯¯米塔格

1
@JörgWMittag:是的,但是然后,我想知道Java编程语言没有正确的尾调用是否是真的。这可能是更准确地指出了Java编程语言不具备有适当的尾调用,在规范的任务也没有什么。(也就是说:我不确定规范中的任何内容实际上都禁止实现消除尾部调用。只是没有提及。)
ruakh 2015年

4

除非一种语言具有进行尾部调用的特殊语法(递归或其他方式),并且当请求尾部调用但无法生成尾部调用时编译器将发出嘎嘎叫声,否则“可选”尾部调用或尾部递归优化将产生以下情况:一台计算机上的一堆代码可能需要少于100个字节的堆栈,而另一台计算机上则需要超过100,000,000个字节的堆栈。这种差异应被视为定性的,而不仅仅是定量的。

可以预期,机器的堆栈大小可能会有所不同,因此,代码始终可以在一台机器上工作,而将堆栈炸毁在另一台机器上。但是,通常,即使人为地限制了堆栈,也可以在一台计算机上运行的代码可能会在所有具有“正常”堆栈大小的计算机上运行。但是,如果在一台机器上优化了递归深度为1,000,000的方法,而不是在另一台机器上进行尾调用优化,则即使其堆栈异常小,也可以在前一台计算机上执行,而即使堆栈异常大,也可以在后者上执行失败。


2

我认为Java中不使用尾调用递归,主要是因为这样做会改变堆栈跟踪,从而使调试程序变得更加困难。我认为Java的主要目标之一是允许程序员轻松调试他们的代码,而堆栈跟踪对于做到这一点至关重要,尤其是在高度面向对象的编程环境中。由于可以改用迭代,因此语言委员会必须认为不值得添加尾递归。

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.