最近我接受了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
。
我检查了语言规范,但仍然无法理解答案。有人可以向我解释一下吗?
答案 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)