このスニペットを見てください:
#include <atomic>
#include <thread>
typedef volatile unsigned char Type;
// typedef std::atomic_uchar Type;
void fn(Type *p) {
for (int i=0; i<500000000; i++) {
(*p)++;
}
}
int main() {
const int N = 4;
std::thread thr[N];
alignas(64) Type buffer[N*64];
for (int i=0; i<N; i++) {
thr[i] = std::thread(&fn, &buffer[i*1]);
}
for (int i=0; i<N; i++) {
thr[i].join();
}
}
この小さなプログラムは、4 つの異なるスレッドから、隣接する 4 つのバイトを何度もインクリメントします。以前は、ルールを使用していました。異なるスレッドから同じキャッシュ ラインを使用しないでください。キャッシュ ラインの共有は良くないからです。N=4
したがって、4 スレッド バージョン ( ) は 1 スレッド バージョン ( ) よりもはるかに遅いと予想しましたN=1
。
ただし、これらは私の測定値です(Haswell CPUで):
- N=1: 1 秒
- N=4: 1.2秒
だからN=4
それほど遅くはありません。*1
別のキャッシュ ラインを使用すると( に置き換えます*64
)、N=4
少し速くなります: 1.1 秒。
アトミック アクセス (のコメントを入れ替えるtypedef
) の同じ測定値、同じキャッシュ ライン:
- N=1: 3.1 秒
- N=4: 48 秒
したがって、N=4
ケースははるかに遅くなります(予想どおり)。異なるキャッシュ ラインを使用すると、3.3 秒N=4
と同様のパフォーマンスが得られます。N=1
これらの結果の背後にある理由がわかりません。N=4
非アトミックなケースで深刻な速度低下が発生しないのはなぜですか? 4 つのコアはキャッシュに同じメモリを持っているので、何らかの方法で同期する必要がありますね。ほぼ完全に並行して実行するにはどうすればよいでしょうか? アトミックなケースだけで深刻な減速が発生するのはなぜですか?
この場合、メモリがどのように更新されるかを理解する必要があると思います。buffer
最初は、キャッシュにコアはありません。1回のfor
反復の後 ( fn
)、4 つのコアすべてbuffer
がキャッシュ ラインを保持していますが、各コアは異なるバイトを書き込みます。これらのキャッシュラインはどのように同期されますか (アトミックでない場合)? キャッシュは、どのバイトがダーティかをどのように認識しますか? または、このケースを処理する他のメカニズムはありますか? なぜこのメカニズムはアトミックメカニズムよりもはるかに安価なのですか (実際にはほとんど無料です)?