2011-11-02 19 views
9

編集:サンプルサイズが小さすぎました。 8つのCPUで実際のデータと比較してみると、7.2倍の速度向上が見られました。 )私のコードに4文字を追加するためにあまり目立たない;)スカラ並列コレクションランタイム困惑

私は現在、Scalaを使うことのメリットを管理するために、特にCPUを使ってスケーリングを行うという点で管理を売りに出そうとしています。その目的のために、ベクトル計算を行うシンプルなテストアプリケーションを作成しましたが、ランタイムが私のクアッドコアマシンではそれほど優れていないことがわかりました。興味深いことに、私は、あなたがコレクションを通過する最初の時にランタイムが最悪であり、その後の呼び出しでより良くなることを発見しました。パラレルコレクションに、これを引き起こしているいくつかの怠惰なものがありますか、それとも間違っていますか?私はC++/C#の世界から来ていることに注意する必要があります。だから、どうにか私の設定を混乱させる可能性があります。の

InteliJ Scalaのプラグイン

Scalaの2.9.1.final

のWindows 7 64ビット、クアッドコアプロセッサ(なしハイパースレッディング)

import util.Random 

    // simple Vector3D class that has final x,y,z components a length, and a '-' function 
    class Vector3D(val x:Double, val y:Double, val z:Double) 
    { 
    def length = math.sqrt(x*x+y*y+z*z) 
    def -(rhs : Vector3D) = new Vector3D(x - rhs.x, y - rhs.y, z - rhs.z) 
    } 

object MainClass { 

    def main(args : Array[String]) = 
    { 
    println("Available CPU's: " + Runtime.getRuntime.availableProcessors()) 
    println("Parallelism Degree set to: " + collection.parallel.ForkJoinTasks.defaultForkJoinPool.getParallelism); 
    // my position 
    val myPos = new Vector3D(0,0,0); 

    val r = new Random(0); 

    // define a function nextRand that gets us a random between 0 and 100 
    def nextRand = r.nextDouble() * 100; 

    // make 10 million random targets 
    val targets = (0 until 10000000).map(_ => new Vector3D(nextRand, nextRand, nextRand)).toArray 
    // take the .par hit before we start profiling 
    val parTargets = targets.par 

    println("Created " + targets.length + " vectors") 

    // define a range function 
    val rangeFunc : (Vector3D => Double) = (targetPos) => (targetPos - myPos).length 

    // we'll select ones that are <50 
    val within50 : (Vector3D => Boolean) = (targetPos) => rangeFunc(targetPos) < 50 

    // time it sequentially 
    val startTime_sequential = System.currentTimeMillis() 
    val numTargetsInRange_sequential = targets.filter(within50) 
    val endTime_sequential = System.currentTimeMillis() 
    println("Sequential (ms): " + (endTime_sequential - startTime_sequential)) 

    // do the parallel version 10 times 
    for(i <- 1 to 10) 
    { 

     val startTime_par = System.currentTimeMillis() 
     val numTargetsInRange_parallel = parTargets.filter(within50) 
     val endTime_par = System.currentTimeMillis() 

     val ms = endTime_par - startTime_par; 
     println("Iteration[" + i + "] Executed in " + ms + " ms") 
    } 
    } 
} 

出力:かかわらず、ここに私の設定ですこのプログラムは:

Available CPU's: 4 
Parallelism Degree set to: 4 
Created 10000000 vectors 
Sequential (ms): 216 
Iteration[1] Executed in 227 ms 
Iteration[2] Executed in 253 ms 
Iteration[3] Executed in 76 ms 
Iteration[4] Executed in 78 ms 
Iteration[5] Executed in 77 ms 
Iteration[6] Executed in 80 ms 
Iteration[7] Executed in 78 ms 
Iteration[8] Executed in 78 ms 
Iteration[9] Executed in 79 ms 
Iteration[10] Executed in 82 ms 

ここでは何が起こっているのですか?最初の2回はフィルターをかけますが、それは遅くなり、その後はスピードアップしますか?私は本質的には並列化のスタートアップコストとなることを理解しています。私はアプリケーションで並列性を表現するのが理にかなっています。具体的には、管理プログラムを3-4回実行できるようにしたいクアッドコアボックスでは高速です。これは良い問題ではないのですか?

アイデア?

+2

管理の売り方については、http://scala-boss.heroku.com/#1をご覧ください(次のスライドを見るには矢印キーを使用してください)。 – huynhjl

+1

一般に、並列配列を並列ベクトルよりも優先します。少なくとも、連結がベクトルに追加されるまでです。 – axel22

+0

@huynhjl - 最初の2つの漫画に描かれた私の人生を見て、プレゼンテーションが価値があったことは分かっていました。ありがとう! – fbl

答えて

11

あなたはマイクロベンチマークの病気があります。 JITのコンパイル段階をベンチマークする可能性が最も高いです。 JITをウォームアップする必要があります。

おそらく最も良い考えは、http://code.google.com/p/caliper/のようなマイクロベンチマークフレームワークを使用して、それをすべて処理することです。

編集:キャリパーベンチマークScalaのプロジェクトのための素晴らしいSBT Templateがあり、参照されるようにあなたはリンゴにリンゴを比較していない、from this blog post

0

方法について

val numTargetsInRange_sequential = parTargets.filter(within50) 

また、フィルタ操作ではなくマップを使用すると、より印象的な結果が得られます。

7

物事はスピードアップしますが、それは、シーケンシャル対並列とは何の関係もありません。 JVMにはJIT(Just in Time)コンパイラがあり、コードが一定回数使用された後でのみバイトコードをコンパイルします。最初の反復で見られるのは、まだJIT化されていないコードと、進行中のJITコンパイル自体の時間が遅いことです。

Sequential (ms): 312 
Iteration[1] Executed in 117 ms 
Iteration[2] Executed in 112 ms 
Iteration[3] Executed in 112 ms 
Iteration[4] Executed in 112 ms 
Iteration[5] Executed in 114 ms 
Iteration[6] Executed in 113 ms 
Iteration[7] Executed in 113 ms 
Iteration[8] Executed in 117 ms 
Iteration[9] Executed in 113 ms 
Iteration[10] Executed in 111 ms 

しかし、それはすべてのシーケンシャルだ:それはここにすべてのシーケンシャルをだよう.parを削除すると、私は私のマシン(私は古いマシンを使用している原因の10倍以下の繰り返し)に表示されるものです! JVM -XX:+PrintCompilationJAVA_OPTSに設定されているか、-J-XX:+PrintCompilation scalaオプションを使用してJVMに関してJVMの動作を確認できます。最初の繰り返しでは、何がJITされているかを示す多数のJVM印刷ステートメントが表示されます。後でだから、リンゴにリンゴを比較する

、パーなしのあなたの最初の実行、私が手.parを使用した場合、その後、パーを追加して、私のデュアルコアで同じプログラムを実行します。。

Sequential (ms): 329 
Iteration[1] Executed in 197 ms 
Iteration[2] Executed in 60 ms 
Iteration[3] Executed in 57 ms 
Iteration[4] Executed in 58 ms 
Iteration[5] Executed in 59 ms 
Iteration[6] Executed in 73 ms 
Iteration[7] Executed in 56 ms 
Iteration[8] Executed in 60 ms 
Iteration[9] Executed in 58 ms 
Iteration[10] Executed in 57 ms 

ので、安定していれば2倍のスピードアップが可能です。

オンrelaあなたが慎重にしたい他のものは、ボクシングとun-boxingです。特にJavaと比較している場合は、特に注意してください。フィルタのようなスカラライブラリの高次関数は、プリミティブ型のボクシングとアンボクシングを行っています。これは、通常、JavaからScalaにコードを変換する人にとっては初期の失望の原因です。

forはタイミングの外にあるとして、それはこの場合には適用されませんが、for代わりのwhileを使用するためのいくつかのコストもあるが、-optimize scalacフラグを使用している場合2.9.1コンパイラはまともな仕事をしてする必要があります。

3

これまでに述べたJITの最適化の次に、評価する必要がある重要な概念は、問題が並列化にあるかどうかです。スプリット、スレッドコーディネーション、ジョインのコストは、パラレルで行う利点があります。 Scalaはこの複雑さをあなたから隠しますが、良い結果のためにこれをいつ適用するかを知る必要があります。

あなたのケースでは、大量の操作を実行していますが、それぞれの操作はほとんどCPUがほとんどありません。動作している並列コレクションを確認するには、ユニット単位で重い操作を試してください。似たScalaのプレゼンテーションのために

、私は数が素数であるかどうかを計算するアルゴ簡単な(innefficient)を使用: def isPrime(x:Int) = (2 to x/2).forall(y=>x%y!=0)

その後プライムあるコレクションの中の数字を決定するためにあなたが提示同じロジックを使用:

val col = 1 to 1000000 
col.filter(isPrime(_)) // sequential 
col.par.filter(isPrime(_)) // parallel 

CPUの動作は、実際に両者の差を示した: prime numbers: sequential vs parallel

時間が3.5倍のベティについてでしたrを4コアCPUの並列コレクションに使用します。

関連する問題