用于理解和功能创建的数量

时间:2014-05-19 11:58:07

标签: scala for-comprehension

最近我接受了Scala Developer职位的采访。我被问到这样的问题

// matrix 100x100 (content unimportant)

val matrix = Seq.tabulate(100, 100) { case (x, y) => x + y }

// A

for {

   row <- matrix

   elem <- row

} print(elem)

// B

val func = print _
for {

   row <- matrix

   elem <- row

} func(elem)

问题是:哪种实施方式A或B更有效?

我们都知道 for comprehensions 可以翻译成

// A

matrix.foreach(row => row.foreach(elem => print(elem)))

// B

matrix.foreach(row => row.foreach(func))

B可以写成matrix.foreach(row => row.foreach(print _))

据说正确答案是B,因为A会创建100倍以上的函数print

我检查了语言规范,但仍然无法理解答案。有人可以向我解释一下吗?

3 个答案:

答案 0 :(得分:8)

简而言之:

示例A在理论上更快,但实际上你不应该测量任何差异。

答案很长:

正如你已经发现的那样

for {xs <- xxs; x <- xs} f(x)

被翻译为

xxs.foreach(xs => xs.foreach(x => f(x)))

这在§6.19SLS中解释:

  

for循环

for ( p <- e; p' <- e' ... ) e''
     

其中......是(可能为空的)生成器,定义或保护序列,被转换为

e .foreach { case p => for ( p' <- e' ... ) e'' }

现在当一个人写一个函数文字时,每次需要调用该函数时都会获得一个新实例(§6.23SLS)。这意味着

xs.foreach(x => f(x))

相当于

xs.foreach(new scala.Function1 { def apply(x: T) = f(x)})

引入本地函数类型时

val g = f _; xxs.foreach(xs => xs.foreach(x => g(x)))

您没有引入优化,因为您仍然将函数文字传递给foreach。实际上代码较慢,因为内部foreach被转换为

xs.foreach(new scala.Function1 { def apply(x: T) = g.apply(x) })

发生对apply g方法的额外调用。但是,您可以在编写时进行优化

val g = f _; xxs.foreach(xs => xs.foreach(g))

因为内部foreach现在被翻译为

xs.foreach(g())

表示函数g本身传递给foreach

这意味着B在理论上更快,因为每次执行for comprehension的主体时都不需要创建匿名函数。但是,上面提到的优化(函数直接传递给foreach)不适用于理解,因为规范说翻译包括函数文字的创建,因此总是创建不必要的函数对象(在这里我必须说编译器也可以对其进行优化,但事实并非如此,因为对于理解的优化是困难的,并且在2.11中仍然没有发生。总而言之,这意味着A更有效,但如果没有理解的话,B会更有效率(并且没有为最内层函数创建函数文字)。

然而,所有这些规则只能在理论上应用,因为实际上有scalac的后端和JVM本身都可以进行优化 - 更不用说CPU完成的优化了。此外,您的示例包含在每次迭代时执行的系统调用 - 这可能是最昂贵的操作,超过其他所有操作。

答案 1 :(得分:2)

我同意 sschaef 并说A是更有效的选择。

查看生成的类文件,我们得到以下匿名函数及其apply方法:

MethodA:
  anonfun$2            -- row => row.foreach(new anonfun$2$$anonfun$1)
  anonfun$2$$anonfun$1 -- elem => print(elem)

即。 matrix.foreach(row => row.foreach(elem => print(elem)))

MethodB:
  anonfun$3            -- x => print(x)
  anonfun$4            -- row => row.foreach(new anonfun$4$$anonfun$2)
  anonfun$4$$anonfun$2 -- elem => func(elem)

即。 matrix.foreach(row => row.foreach(elem => func(elem))) 其中func只是在呼叫print之前的另一个间接方向。此外,需要查找func,即通过对每行的实例(this.func())进行方法调用。

因此,对于方法B,创建了1个额外对象(func),并且还有# of elem个附加函数调用。

最有效的选择是

matrix.foreach(row => row.foreach(func))

因为它创建的对象数量最少,并且完全符合您的预期。

答案 2 :(得分:2)

基准

摘要

方法A比方法B快近30%。

代码链接:https://gist.github.com/ziggystar/490f693bc39d1396ef8d

实施细则

我添加了方法C(两个while循环)和D(fold,sum)。我还增加了矩阵的大小,改为使用IndexedSeq。我还用较重的东西替换了print(加上所有条目)。

奇怪的是while构造并不是最快的。但是,如果使用Array而不是IndexedSeq,它会变得最快(因子5,不再有拳击)。使用明确的盒装整数,方法A,B,C都同样快。特别是与A,B的隐式盒装版本相比,它们的速度提高了50%。

结果

A
4.907797735
4.369745787
4.375195012000001
4.7421321800000005
4.35150636
B
5.955951859000001
5.925475619
5.939570085000001
5.955592247
5.939672226000001
C
5.991946029
5.960122757000001
5.970733164
6.025532582
6.04999499
D
9.278486201
9.265983922
9.228320372
9.255641645
9.22281905
verify results
999000000
999000000
999000000
999000000

>$ scala -version
Scala code runner version 2.11.0 -- Copyright 2002-2013, LAMP/EPFL

代码摘录

val matrix = IndexedSeq.tabulate(1000, 1000) { case (x, y) => x + y }

def variantA(): Int = {
  var r = 0
  for {
    row <- matrix
    elem <- row
  }{
    r += elem
  }
  r
}

def variantB(): Int = {
  var r = 0
  val f = (x:Int) => r += x
  for {
    row <- matrix
    elem <- row
  } f(elem)
  r
}

def variantC(): Int = {
  var r = 0
  var i1 = 0
  while(i1 < matrix.size){
    var i2 = 0
    val row = matrix(i1)
    while(i2 < row.size){
      r += row(i2)
      i2 += 1
    }
    i1 += 1
  }
  r
}

def variantD(): Int = matrix.foldLeft(0)(_ + _.sum)
相关问题