`
fineqtbull
  • 浏览: 51514 次
  • 性别: 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 的原因 ……

相关推荐

    基于.net平台递归树

    在EmployeeSystem中,可能需要考虑缓存中间结果,避免重复计算,或者使用尾递归优化来减少栈空间的使用。 4. **递归深度与栈溢出**:递归深度过大可能导致栈溢出,尤其是在处理大型组织结构时。为防止这种情况,...

    Swifter-100个tips-2018年6月

    85. enum 类型尾递归:在 Swift 中对枚举类型使用尾递归优化以节省资源。 上述知识点覆盖了 Swift 语言的核心特性、高级用法、内存管理、并发处理、性能优化、与其他语言的交互、错误处理以及测试等方面。由于篇幅...

    ocaml book

    - **尾递归优化**: 尾递归是一种特殊的递归形式,它可以被编译器优化,从而避免栈溢出等问题。 - **列表与尾递归**: 如何利用尾递归来高效地处理列表。 #### 六、联合体 **二叉树** - **不平衡二叉树**: 解释了...

    java面试必会200题.docx

    - Java的反射机制允许程序在运行时获取类的信息(如类名、构造方法、成员变量、成员方法等)以及动态地创建和操作对象。这为实现高度灵活的代码提供了可能,例如框架和库的实现中经常使用反射。 5. **什么是ACID**...

    百度2019年最新面试题库

    - **原因**:尾递归可以优化为循环,避免了深度递归造成的栈溢出风险,提高了程序的效率。 #### 什么是控制反转(InversionofControl)与依赖注入(DependencyInjection) - **控制反转(IoC)**: 一种设计模式,...

    kotlin-03-fundamentos-oo:东方基金会基金会

    - **尾递归优化**: Kotlin 对尾递归函数进行优化,避免栈溢出。 - **空安全**: Kotlin 强制类型系统处理 null 值,避免空指针异常。 - **类型别名**: 使用 `typealias` 关键字为现有类型创建别名,提高代码可读性。 ...

    金山2014校园招聘C++试卷A

    ### 金山2014校园招聘C++试卷A解析 #### 浮点数据单向链表类设计 在设计一个支持指定位置插入、删除节点以及升序排列的...此外,现代编译器提供了一些优化技术,如尾递归优化,可以在一定程度上减轻栈空间的压力。

    循环链表实验报告(word文档,详细解说)

    9. **循环表 `circularList` 的实现**:`circularList` 类包含一个尾指针 `ptrToLastLink`,并提供了构造函数、拷贝构造函数、析构函数以及各种链表操作方法。`firstElement` 返回第一个元素,`includes` 判断元素...

    大厂面试系列二.pdf

    为了提升性能,快速排序的优化通常包括使用尾递归优化、选择合适的枢轴元素、使用三数取中法等。 在进行IO操作时,IO多路复用可以有效地处理大量并发的IO请求,通过一个进程监视多个文件描述符,当某个描述符就绪时...

    c++/c长整数四则运算

    这个类通常包含一个头指针和尾指针,用于快速访问链表的首尾元素: ```cpp class BigInt { public: ListNode* head; ListNode* tail; // 构造函数、析构函数和其他成员函数... }; ``` 为了实现加法,我们可以从...

    安卓面试题

    - **优化**:尾递归优化、三数取中法。 #### 48. synchronized关键字 - **作用**:实现线程同步。 - **用法**:修饰方法或代码块。 #### 49. 线程状态 - **新建状态**:Thread对象创建后。 - **就绪状态**:线程...

    Javascript中55个经典技巧

    34. **尾调用优化(Tail Call Optimization)**:在某些情况下,JavaScript引擎可以优化尾递归,避免栈溢出。 35. **模块导出默认值**:`export default`允许导出单个默认值,方便导入。 36. **模块命名导出与导入...

    大数阶乘C++代码

    接下来,我们需要定义一个链表节点类,包含两个成员:一个存储数字位的变量和一个指向下一个节点的指针。例如: ```cpp struct Node { int digit; // 存储数字位 Node* next; // 指向下一个节点的指针 }; ``` ...

    prolog notes

    ##### 知识点2:递归的优化 1. **子句排序**:子句的顺序会影响搜索效率和程序的行为。 - 应该把最常用的规则放在前面,以提高效率。 2. **目标排序**:在规则中,目标(goals)的顺序也会影响搜索效率。 - 尽量将...

    Web开发必用javascript技巧

    26. **尾调用优化(Tail Call Optimization)**:编写递归函数时考虑使用,避免栈溢出。 27. **WeakSet和WeakMap**:了解其特性,用于弱引用存储,避免内存泄漏。 28. **Proxy和Reflect**:学习这两个新特性,用于...

    c语言经典算法

    本例关注的是算法的优化,尤其是针对递归问题的解决方案。通过分析儿子做题的过程,可以学习到如何在C语言中使用递归函数,并理解递归算法的效率与局限性。 #### 1.4 乐队人数 该问题探讨了如何计算满足特定条件下...

Global site tag (gtag.js) - Google Analytics