“ JVM不支持尾调用优化,因此我预计会有很多爆炸式堆栈”
谁这样说的要么(1)不了解尾部调用优化,要么(2)不了解JVM,或者(3)两者均如此。
我将从Wikipedia的尾声定义开始(如果您不喜欢Wikipedia,这是替代方法):
在计算机科学中,尾部调用是子例程调用,该子例程调用发生在另一个过程中,作为其最终动作。它可能产生一个返回值,然后由调用过程立即返回
在下面的代码中,对的调用bar()
是的尾部调用foo()
:
private void foo() {
// do something
bar()
}
当语言实现看到尾部调用,不使用常规方法调用(创建堆栈框架),而是创建分支时,便会发生尾部调用优化。这是一种优化,因为堆栈帧需要内存,并且它需要CPU周期才能将信息(例如返回地址)推送到该帧上,并且假定调用/返回对比无条件跳转需要更多的CPU周期。
TCO通常用于递归,但这并不是唯一的用途。它也不适用于所有递归。例如,用于计算阶乘的简单递归代码无法进行尾调用优化,因为函数中最后发生的事情是乘法运算。
public static int fact(int n) {
if (n <= 1) return 1;
else return n * fact(n - 1);
}
为了实现尾部调用优化,您需要做两件事:
- 除了subtroutine调用之外,还支持分支的平台。
- 可以确定是否可以进行尾部调用优化的静态分析器。
而已。正如我在其他地方提到的那样,JVM(像其他任何图灵完整的体系结构一样)都有一个转到。它碰巧有一个无条件的goto,但是可以使用条件分支轻松实现该功能。
静态分析很棘手。在一个功能内,这没问题。例如,这是一个尾递归Scala函数,用于将a中的值求和List
:
def sum(acc:Int, list:List[Int]) : Int = {
if (list.isEmpty) acc
else sum(acc + list.head, list.tail)
}
该函数转换为以下字节码:
public int sum(int, scala.collection.immutable.List);
Code:
0: aload_2
1: invokevirtual #63; //Method scala/collection/immutable/List.isEmpty:()Z
4: ifeq 9
7: iload_1
8: ireturn
9: iload_1
10: aload_2
11: invokevirtual #67; //Method scala/collection/immutable/List.head:()Ljava/lang/Object;
14: invokestatic #73; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
17: iadd
18: aload_2
19: invokevirtual #76; //Method scala/collection/immutable/List.tail:()Ljava/lang/Object;
22: checkcast #59; //class scala/collection/immutable/List
25: astore_2
26: istore_1
27: goto 0
请注意goto 0
最后。相比之下,一个等效的Java函数(必须使用Iterator
来模仿将Scala列表分为头尾的行为)变成以下字节码。注意,最后两个操作现在是invoke,然后显式返回该递归调用产生的值。
public static int sum(int, java.util.Iterator);
Code:
0: aload_1
1: invokeinterface #64, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
6: ifne 11
9: iload_0
10: ireturn
11: iload_0
12: aload_1
13: invokeinterface #70, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
18: checkcast #25; //class java/lang/Integer
21: invokevirtual #74; //Method java/lang/Integer.intValue:()I
24: iadd
25: aload_1
26: invokestatic #43; //Method sum:(ILjava/util/Iterator;)I
29: ireturn
单一功能的尾调用优化的很简单:编译器可以看到,有一个使用调用的结果没有代码,因此它可以代替调用了goto
。
如果您有多种方法,生活就会变得棘手。与诸如80x86之类的通用处理器的JVM分支指令不同,JVM的分支指令仅限于单个方法。如果拥有私有方法,它仍然相对简单:编译器可以自由地内联这些方法,因此可以优化尾部调用(如果您想知道这可能如何工作,请考虑使用一种switch
用于控制行为的通用方法)。您甚至可以将此技术扩展到同一类中的多个公共方法:编译器内联方法主体,提供公共桥方法,并且内部调用变成跳转。
但是,当您考虑不同类中的公共方法时,此模型会崩溃,尤其是考虑到接口和类加载器时。源代码级编译器根本没有足够的知识来实现尾调用优化。但是,与 “裸机”实现不同,* JVM(确实以Hotspot编译器的形式(至少是前Sun编译器的形式)具有执行此操作的信息。我不知道它是否实际执行尾部呼叫优化,并且怀疑不是,但是可以。
这将我带到您的问题的第二部分,我将其重新表述为“我们应该照顾吗?”
显然,如果您的语言使用递归作为唯一的迭代原语,则您会在意。但是,需要此功能的语言可以实现它。唯一的问题是用于该语言的编译器是否可以生成可以被任意Java类调用的类。
在那种情况之外,我将通过说无关紧要的方式来邀请不赞成投票的人。我所见过的大多数递归代码(并且我已经处理过许多图形项目)都不是可优化的尾部调用。像简单的阶乘一样,它使用递归来构建状态,并且尾部操作是组合。
对于可以优化尾部调用的代码,通常可以直接将其转换为可迭代的形式。例如,sum()
我之前显示的功能可以概括为foldLeft()
。如果您查看源代码,将会看到它实际上是作为迭代操作实现的。JörgW Mittag举了一个通过函数调用实现状态机的例子。有许多高效(且可维护)的状态机实现,它们不依赖于将函数调用转换为跳转。
我将完成一些完全不同的事情。如果您从SICP的脚注中搜索Google ,您可能会在这里结束。我个人发现,这比将我的编译器替换JSR
为有趣得多JUMP
。