一連のメモリ内データに対して高度な並列処理を実行する .NET アプリケーションで奇妙な動作が発生しました。
マルチコア プロセッサ (IntelCore2 Quad Q6600 2.4GHz) で実行すると、データを処理するために複数のスレッドが開始されるため、非線形スケーリングが発生します。
シングル コアで非マルチスレッド ループとして実行すると、プロセスは 1 秒あたり約 240 万回の計算を完了できます。4 つのスレッドとして実行すると、4 倍のスループット (1 秒あたり約 900 万回の計算) が期待できますが、残念ながら違います。実際には、1 秒あたり約 410 万回しか完了しません。予想されるスループットにはかなり足りません。
さらに、この動作は、PLINQ、スレッド プール、または明示的に作成された 4 つのスレッドのいずれを使用しても発生します。かなり奇妙...
CPU 時間を使用してマシン上で実行されているものは他にありません。また、計算に関与するロックやその他の同期オブジェクトもありません。データを先に進める必要があります。プロセスの実行中に perfmon データを見て、これを (可能な範囲で) 確認しました ... スレッドの競合やガベージ コレクション アクティビティは報告されていません。
現時点での私の理論:
- すべての手法 (スレッド コンテキスト スイッチなど) のオーバーヘッドが計算を圧倒します。
- スレッドは 4 つのコアのそれぞれに割り当てられておらず、同じプロセッサ コアでの待機に時間を費やしています。この理論をテストする方法がわかりません...
- .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 );