Scala:嵌套for循环和for-comprehensions之间的性能差异

时间:2016-02-20 19:36:25

标签: performance scala for-loop foreach scala-collections

我的印象是在Scala(2.11.7)中,下面的代码片段会有类似的性能特征,但看起来我错了:

a: IndexedSeq[(Int, Int)]

选项1:

var ans = 0
for {
  i <- a.indices
  (x1, y1) = a(i)
  j <- a.indices drop (i+1)
  (x2, y2) = a(j)
  if x1 == x2 || y1 == y2
} ans += 1

选项2:

var ans = 0
for (i <- a.indices) {
  val (x1, y1) = a(i)
  for (j <- a.indices drop (i+1)) {
    val (x2, y2) = a(j)
    if (x1 == x2 || y1 == y2) ans += 1
  }
}

但是,即使是小尺寸(a.length == 100),第二种方式似乎比第一种方式快5-10倍。此处a也是IndexedSeq,因此随机访问几乎不应该那么重要(请参阅下面的f1 vs f2)。以下是完整的基准测试程序:

import scala.util.Random

object PerfTester extends App {

  def f1(a: IndexedSeq[(Int, Int)]) = {
    var ans = 0
    for {
      i <- a.indices
      j <- a.indices drop (i+1)
      ((x1, y1), (x2, y2)) = (a(i), a(j))
      if x1 == x2 || y1 == y2
    } ans += 1
    ans
  }

  def f2(a: IndexedSeq[(Int, Int)]) = {
    var ans = 0
    for {
      i <- a.indices
      (x1, y1) = a(i)
      j <- a.indices drop (i+1)
      (x2, y2) = a(j)
      if x1 == x2 || y1 == y2
    } ans += 1
    ans
  }

  def f3(a: IndexedSeq[(Int, Int)]) = {
    var ans = 0
    for (i <- a.indices) {
      val (x1, y1) = a(i)
      for (j <- a.indices drop (i+1)) {
        val (x2, y2) = a(j)
        if (x1 == x2 || y1 == y2) ans += 1
      }
    }
    ans
  }

  def profile[R](code: => R, t: Long = System.nanoTime()) = (code, (System.nanoTime() - t)/1e6)

  val n = 1000
  val data = IndexedSeq.fill(n) {
    Random.nextInt(100) -> Random.nextInt(100)
  }
  val (r1, t1) = profile(f1(data))
  val (r2, t2) = profile(f2(data))
  val (r3, t3) = profile(f3(data))
  require(r1 == r2 && r2 == r3)
  println(s"f1: $t1 ms")
  println(s"f2: $t2 ms")
  println(s"f3: $t3 ms")
}

我知道这些测试很容易受到JVM热身,热点优化和排序等的影响,所以我通过随机调用f1f2f3的调用和平均来验证这一点许多不同运行的运行时间。

我在codeforces.com上进行编程竞赛时碰到了这个:

  1. 超出了时间限制&#34;:http://codeforces.com/contest/629/submission/16248545
  2. 这已被接受&#34;:http://codeforces.com/contest/629/submission/16248804

1 个答案:

答案 0 :(得分:2)

尽管Scala在概念上是等效的,但代码并不是Scala如何编码前一个for循环。特别是,当您编写y = x时,Scala会执行单独的地图操作,并将答案捆绑为元组。

如果您在命令行询问:

scala -Xprint:typer -e 'for {i <- 1 to 10; j = i*i } println(j)'

你得到(部分):

def main(args: Array[String]): Unit = {
  final class $anon extends scala.AnyRef {
    def <init>(): <$anon: AnyRef> = {
      $anon.super.<init>();
      ()
    };
    scala.this.Predef.intWrapper(1).to(10).map[(Int, Int), scala.collection.immutable.IndexedSeq[(Int, Int)]](((i: Int) => {
      val j: Int = i.*(i);
      scala.Tuple2.apply[Int, Int](i, j)
    }))(immutable.this.IndexedSeq.canBuildFrom[(Int, Int)]).foreach[Unit](((x$1: (Int, Int)) => (x$1: (Int, Int) @unchecked) match {
        case (_1: Int, _2: Int)(Int, Int)((i @ _), (j @ _)) => scala.this.Predef.println(j)
    }))
  };
  {
    new $anon();
    ()
  }
}

你可以看到第7行中的额外地图和第9行创建的元组。然后它只需要在第11行再次提取它。相比于将其插入到foreach函数的闭包中,这是非常昂贵,特别是对于我在本例中使用的整数,因为它们必须装箱。

有人可能会争辩说,现有的方法可能会有所改进,但这就是目前的实施方式。