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に上げました。