私は非常に長い回答を書き始め、これがどのように機能するかを正確に説明しましたが、正確な詳細についてはおそらく十分に知らないことに気づきました。だから私はより短い答えをします....
したがって、あるプロセッサに何かを書き込むとき、そのプロセッサのキャッシュにまだない場合は、フェッチする必要があり、プロセッサがデータを読み取った後、実際の書き込みが実行されます。そうすることで、システム内の他のすべてのプロセッサにキャッシュ無効化メッセージが送信されます。これらはその後、コンテンツを破棄します。別のプロセッサが「ダーティ」コンテンツを持っている場合、それ自体がデータを書き出し、無効化を要求します。この場合、最初のプロセッサは書き込みを完了する前にデータをリロードする必要があります (そうでない場合、同じキャッシュライン内の他の要素壊れる可能性があります)。
そのキャッシュラインに関心のある他のすべてのプロセッサで、キャッシュに読み戻す必要があります。
__sync_fetch_and_add() は "ロック" プレフィックスを使用します [x86 では、他のプロセッサは異なる場合がありますが、"命令ごと" のロックをサポートするプロセッサの一般的な考え方はほぼ同じです] - これにより、"I want this cacheline EXCLUSIVELY,他の人はあきらめて無効にしてください。」最初のケースと同様に、プロセッサは、別のプロセッサがダーティにした可能性のあるものをすべて再読み取りする必要があります。
メモリ バリアは、データが「安全に」更新されることを保証するものではありません。「このインストラクションが終了するまでに、これまでに (メモリに対して) 発生したことがすべてのプロセッサに表示される」ことを確認するだけです。
プロセッサの使用を最適化する最善の方法は、共有をできるだけ少なくすることです。特に、「誤った共有」を避けることです。何年も前のベンチマークでは、[単純化された] 次のような構造がありました。
struct stuff {
int x[2];
... other data ... total data a few cachelines.
} data;
void thread1()
{
for( ... big number ...)
data.x[0]++;
}
void thread2()
{
for( ... big number ...)
data.x[1]++;
}
int main()
{
start = timenow();
create(thread1);
create(thread2);
end = timenow() - start;
}
スレッド 1 が x[0] に書き込むたびに、スレッド 2 のプロセッサは x[1] のコピーを削除する必要があり、その逆も同様でした。その結果、SMP テスト [対スレッド 1 の実行のみ] は約 15 回実行されました。もっとゆっくり。次のように構造体を変更します。
struct stuff {
int x;
... other data ...
} data[2];
と
void thread1()
{
for( ... big number ...)
data[0].x++;
}
1 つのスレッド バリアントの 200% を取得しました [ギブまたはテイク数パーセント]
そうです、プロセッサには、プロセッサがメモリに書き込んでいるときに書き込み操作が格納されるバッファのキューがあります。メモリ バリア (mfence、sfence、または lfence) 命令は、プロセッサが次の命令に進む前に未処理の読み取り/書き込み、書き込み、または読み取りタイプの操作が完全に終了していることを確認するために存在します。通常、プロセッサは後続の命令を順調に実行し続け、最終的にメモリ操作は何らかの方法で実行されます。最新のプロセッサには多くの並列操作とバッファがいたるところにあるため、実際に何かが最終的に到達する場所にたどり着くまでにはかなりの時間がかかる場合があります。そのため、続行する前に実際に何かが行われたことを確認することが重要な場合 (たとえば、ビデオ メモリに一連の命令を書き込んで、それらの命令の実行を開始したい場合は、「命令」の書き込みが実際に終了し、プロセッサの他の部分が終了していることを確認する必要があります。 t はまだそれを仕上げる作業を行っています。だから使用するsfence
書き込みが実際に行われたことを確認するためです。これはあまり現実的な例ではないかもしれませんが、おわかりいただけると思います。)