8

一連のメモリ内データに対して高度な並列処理を実行する .NET アプリケーションで奇妙な動作が発生しました。

マルチコア プロセッサ (IntelCore2 Quad Q6600 2.4GHz) で実行すると、データを処理するために複数のスレッドが開始されるため、非線形スケーリングが発生します。

シングル コアで非マルチスレッド ループとして実行すると、プロセスは 1 秒あたり約 240 万回の計算を完了できます。4 つのスレッドとして実行すると、4 倍のスループット (1 秒あたり約 900 万回の計算) が期待できますが、残念ながら違います。実際には、1 秒あたり約 410 万回しか完了しません。予想されるスループットにはかなり足りません。

さらに、この動作は、PLINQ、スレッド プール、または明示的に作成された 4 つのスレッドのいずれを使用しても発生します。かなり奇妙...

CPU 時間を使用してマシン上で実行されているものは他にありません。また、計算に関与するロックやその他の同期オブジェクトもありません。データを先に進める必要があります。プロセスの実行中に perfmon データを見て、これを (可能な範囲で) 確認しました ... スレッドの競合やガベージ コレクション アクティビティは報告されていません。

現時点での私の理論:

  1. すべての手法 (スレッド コンテキスト スイッチなど) のオーバーヘッドが計算を圧倒します。
  2. スレッドは 4 つのコアのそれぞれに割り当てられておらず、同じプロセッサ コアでの待機に時間を費やしています。この理論をテストする方法がわかりません...
  3. .NET CLR スレッドが予期した優先度で実行されていないか、内部オーバーヘッドが隠れています。

以下は、同じ動作を示すコードからの代表的な抜粋です。

    var evaluator = new LookupBasedEvaluator();

    // find all ten-vertex polygons that are a subset of the set of points
    var ssg = new SubsetGenerator<PolygonData>(Points.All, 10);

    const int TEST_SIZE = 10000000;  // evaluate the first 10 million records

    // materialize the data into memory...
    var polygons = ssg.AsParallel()
                      .Take(TEST_SIZE)
                      .Cast<PolygonData>()
                      .ToArray();

    var sw1 = Stopwatch.StartNew();
    // for loop completes in about 4.02 seconds... ~ 2.483 million/sec
    foreach( var polygon in polygons )
        evaluator.Evaluate(polygon);
    s1.Stop(); 
    Console.WriteLine( "Linear, single core loop: {0}", s1.ElapsedMilliseconds );

    // now attempt the same thing in parallel using Parallel.ForEach...
    // MS documentation indicates this internally uses a worker thread pool
    // completes in 2.61 seconds ... or ~ 3.831 million/sec
    var sw2 = Stopwatch.StartNew();
    Parallel.ForEach(polygons, p => evaluator.Evaluate(p));
    sw2.Stop();
    Console.WriteLine( "Parallel.ForEach() loop: {0}", s2.ElapsedMilliseconds );

    // now using PLINQ, er get slightly better results, but not by much
    // completes in 2.21 seconds ... or ~ 4.524 million/second
    var sw3 = Stopwatch.StartNew();
    polygons.AsParallel(Environment.ProcessorCount)
            .AsUnordered() // no sure this is necessary...
            .ForAll( h => evalautor.Evaluate(h) );
    sw3.Stop();
    Console.WriteLine( "PLINQ.AsParallel.ForAll: {0}", s3.EllapsedMilliseconds );

    // now using four explicit threads:
    // best, still short of expectations at 1.99 seconds = ~ 5 million/sec
    ParameterizedThreadStart tsd = delegate(object pset) { foreach (var p in (IEnumerable<Card[]>) pset) evaluator.Evaluate(p); };
     var t1 = new Thread(tsd);
     var t2 = new Thread(tsd);
     var t3 = new Thread(tsd);
     var t4 = new Thread(tsd);

     var sw4 = Stopwatch.StartNew(); 
     t1.Start(hands);
     t2.Start(hands);
     t3.Start(hands);
     t4.Start(hands);
     t1.Join();
     t2.Join();
     t3.Join();
     t4.Join();
     sw.Stop();
     Console.WriteLine( "Four Explicit Threads: {0}", s4.EllapsedMilliseconds );
4

4 に答える 4

5

だから私は最終的に問題が何であるかを理解しました.SOコミュニティと共有することは有益だと思います.

非線形パフォーマンスの問題全体は、Evaluate()メソッド内の単一行の結果でした。

var coordMatrix = new long[100];

は何百万回も呼び出されるためEvaluate()、このメモリ割り当ては何百万回も発生していました。たまたま、CLR はメモリを割り当てるときにスレッド間同期を内部的に実行します。そうしないと、複数のスレッドでの割り当てが誤ってオーバーラップする可能性があります。配列をメソッド ローカル インスタンスから、一度だけ割り当てられる (ただし、メソッド ローカル ループで初期化する) クラス インスタンスに変更すると、スケーラビリティの問題が解消されました。

通常、単一のメソッドのスコープ内でのみ使用される (そして意味のある) 変数のクラスレベルのメンバーを作成することはアンチパターンです。しかし、この場合、可能な限り最大のスケーラビリティが必要なので、この最適化を受け入れます (そして文書化します)。

エピローグ:この変更を行った後、並行プロセスは 1 秒あたり 1220 万回の計算を達成することができました。

PS問題を特定して診断するのに役立った MSDN ブログへの密接なリンクについて、Igor Ostrovsky に感謝します。

于 2009-09-21T03:10:23.200 に答える
5

この記事をご覧ください: http://blogs.msdn.com/pfxteam/archive/2008/08/12/8849984.aspx

具体的には、並列領域でのメモリ割り当てを制限し、書き込みを注意深く調べて、他のスレッドが読み書きするメモリ位置の近くで書き込みが発生しないようにします。

于 2009-09-20T19:03:00.067 に答える
3

並列化には固有のオーバーヘッドがあるため、並列アルゴリズムでは、順次アルゴリズムと比較して非線形スケーリングが予想されます。(もちろん、できるだけ近づきたいのが理想的です。)

さらに、通常、並列アルゴリズムでは処理する必要がある特定の事項がありますが、順次アルゴリズムでは処理する必要はありません。同期(これは実際に作業を妨げる可能性があります)以外にも、発生する可能性のあることがいくつかあります。

  • CPUとOSは、常にアプリケーションに専念することはできません。したがって、他のプロセスに作業を行わせるために、コンテキスト切り替えを時々行う必要があります。単一のコアのみを使用している場合、他に3つのコアから選択できるため、プロセスが切り替えられる可能性は低くなります。他に何も実行されていないと思われる場合でも、OSまたは一部のサービスがバックグラウンド作業を実行している可能性があることに注意してください。
  • 各スレッドが大量のデータにアクセスしていて、このデータがスレッド間で共通ではない場合、これらすべてをCPUキャッシュに保存できない可能性があります。つまり、より多くのメモリアクセスが必要になり、(比較的)低速になります。

私の知る限り、現在の明示的なアプローチでは、スレッド間で共有イテレータを使用しています。処理が配列全体で大きく異なる場合、これは問題のない解決策ですが、要素がスキップされるのを防ぐために同期オーバーヘッドが発生する可能性があります(現在の要素を取得して内部ポインターを次の要素に移動することは、防止するための不可分操作である必要があります)要素をスキップします)。

したがって、各要素の処理時間は要素の位置に関係なくほぼ等しいと予想されると仮定して、配列を分割することをお勧めします。1000万のレコードがあるとすると、スレッド1に要素0〜2,499,999で機能するように指示し、スレッド2で要素2,500,000〜4,999,999などで機能するようにします。各スレッドにIDを割り当て、これを使用して実際の範囲を計算できます。

もう1つの小さな改善は、メインスレッドを計算するスレッドの1つとして機能させることです。しかし、私が正しく覚えていれば、それは非常に小さなことです。

于 2009-09-20T00:58:33.747 に答える
0

私は確かに直線的な関係を期待していませんが、それよりも大きな利益が見られると思っていたでしょう. すべてのコアで CPU 使用率が最大になっていると想定しています。私の頭の上からちょうどいくつかの考え。

  • 同期が必要な共有データ構造を (明示的または暗黙的に) 使用していますか?
  • ボトルネックがどこにあるかを判断するために、パフォーマンス カウンターのプロファイリングまたは記録を試みましたか? もっと手がかりを教えてください。

編集:申し訳ありませんが、あなたが私の両方のポイントにすでに対処していることに気付きました.

于 2009-09-20T00:31:32.913 に答える