JVM对尾调用优化施加哪些限制


36

Clojure不会单独执行尾部调用优化:当您具有尾部递归函数并且想要对其进行优化时,必须使用特殊形式recur。同样,如果您有两个相互递归的函数,则只能使用来优化它们trampoline

Scala编译器能够为递归函数执行TCO,但不能为两个相互递归函数执行TCO。

每当我阅读这些限制时,它们总是归因于JVM模型固有的某些限制。我对编译器一无所知,但这使我有些困惑。让我举一个例子Programming Scala。这里的功能

def approximate(guess: Double): Double =
  if (isGoodEnough(guess)) guess
  else approximate(improve(guess))

被翻译成

0: aload_0
1: astore_3
2: aload_0
3: dload_1
4: invokevirtual #24; //Method isGoodEnough:(D)Z
7: ifeq
10: dload_1
11: dreturn
12: aload_0
13: dload_1
14: invokevirtual #27; //Method improve:(D)D
17: dstore_1
18: goto 2

因此,在字节码级别,只需要一个goto。实际上,在这种情况下,繁琐的工作由编译器完成。

基础虚拟机的什么功能将使编译器更轻松地处理TCO?

附带说明一下,我不希望实际的计算机比JVM更智能。尽管如此,许多编译为本机代码的语言(例如Haskell)似乎在优化尾调用方面没有问题(嗯,Haskell有时可能由于懒惰而遇到问题,但这是另一个问题)。

Answers:


25

现在,我对Clojure的了解不多,对Scala的了解也不多,但是我会给它一个机会。

首先,我们需要区分tail-CALL和tail-RECURSION。尾递归确实很容易转换成循环。对于尾部调用,在一般情况下很难甚至不可能实现。您需要知道被调用的内容,但是对于多态和/或一流的功能,您几乎不知道,因此编译器无法知道如何替换该调用。只有在运行时,您才知道目标代码,并且可以在不分配另一个堆栈框架的情况下跳转到目标代码。例如,以下片段有一个尾部调用,并且在经过适当优化(包括TCO)时不需要任何堆栈空间,但是在为JVM进行编译时无法消除它:

function forward(obj: Callable<int, int>, arg: int) =
    let arg1 <- arg + 1 in obj.call(arg1)

虽然这只是一点点的低效率,但还是有很多编程风格(例如Continuation Passing Style或CPS)具有大量的尾部调用,并且很少返回。在没有完全TCO的情况下执行此操作意味着您只能在耗尽堆栈空间之前运行少量代码。

基础虚拟机的什么功能将使编译器更轻松地处理TCO?

一条尾部调用指令,例如在Lua 5.1 VM中。您的示例并没有变得简单得多。我的变成这样的事情:

push arg
push 1
add
load obj
tailcall Callable.call
// implicit return; stack frame was recycled

附带一提,我不希望实际的计算机比JVM更智能。

你是对的,他们不是。实际上,它们不那么聪明,因此甚至不了解堆栈框架之类的东西。这就是为什么人们可以在不推送返回地址的情况下重用堆栈空间和跳转到代码之类的技巧。


我懂了。我没有意识到不那么聪明可以进行优化,否则该优化将被禁止。
安德里亚(Andrea)2012年

7
+1,tailcall早在2007年就已经提出了有关JVM的指令:通过回溯机在sun.com上发布博客。在Oracle接管之后,此链接为404。我猜它没有进入JVM 7优先级列表。
K.Steff

1
一条tailcall指令只会将尾部调用标记为尾部调用。JVM是否随后实际上优化了所述的尾部调用则是一个完全不同的问题。CLI CIL具有.tail指令前缀,但是很长时间以来,Microsoft 64位CLR并未对其进行优化。OTOH,IBM J9 JVM 确实可以检测到尾部调用并对其进行优化,而无需特殊的指令来告诉它哪些调用是尾部调用。注释尾调用和优化尾调用实际上是正交的。(除了静态推断出哪个呼叫是尾呼叫可能还是不确定,还不是不确定的。Dunno。)
JörgW Mittag

@JörgWMittag您说得很对,JVM可以轻松检测到模式call something; oreturn。JVM规范更新的主要工作不是引入明确的尾调用指令,而是要求对此类指令进行优化。这样的一条指令只会使编译器编写者的工作变得更容易:JVM作者不必确保在该指令序列无法识别之前就能够识别出该指令序列,并且X-> bytecode编译器可以放心地确保其字节码无效或实际进行了优化,从不纠正而是堆栈溢出。

@delnan:call something; return;仅当被调用的对象从不要求堆栈跟踪时,该序列才等效于尾部调用;如果所讨论的方法是虚拟方法或调用虚拟方法,则JVM将无法知道是否可以查询堆栈。
2015年

12

Clojure 可以将尾递归自动执行为循环的自动优化:正如Scala所证明的,在JVM上当然可以这样做。

实际上,这是设计决定,不要执行此操作- recur如果要使用此功能,则必须明确使用特殊格式。请参阅邮件线程“ Re:为什么不对 Clojure google组进行尾部优化 ”。

在当前的JVM上,唯一不可能做的是不同函数之间的尾部调用优化(相互递归)。实现起来并不是特别复杂(Scheme等其他语言从一开始就具有此功能),但是需要更改JVM规范。例如,您必须更改有关保留完整函数调用堆栈的规则。

JVM的将来版本可能会获得此功能,尽管可能会作为一种选择,以便维护旧代码的向后兼容行为。说,Geeknizer的功能预览列出了Java 9的功能:

添加尾声和继续...

当然,未来的路线图总是会发生变化。

事实证明,这没什么大不了的。在过去两年的Clojure编码中,我从未遇到缺乏TCO成为问题的情况。这样做的主要原因是:

  • 使用recur或循环,您已经可以在99%的常见情况下获得快速的尾部递归。互尾递归的情况在普通代码中很少见
  • 即使需要相互递归,递归深度通常也足够浅,以至于无论如何您都可以在没有TCO的情况下在堆栈上进行递归。毕竟,TCO只是一种“优化”。
  • 在极少数情况下,您确实需要某种形式的不消耗堆栈的相互递归,那么还有很多其他选择可以达到相同的目标:惰性序列,蹦床等。

“未来迭代” -Geeknizer的功能预览针对Java 9:添加尾部调用和延续 -是吗?
蚊蚋

1
是的-就是这样。当然,未来的发展蓝图是会随时改变....
mikera

5

附带一提,我不希望实际的计算机比JVM更智能。

这不是要变得更聪明,而是要与众不同。直到最近,JVM仍专门针对单一语言(显然是Java)进行设计和优化,该语言具有非常严格的内存和调用模型。

不仅没有goto指针,也没有指针来调用“裸”函数(不是在类中定义的方法)的任何方法。

从概念上讲,在针对JVM时,编译器编写者必须问“如何用Java术语表达这个概念?”。显然,没有办法用Java表达TCO。

请注意,这些不被视为JVM的故障,因为Java不需要它们。Java一旦需要此类功能,就将其添加到JVM。

直到最近Java当局才开始认真考虑将JVM用作非Java语言的平台,因此它已经获得了与Java等效的功能的一些支持。最有名的是动态类型,它已经在JVM中,但在Java中却没有。


3

因此,在字节码级别,只需要转到即可。实际上,在这种情况下,繁琐的工作由编译器完成。

您是否注意到方法地址以0开头?这一切方法ofsets 0开始?JVM不允许人们跳出一种方法。

我不知道Java加载方法外的具有偏移量的分支会发生什么-也许它将被字节码验证程序捕获,也许会生成异常,也许实际上会跳出方法之外。

当然,问题在于您不能真正保证同一类的其他方法在哪里,更不用说其他类的方法了。我很怀疑JVM是否保证将在何处加载方法,尽管我很乐意得到纠正。


好点子。但是要尾部调用优化自递归函数,您只需要在同一方法内执行 GOTO即可。因此,此限制并不排除自我递归方法的TCO。
Alex D
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.