私はマルチスレッドのプログラムでキャッシュ競合の影響を説明するプログラムを書いていました。私の最初のカットは、long
という配列を作成し、隣接するアイテムの変更がどのように競合を引き起こすかを示すことでした。ここにプログラムがあります。アレイの並行修正が遅いのはなぜですか?
const long maxCount = 500000000;
const int numThreads = 4;
const int Multiplier = 1;
static void DoIt()
{
long[] c = new long[Multiplier * numThreads];
var threads = new Thread[numThreads];
// Create the threads
for (int i = 0; i < numThreads; ++i)
{
threads[i] = new Thread((s) =>
{
int x = (int)s;
while (c[x] > 0)
{
--c[x];
}
});
}
// start threads
var sw = Stopwatch.StartNew();
for (int i = 0; i < numThreads; ++i)
{
int z = Multiplier * i;
c[z] = maxCount;
threads[i].Start(z);
}
// Wait for 500 ms and then access the counters.
// This just proves that the threads are actually updating the counters.
Thread.Sleep(500);
for (int i = 0; i < numThreads; ++i)
{
Console.WriteLine(c[Multiplier * i]);
}
// Wait for threads to stop
for (int i = 0; i < numThreads; ++i)
{
threads[i].Join();
}
sw.Stop();
Console.WriteLine();
Console.WriteLine("Elapsed time = {0:N0} ms", sw.ElapsedMilliseconds);
}
私は、Visual Studio 2010、リリースモードでコンパイルされたプログラム、.NET 4.0のターゲットを実行している、 "どれCPU"、および付属のデバッガ(Ctrlキー+ F5)することなく、64ビットランタイムで実行。
このプログラムは、私のシステムで約1,700ミリ秒で実行され、1つのスレッドで実行されます。 2つのスレッドでは、25秒以上かかります。キャッシュの競合であることが分かったので、私はMultipler = 8
と設定して再度実行しました。結果は12秒ですので、競合は少なくとも部分でした。
Multiplier
が8を超えても、パフォーマンスが向上しません。
変数が隣接している場合、similar program that doesn't use an arrayは2つのスレッドで約2,200ミリ秒しかかかりません。変数を分割すると、2つのスレッドバージョンは、シングルスレッドバージョンと同じ時間で実行されます。
問題がアレイインデックスのオーバーヘッドであった場合は、シングルスレッドバージョンで表示されることが予想されます。配列を変更するときに何らかの相互排除が行われているように見えますが、何か分かりません。
生成されたILを見ると、あまり啓蒙されません。分解も見ていませんでした。逆アセンブリは、(私は思うが)ランタイムライブラリへの呼び出しのカップルを示していますが、私はそれらに踏み込むことができませんでした。
最近、windbgやその他の低レベルデバッグツールに堪能ではありません。私がそれらを必要として以来、本当に長い時間が経ちました。だから私は困惑している。
私の唯一の仮説は、ランタイムコードがすべての書き込みで「ダーティ」フラグを設定しているということです。配列が列挙されている間に配列が変更された場合、例外をスローするのをサポートするために必要なもののようです。しかし私は、その仮説を裏付ける証拠がないことをすぐに認めます。
この大きな減速の原因は何ですか?
FYI配列は、列挙されている間に変更されてもスローされません。 (実装にはその権利がありますが、実際にはCLR実装ではそうしていません) –
@Eric:情報をありがとう。それは知って良いです。 –