为什么我的Scala尾递归比while循环快? [英] Why is my Scala tail-recursion faster than the while loop?

查看:156
本文介绍了为什么我的Scala尾递归比while循环快?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

以下是在Cay Horstmann的Scala中针对不耐烦而练习4.9的两种解决方案:写一个函数lteqgt(values:Array [Int],v:Int),该函数返回一个三元组,其中包含小于v的值计数,等于v,并大于v."一个使用尾部递归,另一个使用while循环.我以为两者都可以编译成类似的字节码,但是while循环比尾部递归慢了将近2倍.这向我表明我的while方法编写得很糟糕.

Here are two solutions to exercise 4.9 in Cay Horstmann's Scala for the Impatient: "Write a function lteqgt(values: Array[Int], v: Int) that returns a triple containing the counts of values less than v, equal to v, and greater than v." One uses tail recursion, the other uses a while loop. I thought that both would compile to similar bytecode but the while loop is slower than the tail recursion by a factor of almost 2. This suggests to me that my while method is badly written.

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的大小.这是一些示例输出:

Adjust the size of bigArray according to your heap size. Here is some sample output:

Time = 245.110899 millis
(50004367,2003090,47992543)
Time = 465.836894 millis
(50004367,2003090,47992543)

为什么while方法比tailrec慢得多?天真地,tailrec版本看起来有一点劣势,因为它必须始终为每个迭代执行3次"if"检查,而while版本由于else构造通常只执行1或2个测试. (注意,颠倒我执行这两种方法的顺序不会影响结果.)

Why is the while method so much slower than the tailrec? Naively the tailrec version looks to be at a slight disadvantage, as it must always perform 3 "if" checks for every iteration, whereas the while version will often only perform 1 or 2 tests due to the else construct. (NB reversing the order I perform the two methods does not affect the outcome).

推荐答案

测试结果(将数组大小减少到20000000后)

Test results (after reducing array size to 20000000)

在Java 1.6.22下,对于尾部递归和while循环,我分别得到151 and 122 ms.

Under Java 1.6.22 I get 151 and 122 ms for tail-recursion and while-loop respectively.

在Java 1.7.0下,我得到55 and 101 ms

Under Java 1.7.0 I get 55 and 101 ms

因此,在Java 6下,while循环实际上更快.两者都在Java 7下提高了性能,但是尾递归版本已经超越了循环.

So under Java 6 your while-loop is actually faster; both have improved in performance under Java 7, but the tail-recursive version has overtaken the loop.

说明

性能差异是由于以下事实:在循环中,有条件地将总数加1,而对于递归,则总是将1或0加起来.因此它们不是等效的.与递归方法等效的while循环为:

The performance difference is due to the fact that in your loop, you conditionally add 1 to the totals, while for recursion you always add either 1 or 0. So they are not equivalent. The equivalent while-loop to your recursive method is:

  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) {
      gt += (if (values(pos) > v) 1 else 0)
      lt += (if (values(pos) < v) 1 else 0)
      eq += (if (values(pos) == v) 1 else 0)
      pos += 1
    }
    (lt, eq, gt)
  }

这与递归方法(无论Java版本如何)的执行时间完全相同.

and this gives exactly the same execution time as the recursive method (regardless of Java version).

讨论

我不是Java 7 VM(HotSpot)可以比您的第一个版本更好地优化它的专家,但是我想这是因为它每次都在代码中采用相同的路径(而不是沿代码分支). if/else if路径),因此可以更高效地内联字节码.

I'm not an expert on why the Java 7 VM (HotSpot) can optimize this better than your first version, but I'd guess it's because it's taking the same path through the code each time (rather than branching along the if / else if paths), so the bytecode can be inlined more efficiently.

但是请记住,Java 6并非如此.为什么一个while循环的性能优于另一个,这是JVM内部的问题.对于Scala程序员来说,幸运的是,从惯用的尾部递归生成的版本是最新版本的JVM中速度更快的版本.

But remember that this is not the case in Java 6. Why one while-loop outperforms the other is a question of JVM internals. Happily for the Scala programmer, the version produced from idiomatic tail-recursion is the faster one in the latest version of the JVM.

差异也可能发生在处理器级别.请参阅此问题,其中说明了如果代码包含不可预测的分支,代码将如何降低速度.

The difference could also be occurring at the processor level. See this question, which explains how code slows down if it contains unpredictable branching.

这篇关于为什么我的Scala尾递归比while循环快?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆