akka の例題を parallel collection で実装
akka の例題では、arctan(1) をテーラー級数展開して円周率を求めるという例題を取り上げている。具体的には、下記の数式を10000項ずつ Actor に割り振って、並列計算している。
このような級数を scala で並列計算する方法としては、akka を使う以外にも、scala 2.9.0 から加わった parallel collection を使うという方法もある。
object PiParallel extends App { import scala.collection.GenSeq // π = 4*atan(1) のテーラー展開 def calculatePi(rng: GenSeq[Int]): Double = rng.map{i => 4.0 * (1 - (i % 2) * 2) / (2 * i + 1)}.sum; val seqRng = Range(0, 10000 * 10000).view // 逐次計算する Range val parRng = Range(0, 10000 * 10000).par.view // 並列計算する ParRange // CPU コア数の表示 println("collection.parallel.availableProcessors == "+collection.parallel.availableProcessors) timeKeep(calculatePi(seqRng), "Sequential:") // 逐次直列計算 timeKeep(calculatePi(parRng), "Parallel:") // 平行同時計算 def timeKeep[A <: AnyRef](calcPi: => Double, msg: String) { println(msg) val t1 = System.nanoTime val pi = calcPi val t2 = System.nanoTime println( "\tPi estimate: \t\t%s\n\tCalculation time: \t%s millis" .format(pi, (t2 - t1) / 1000000)) } }
実行結果 @ Athlon II 250 (CPUコア数 == 2)
collection.parallel.availableProcessors == 2 Sequential: Pi estimate: 3.141592643589326 Calculation time: 8329 millis Parallel: Pi estimate: 3.1415926435898958 Calculation time: 4619 millis
πの計算結果が違うのは、計算順序の違いによる丸め誤差の違いによるもの。
ちなみに、この実行時間は「かなり遅い」ので要注意。その原因としては、scala collection 自体が遅いため。
下記のように手動でインライン展開してみると、実行時間は 2.4秒となり、直列 Range 版に比べて実行時間が7割も削減できる。
def calculatePiInline: Double = { var sum = 0.0 var i = 0; while (i < 10000 * 10000) { sum += 4.0 * (1 - (i % 2) * 2) / (2 * i + 1) i += 1 } sum }
高速化のために並列計算するのに、もともとの collection 自体のパフォーマンスが悪いというのは、なんとも皮肉な話である。
※ソース一式をgistに上げました。