【问题标题】:Why are Scala "for loop comprehensions" so very slow compared to FOR loops?与 FOR 循环相比,为什么 Scala 的“for 循环理解”非常慢?
【发布时间】:2013-05-28 07:08:25
【问题描述】:

据说 Scala 的 For 推导实际上很慢。给我的原因是由于 Java 的限制,对于推导式(例如下面使用的“reduce”)需要在每次迭代时生成一个临时对象,以便调用传入的函数。

这是……这……是真的吗?下面的测试似乎证实了这一点,但我不完全明白为什么会这样。

这可能对“lambdas”或匿名函数有意义,但对非匿名函数没有意义。

在我的测试中,我针对 list.reduce 运行 for 循环(参见下面的代码),发现它们的速度是原来的两倍,即使每次迭代调用的函数与传递给 reduce 的函数完全相同!

我发现这非常违反直觉(曾经认为 Scala 库会经过精心创建以尽可能优化)。

在我进行的一项测试中,我以五种不同的方式运行了相同的循环(将数字 1 加到一百万,而不考虑溢出):

  1. for 循环遍历值数组
  2. for 循环,但调用函数而不是内联算术
  3. for 循环,创建一个包含加法函数的对象
  4. list.reduce,传递一个匿名函数
  5. list.reduce,传入一个对象成员函数

结果如下: 测试:最小/最大/平均(毫秒)

1. 27/157/64.78
2. 27/192/65.77 <--- note the similarity between tests 1,2 and 4,5
3. 139/313/202.58
4. 63/342/150.18
5. 63/341/149.99

可以看出,“for comprehension”版本的顺序是“for with new for each instance”,这意味着实际上可以为匿名和非匿名函数版本执行“new”。

方法:下面的代码(删除了测试调用)被编译成一个 .jar 文件,以确保所有版本都运行相同的库代码。每次迭代中的每个测试都在一个新的 JVM 中调用(即每个测试的 scala -cp ...),以消除堆大小问题。

class t(val i: Int) {
    def summit(j: Int) = j + i
}

object bar {
    val biglist:List[Int]  =  (1 to 1000000).toList

    def summit(i: Int, j:Int) = i+j

    // Simple for loop
    def forloop:  Int = {
        var result: Int = 0
        for(i <- biglist) {
            result += i
        }
        result
    }

    // For loop with a function instead of inline math
    def forloop2:  Int = {
        var result: Int = 0
        for(i <- biglist) {
            result = summit(result,i)
        }
        result
    }

    // for loop with a generated object PER iteration
    def forloop3: Int = {
        var result: Int = 0
        for(i <- biglist) {
            val t = new t(result)
            result = t.summit(i)
        }
        result
    }

    // list.reduce with an anonymous function passed in
    def anonymousfunc: Int = {
        biglist.reduce((i,j) => {i+j})
    }

    // list.reduce with a named function
    def realfunc: Int = {
        biglist.reduce(summit)
    }

    // test calling code excised for brevity. One example given:
    args(0) match {
        case "1" => {
                    val start = System.currentTimeMillis()
                    forloop
                    val end = System.currentTimeMillis()
                    println("for="+(end - start))
                    }
         ...
}

【问题讨论】:

  • .reduce 与“理解”无关
  • 附带说明,有一个 scala 编译器插件旨在消除常见情况下的这种开销:code.google.com/p/scalacl/wiki/ScalaCLPlugin。不过我自己没试过。
  • 事实上,您的前 3 个测试使用的是 for-comprehensions,并且您正在比较时间与 reduce..
  • @RégisJean-Gilles :: ScalaCL 旨在(回到 Scala 2.9)为 OpenCL 生成代码,即:在 GPU 上运行。特别是,您也可以告诉它完成它的工作,但为 CPU(而不是 GPU)生成代码。它工作得很好,但不幸的是,它在很大程度上基于宏,这是 Scala 中的移动目标,从一个版本到另一个版本。 ScalaCL 在最新版本的 Scala 中不可用。
  • 好吧,当我写评论时,scala 2.10 刚刚发布。 scalaCL 被明确宣传为一种加速循环的方法(除了它的 OpenCL 功能)。我引用:“它还经常大幅度优化一般的 Scala 循环(在数组、列表和内联范围上),所以即使你不关心 OpenCL,你也会想要使用它”。附带说明一下,它是/曾经是一个编译器插件,而不是一组宏(尽管我想这是一个有争议的问题,因为它更依赖于实现细节,因此更不可靠)。

标签: performance scala for-loop


【解决方案1】:

您被告知的关于“用于理解”的内容是正确的,但您的问题的问题是您将“用于理解”与“匿名函数”混为一谈。

Scala 中的“为了理解”是一系列.flatMap.map.filter 应用程序的语法糖。由于您正在测试归约算法,并且由于不可能使用这三个函数实现归约算法,因此您的测试用例不正确。

这是一个“理解”的例子:

val listOfLists = List(List(1,2), List(3,4), List(5))
val result = 
  for {
    itemOfListOfLists <- listOfLists
    itemOfItemOfListOfLists <- itemOfListOfLists
  }
  yield (itemOfItemOfListOfLists + 1)
assert( result == List(2,3,4,5,6) )

编译器将 Comprehension 部分脱糖为以下内容:

val result =
  listOfLists.flatMap(
    itemOfListOfLists => itemOfListOfLists.map(
      itemOfItemOfListOfLists => itemOfItemOfListOfLists + 1
    )
  )

然后它对匿名函数语法进行去糖:

val result =
  listOfLists.flatMap(
    new Function1[List[Int], List[Int]] {
      override def apply(itemOfListOfLists: List[Int]): List[Int] =
        itemOfListOfLists.map(
          new Function1[Int, Int] {
            override def apply(itemOfItemOfListOfLists: Int): Int =
              itemOfItemOfListOfLists + 1
          }
        )
    }
  )

从脱糖代码中可以明显看出,每次调用 apply(itemOfListOfLists: List[Int]): List[Int] 方法时都会实例化 Function1[Int, Int] 类。 listOfLists 的每个条目都会发生这种情况。因此,您的理解越复杂,您获得的 Function 对象的实例化就越多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-02-17
    • 2017-11-09
    • 1970-01-01
    • 2019-12-08
    • 2017-01-29
    • 2021-10-01
    • 1970-01-01
    相关资源
    最近更新 更多