【发布时间】:2012-02-06 22:58:33
【问题描述】:
下面是 Cay Horstmann 的 Scala 中针对不耐烦的练习 4.9 的两个解决方案:“编写一个函数 lteqgt(values: Array[Int], v: Int),它返回一个包含小于 v 的值的计数的三元组,等于v,并且大于 v。”一个使用尾递归,另一个使用 while 循环。我认为两者都可以编译成相似的字节码,但是 while 循环比尾递归慢了将近 2 倍。这表明我的 while 方法写得不好。
import scala.annotation.tailrec
import scala.util.Random
object PerformanceTest {
def main(args: Array[String]): Unit = {
val bigArray:Array[Int] = fillArray(new Array[Int](100000000))
println(time(lteqgt(bigArray, 25)))
println(time(lteqgt2(bigArray, 25)))
}
def time[T](block : => T):T = {
val start = System.nanoTime : Double
val result = block
val end = System.nanoTime : Double
println("Time = " + (end - start) / 1000000.0 + " millis")
result
}
@tailrec def fillArray(a:Array[Int], pos:Int=0):Array[Int] = {
if (pos == a.length)
a
else {
a(pos) = Random.nextInt(50)
fillArray(a, pos+1)
}
}
@tailrec def lteqgt(values: Array[Int], v:Int, lt:Int=0, eq:Int=0, gt:Int=0, pos:Int=0):(Int, Int, Int) = {
if (pos == values.length)
(lt, eq, gt)
else
lteqgt(values, v, lt + (if (values(pos) < v) 1 else 0), eq + (if (values(pos) == v) 1 else 0), gt + (if (values(pos) > v) 1 else 0), pos+1)
}
def lteqgt2(values:Array[Int], v:Int):(Int, Int, Int) = {
var lt = 0
var eq = 0
var gt = 0
var pos = 0
val limit = values.length
while (pos < limit) {
if (values(pos) > v)
gt += 1
else if (values(pos) < v)
lt += 1
else
eq += 1
pos += 1
}
(lt, eq, gt)
}
}
根据你的堆大小调整 bigArray 的大小。这是一些示例输出:
Time = 245.110899 millis
(50004367,2003090,47992543)
Time = 465.836894 millis
(50004367,2003090,47992543)
为什么while方法比tailrec慢很多?天真地,tailrec 版本似乎处于轻微的劣势,因为它必须始终为每次迭代执行 3 次“if”检查,而 while 版本由于 else 构造通常只会执行 1 或 2 次测试。 (注意颠倒我执行这两种方法的顺序不会影响结果)。
【问题讨论】:
-
我自己也经常想知道这一点。答案肯定在于 JIT。在完全禁用 JIT 的同时重复基准测试会很有趣。
-
在stackoverflow.com/a/48143130/1172685 中查看结果,其中 while 循环比尾递归快得多(scala 2.12.x 带有试图管理 JVM 不一致的 scalameter 基准)。
标签: performance scala loops tail-recursion