`
fineqtbull
  • 浏览: 50783 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类

类成员方法的尾递归优化限制

阅读更多
PrgInScala的8.9中提到了,对于尾递归(方法的递归调用在方法体的最后)方法Scala编译器会把字节码优化成循环,从而提高性能。但是在尝试书中的例子时却发现没有发生想象中的优化。下面是测试代码。
package fineqtbull;
class Approx { 
    def isGoodEnough(guess:Double):Boolean = 
        if (guess < 1) true 
        else false 
    def improve(guess:Double): Double = guess - 1 
    def approximate(guess:Double):Double = 
        if (isGoodEnough(guess)) guess 
        else approximate(improve(guess)) 
    def approximateLoop(initialGuess:Double):Double = { 
        var guess = initialGuess 
        while (!isGoodEnough(guess)) 
            guess = improve(guess) 
        guess 
    } 
} 

object ApproxMain { 
    val app = new Approx() 
    def main(args:Array[String]) = { 
    	println(System.getProperty("java.class.path"))
        recursive(1) 
        iterate(1) 
        recursive(10) 
        iterate(10) 
        recursive(100) 
        iterate(100) 
    } 
    def recursive(n:Int) = { 
        val start = java.lang.System.currentTimeMillis() 
        for (i <- 0 to 10000000) { 
            app.approximate(n) 
        } 
        println(java.lang.System.currentTimeMillis() - start) 
    } 
    def iterate(n:Int) = { 
        val start = java.lang.System.currentTimeMillis() 
        for (i <- 0 to 10000000) { 
            app.approximateLoop(n) 
        } 
        println(java.lang.System.currentTimeMillis() - start) 
    } 
} 

下面是执行结果,可以看出递归调用的方式比循环方式慢了很多,而且有次数越多慢的幅度越大的倾向。
922
969
2406
2032
13578
8047

接下来用javap来看看Approx类approximate方法的字节码,确认一下是否优化了尾递归。
public double approximate(double);
  Code:
   Stack=4, Locals=3, Args_size=2
   0:   aload_0
   1:   dload_1
   2:   invokevirtual   #18; //Method isGoodEnough:(D)Z
   5:   ifeq    12
   8:   dload_1
   9:   goto    21
   12:  aload_0
   13:  aload_0
   14:  dload_1
   15:  invokevirtual   #21; //Method improve:(D)D
   18:  invokevirtual   #30; //Method approximate:(D)D 
   21:  dreturn
  LineNumberTable:
   line 8: 0
   line 9: 12
   line 8: 21

字节码中的18:行处,以invokevirtual指令调用approximate方法,可以看出这是递归调用,编译器并没有进行尾递归优化。
没什么没有进行尾递归优化呢?代码和书上写的是完全一样的呀。google了一下,知道原因了,原来approximate方法没有加final修饰符。由于该approximate方法可能被子类override,所以编译器不能对它进行尾递归优化。接着在approximate方法前加上final,变为如下代码。
class Approx {
...
    final def approximate(guess:Double):Double = 
        if (isGoodEnough(guess)) guess 
        else approximate(improve(guess)) 
...
}
...

执行结果如下,这次尾递归和循环两种方式所花时间就基本差不多了。
937
1000
1969
2109
8219
8328

再看一下字节码。
public final double approximate(double);
  Code:
   Stack=3, Locals=4, Args_size=2
   0:   aload_0
   1:   dload_1
   2:   invokevirtual   #18; //Method isGoodEnough:(D)Z
   5:   ifeq    10
   8:   dload_1
   9:   dreturn
   10:  aload_0
   11:  dload_1
   12:  invokevirtual   #21; //Method improve:(D)D
   15:  dstore_1
   16:  goto    0
  LineNumberTable:
   line 8: 0
   line 7: 9
   line 9: 10

原来的
   18:  invokevirtual   #30; //Method approximate:(D)D
   21:  dreturn
现在变成了
   15:  dstore_1
   16:  goto    0
也就是,在字节码层次上用循环替代了原来的递归,速度自然就与代码层次的循环实现不相上下了。
原书中也提到了Scala在尾递归的优化上有诸多限制,比如递归必须是直接递归而不能是间接的,也不能是通过函数对象调用实现的递归。综上所述,本文提到的类成员方法必须是final的也是限制条件之一了。附带提一下如果是object中定义的方法就不需要加final修饰符了,那是因为object中所有的方法默认都是final的。
分享到:
评论
3 楼 RednaxelaFX 2009-10-27  
嗯不错,这个细节注意得好
2 楼 fineqtbull 2009-10-27  
呵呵,看来没白写。
为此我还专门学了一点JVM呢。
1 楼 night_stalker 2009-10-27  
(⊙o⊙)哦,原来如此!
前几天有个栈溢出代码重写了有点纠结,原来是没加 final 的原因 ……

相关推荐

    JS尾递归的实现方法及代码优化技巧

    本文实例讲述了JS尾递归的实现方法及代码优化技巧。分享给大家供大家参考,具体如下: 在学习数据结构和算法的时候,我们都知道...这里实现了一个通用的方法,将尾递归优化成栈+循环。 代码摘自阮一峰的《ECMAScript入

    Python递归及尾递归优化操作实例分析

    本文实例讲述了Python递归及尾递归优化操作。分享给大家供大家参考,具体如下: 1、递归介绍 递归简而言之就是自己调用自己。使用递归解决问题的核心就是分析出递归的模型,看这个问题能拆分出和自己类似的问题并且...

    Python尾递归优化实现代码及原理详解

    在尾递归中,先执行某部分的计算,然后开始调用递归,所以你可以得到当前的计算结果,而这个结果也将作为参数传入下一次递归。这也就是说函数调用出现在调用者函数的尾部,因为是尾部,所以其有一个优越于传统递归之...

    Python中使用装饰器来优化尾递归的示例

    尾递归简介 尾递归是函数返回最后一个操作是递归...但是python不支出编译器优化尾递归所以当递归多次的话还是会报错(学习用)。 eg: def factorial(n, x): if n == 0: return x else: return factorial(n-1, n*x

    尾递归.cpp

    /*int f(int n,int a1,int a2) { if(n) return a1; else return f(n - 1, a2, a1 + a2); }*/

    Fibonacci数列的四种解法:递归、存储优化的递归、自下而上的递归(迭代法)、尾递归

    fibonacci数列的各种解法,递归、存储优化的递归、自下而上的递归(迭代法)、尾递归。其中分析内容请移步我的博客、

    关于尾递归的使用详解

    这几天看到几篇关于尾递归的文章,之前对尾递归没有多大概念,所以回头研究了一下尾递归。  尾递归的概念尾递归(Tail Recursion)的概念是递归概念的一个子集。对于普通的递归,由于必须要记住递归的调用堆栈,...

    【RP-CNN-LSTM-Attention分类】基于递归图优化卷积长短期记忆神经网络注意力机制的数据分类预测

    【RP-CNN-LSTM-Attention分类】基于递归图优化卷积长短期记忆神经网络注意力机制的数据分类预测 【RP-CNN-LSTM-Attention分类】基于递归图优化卷积长短期记忆神经网络注意力机制的数据分类预测 【RP-CNN-LSTM-...

    .net递归算法优化源码案例

    .net递归算法优化源码案例

    es6函数之尾递归用法实例分析

    本文实例讲述了es6函数之尾递归用法。分享给大家供大家参考,具体如下: 函数调用自身,称为递归,如果尾调用自身,就称为尾递归。 递归非常耗费内存。因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误...

    详解python使用递归、尾递归、循环三种方式实现斐波那契数列

    本篇文章主要介绍了python使用递归、尾递归、循环三种方式实现斐波那契数列,非常具有实用价值,需要的朋友可以参考下

    尾递归详细总结分析

    尾递归与Continuation递归与尾递归关于递归操作,相信大家都已经不陌生。简单地说,一个函数直接或间接地调用自身,是为直接或间接递归。例如,我们可以使用递归来计算一个单向链表的长度: 代码如下:public class ...

    存款利润 组合算法-2递归优化

    递归优化算法: 某人手中有2000元钱,通过计算选择一种存钱方案,使得钱存入银行20年后得到的利息最多 (假定银行对超过存款期限的那一部分时间不付利息)。为了得到最多的利息,存入银行的钱 应在到期时马上取出来,...

    C#中的尾递归与Continuation详解

    主要介绍了C#中的尾递归与Continuation详解,本文讲解了递归与尾递归、尾递归与Continuation、Continuation的改进等内容,需要的朋友可以参考下

    C#函数式编程中的递归调用之尾递归详解

    普通递归和尾递归如果仅仅只是从代码的角度出发来看,我们可能发现不了他的特点,所以笔者利用两张堆栈上的图来展示具体的差距在哪,首先我们来看看普通的递归调用的情况,如下图1.1所示: 假设这里执行的函数是...

    关于尾递归和Cooper变换

    这是一个关于尾递归和Cooper变换的文档,有兴趣的人可以下载。

    C.rar_instead5ss_尾递归_整数转为二进制数

    在C语言的环境下,运用尾递归将整数转换为二进制数 在C语言的环境下,运用尾递归将整数转换为二进制数

    fibonacci 递归求法

    fibonacci 递归求法

Global site tag (gtag.js) - Google Analytics