2010-06-23 66 views
17

我已经尝试过不同的斯卡拉集合来总结它的元素,它们比Java慢得多总结它的数组(与for周期)。有没有办法让Scala像Java数组一样快?什么是在斯卡拉总结一个集合的最快方法

我听说在斯卡拉2.8阵列将同在Java中,但他们在实践中

+9

向我们展示您的基准测试代码。 – Jesper 2010-06-23 16:42:43

回答

26

在while循环中对数组进行索引在Scala中与在Java中一样快。 (Scala的“for”循环是不是低级别的结构Java的是,这样就不会工作,你所希望的方式。)

因此,如果在Java中你看到

for (int i=0 ; i < array.length ; i++) sum += array(i) 

在斯卡拉你应该写

var i=0 
while (i < array.length) { 
    sum += array(i) 
    i += 1 
} 

如果你适当地做你的基准,你会发现没有速度差异。

如果您有迭代器,那么在大多数情况下,Scala的速度与Java一样快。举例来说,如果你有双打,并在Java中一个ArrayList您添加它们使用

for (double d : arraylist) { sum += d } 

然后在斯卡拉你会大致一样快 - 如果使用等效的数据结构是怎样的ArrayBuffer - 与

arraybuffer.foreach(sum += _) 

并不算太离谱用其中一种

sum = (0 /: arraybuffer)(_ + _) 
sum = arraybuffer.sum // 2.8 only 

请记住,虽然,有一个点球,以混合的高层次和低层次的结构。例如,如果你决定从一个数组开始,但是在其上使用“foreach”而不是索引到它,Scala必须将它包装在一个集合中(ArrayOps在2.8)以使其起作用,并且通常必须将基元也是如此。

无论如何,对于基准测试,这两个功能是你的朋友:

def time[F](f: => F) = { 
    val t0 = System.nanoTime 
    val ans = f 
    printf("Elapsed: %.3f\n",1e-9*(System.nanoTime-t0)) 
    ans 
} 

def lots[F](n: Int, f: => F): F = if (n <= 1) f else { f; lots(n-1,f) } 

例如:

val a = Array.tabulate(1000000)(_.toDouble) 
val ab = new collection.mutable.ArrayBuffer[Double] ++ a 
def adSum(ad: Array[Double]) = { 
    var sum = 0.0 
    var i = 0 
    while (i<ad.length) { sum += ad(i); i += 1 } 
    sum 
} 

// Mixed array + high-level; convenient, not so fast 
scala> lots(3, time(lots(100,(0.0 /: a)(_ + _)))) 
Elapsed: 2.434 
Elapsed: 2.085 
Elapsed: 2.081 
res4: Double = 4.999995E11 

// High-level container and operations, somewhat better 
scala> lots(3, time(lots(100,(0.0 /: ab)(_ + _))))  
Elapsed: 1.694 
Elapsed: 1.679 
Elapsed: 1.635 
res5: Double = 4.999995E11 

// High-level collection with simpler operation 
scala> lots(3, time(lots(100,{var s=0.0;ab.foreach(s += _);s}))) 
Elapsed: 1.171 
Elapsed: 1.166 
Elapsed: 1.162 
res7: Double = 4.999995E11 

// All low level operations with primitives, no boxing, fast! 
scala> lots(3, time(lots(100,adSum(a))))    
Elapsed: 0.185 
Elapsed: 0.183 
Elapsed: 0.186 
res6: Double = 4.999995E11 
+2

'a.sum'需要多长时间? – 2010-06-23 17:08:46

+0

非常好的答案,我一直在用for循环尝试吓唬自己... – 2010-06-23 17:21:06

+0

@Daniel - 'a.sum'需要的时间和'(0 /:a)(_ + _)'一样长,至少是2.8.0.RC6。 – 2010-06-23 18:08:13

4

斯卡拉2.8 Array慢得多 JVM/Java数组,因此具有相同的性能特点。但这意味着他们不能直接拥有额外的方法来将它们与其余的Scala集合统一起来。为了提供数组具有这些方法的错觉,隐式转换为添加这些功能的包装类。如果你不小心,会使用这些功能导致无节制的开销。

在这种情况下,迭代的开销是至关重要的,你可以明确地得到一个迭代器(或维持一个整数索引,对于喜欢Array或其他IndexedSeq索引顺序结构),并使用while循环,这是一种语言级的构造,不需要对函数(文字或其他)进行操作,但可以编译内联代码块。

val l1 = List(...) // or any Iteralbe 
val i1 = l1.iterator 
while (i1.hasNext) { 
    val e = i1.next 
    // Do stuff with e 
} 

这样的代码的执行速度基本上与Java相同。

+0

嗨,兰德尔。感谢您的回答。我做了一个测试,在Java和Scala中使用你的答案添加10万个双打,结果是23.23ms和141ms。那么,还有什么可以帮助吗? – Tala 2010-06-23 15:57:19

+1

@塔拉:通常的基准警告适用。您是否知道微型基准测试JVM代码的问题? – 2010-06-23 16:09:17

+0

Scala的2.8,IterableLike: “DEF FOREACH [U](F:A => U):单位= iterator.foreach(F)” 迭代: “DEF FOREACH [U](F:A =>û ){while(hasNext)f(next())}“ 假设f不需要装箱(因为”@specialized“),l1.foreach应该和Randall的while循环具有几乎相同的性能,不是吗? – 2010-06-24 08:08:38

5

这是很难解释为什么你还没有显示出一些代码执行得很差比其他一些代码还没有显示在一些基准测试中。

您可能有兴趣在this question及其接受的答案,一件事。但是,对JVM代码进行基准测试很困难,因为JIT将以难以预测的方式优化代码(这就是JIT在编译时击败传统优化的原因)。

+0

嗨,丹尼尔。感谢您的链接。这非常有帮助。 – Tala 2010-06-24 08:59:04

3

适当阶或功能是要做到这一点是:

val numbers = Array(1, 2, 3, 4, 5) 
val sum = numbers.reduceLeft[Int](_+_) 

退房此链接语法的完整说明: http://www.codecommit.com/blog/scala/quick-explanation-of-scalas-syntax

我怀疑这将是比做得更快在其他答案中描述的方式,但我没有测试它,所以我不知道。在我看来,尽管Scala是一种功能性语言,但它仍然是正确的方式。

9

您现在可以简单地使用sum。

val values = Array.fill[Double](numValues)(0) 

val sumOfValues = values.sum 
1

时间不是唯一的问题。 随着sum你可能会发现一个溢出问题:在这种情况下,与Long播种foldLeft

scala> Array(2147483647,2147483647).sum 
res0: Int = -2 

最好

scala> Array(2147483647,2147483647).foldLeft(0L)(_+_) 
res1: Long = 4294967294