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的。
分享到:
相关推荐
在EmployeeSystem中,可能需要考虑缓存中间结果,避免重复计算,或者使用尾递归优化来减少栈空间的使用。 4. **递归深度与栈溢出**:递归深度过大可能导致栈溢出,尤其是在处理大型组织结构时。为防止这种情况,...
85. enum 类型尾递归:在 Swift 中对枚举类型使用尾递归优化以节省资源。 上述知识点覆盖了 Swift 语言的核心特性、高级用法、内存管理、并发处理、性能优化、与其他语言的交互、错误处理以及测试等方面。由于篇幅...
- **尾递归优化**: 尾递归是一种特殊的递归形式,它可以被编译器优化,从而避免栈溢出等问题。 - **列表与尾递归**: 如何利用尾递归来高效地处理列表。 #### 六、联合体 **二叉树** - **不平衡二叉树**: 解释了...
- Java的反射机制允许程序在运行时获取类的信息(如类名、构造方法、成员变量、成员方法等)以及动态地创建和操作对象。这为实现高度灵活的代码提供了可能,例如框架和库的实现中经常使用反射。 5. **什么是ACID**...
- **原因**:尾递归可以优化为循环,避免了深度递归造成的栈溢出风险,提高了程序的效率。 #### 什么是控制反转(InversionofControl)与依赖注入(DependencyInjection) - **控制反转(IoC)**: 一种设计模式,...
- **尾递归优化**: Kotlin 对尾递归函数进行优化,避免栈溢出。 - **空安全**: Kotlin 强制类型系统处理 null 值,避免空指针异常。 - **类型别名**: 使用 `typealias` 关键字为现有类型创建别名,提高代码可读性。 ...
### 金山2014校园招聘C++试卷A解析 #### 浮点数据单向链表类设计 在设计一个支持指定位置插入、删除节点以及升序排列的...此外,现代编译器提供了一些优化技术,如尾递归优化,可以在一定程度上减轻栈空间的压力。
9. **循环表 `circularList` 的实现**:`circularList` 类包含一个尾指针 `ptrToLastLink`,并提供了构造函数、拷贝构造函数、析构函数以及各种链表操作方法。`firstElement` 返回第一个元素,`includes` 判断元素...
为了提升性能,快速排序的优化通常包括使用尾递归优化、选择合适的枢轴元素、使用三数取中法等。 在进行IO操作时,IO多路复用可以有效地处理大量并发的IO请求,通过一个进程监视多个文件描述符,当某个描述符就绪时...
这个类通常包含一个头指针和尾指针,用于快速访问链表的首尾元素: ```cpp class BigInt { public: ListNode* head; ListNode* tail; // 构造函数、析构函数和其他成员函数... }; ``` 为了实现加法,我们可以从...
- **优化**:尾递归优化、三数取中法。 #### 48. synchronized关键字 - **作用**:实现线程同步。 - **用法**:修饰方法或代码块。 #### 49. 线程状态 - **新建状态**:Thread对象创建后。 - **就绪状态**:线程...
34. **尾调用优化(Tail Call Optimization)**:在某些情况下,JavaScript引擎可以优化尾递归,避免栈溢出。 35. **模块导出默认值**:`export default`允许导出单个默认值,方便导入。 36. **模块命名导出与导入...
接下来,我们需要定义一个链表节点类,包含两个成员:一个存储数字位的变量和一个指向下一个节点的指针。例如: ```cpp struct Node { int digit; // 存储数字位 Node* next; // 指向下一个节点的指针 }; ``` ...
##### 知识点2:递归的优化 1. **子句排序**:子句的顺序会影响搜索效率和程序的行为。 - 应该把最常用的规则放在前面,以提高效率。 2. **目标排序**:在规则中,目标(goals)的顺序也会影响搜索效率。 - 尽量将...
26. **尾调用优化(Tail Call Optimization)**:编写递归函数时考虑使用,避免栈溢出。 27. **WeakSet和WeakMap**:了解其特性,用于弱引用存储,避免内存泄漏。 28. **Proxy和Reflect**:学习这两个新特性,用于...
本例关注的是算法的优化,尤其是针对递归问题的解决方案。通过分析儿子做题的过程,可以学习到如何在C语言中使用递归函数,并理解递归算法的效率与局限性。 #### 1.4 乐队人数 该问题探讨了如何计算满足特定条件下...