(過去に多少関連する質問がいくつかあったことは知っていますが、L1d キャッシュ ミスとハイパースレッディング/SMT に関する質問は見つかりませんでした。)
False Sharing、MESI/MOESI Cache Coherence Protocols などの非常に興味深いものについて数日間読んだ後、C で小さな「ベンチマーク」を作成して (以下を参照)、False Sharing の動作をテストすることにしました。
私は基本的に 8 つの double の配列を持っているので、1 つのキャッシュ ラインと隣接する配列位置をインクリメントする 2 つのスレッドに収まります。
この時点で、私は Ryzen 5 3600 を使用していることを述べる必要があります。そのトポロジーはここで見ることができます。
2 つのスレッドを作成し、それらを 2 つの異なる論理コアに固定し、それぞれが独自の配列位置にアクセスして更新します。つまり、スレッド A は array[2] を更新し、スレッド B は array[3] を更新します。
同じコアに属するハードウェア スレッド#0と#6を使用してコードを実行すると、(トポロジ ダイアグラムに示すように) L1d キャッシュを共有すると、実行時間は約 5 秒になります。
共通のキャッシュを持たないスレッド#0と#11を使用すると、完了するまでに ~ 9.5 秒かかります。この場合、「キャッシュ ライン ピンポン」が進行しているため、この時間差が予想されます。
ただし、スレッド#0および#11を使用している場合、L1d キャッシュ ミスはスレッド#0および#6で実行する場合よりも少なくなります。
私の推測では、共通キャッシュを持たないスレッド#0と#11を使用している場合、一方のスレッドが共有キャッシュ ラインの内容を更新すると、MESI/MOESI プロトコルに従って、もう一方のコアのキャッシュ ラインが無効になります。そのため、ピンポンが発生していても、(スレッド#0および#6で実行している場合と比較して) キャッシュ ミスはそれほど多くなく、一連の無効化とキャッシュ ライン ブロックがコア間で転送されるだけです。
では、共通の L1d キャッシュを持つスレッド #0 と #6 を使用すると、キャッシュ ミスが増えるのはなぜでしょうか?
(スレッド#0と#6にも共通の L2 キャッシュがありますが、ここでは重要ではないと思います。キャッシュ ラインが無効になると、メイン メモリ (MESI) または別のコアのいずれかからフェッチする必要があるためです。キャッシュ (MOESI) であるため、L2 が必要なデータを保持することさえ不可能であるように思われますが、それを要求することもできません) .
もちろん、1 つのスレッドが L1d キャッシュ ラインに書き込むと、キャッシュ ラインは「ダーティ」になりますが、なぜそれが問題になるのでしょうか? 同じ物理コアに存在する他のスレッドは、新しい「ダーティ」値を問題なく読み取れるはずではありませんか?
TLDR : False Sharing をテストすると、2 つの兄弟スレッド (同じ物理コアに属するスレッド) を使用すると、2 つの異なる物理コアに属するスレッドを使用する場合よりも、L1d キャッシュ ミスが約3 倍多くなります。(2.34% 対 0.75% のミス率、3 億 9600 万対 1 億 1800 万のミスの絶対数)。なぜそれが起こっているのですか?
(L1d キャッシュ ミスなどのすべての統計は、Linux の perf ツールを使用して測定されます。)
また、マイナーな二次的な質問ですが、兄弟スレッドが 6 桁離れた ID でペアになっているのはなぜですか? つまり、スレッド 0 の兄弟はスレッド 6 です。スレッド i の兄弟はスレッド i+6 です。それは何らかの形で役立ちますか?Intel と AMD の両方の CPU でこれに気付きました。
私はコンピュータ アーキテクチャに非常に興味があり、まだ学習中なので、上記のいくつかは間違っている可能性があります。申し訳ありません。
だから、これは私のコードです。2 つのスレッドを作成し、それらを特定の論理コアにバインドしてから、隣接するキャッシュ ラインの場所にアクセスするだけです。
#define _GNU_SOURCE
#include <stdio.h>
#include <sched.h>
#include <stdlib.h>
#include <sys/random.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
struct timespec tstart, tend;
static cpu_set_t cpuset;
typedef struct arg_s
{
int index;
double *array_ptr;
} arg_t;
void *work(void *arg)
{
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
int array_index = ((arg_t*)arg)->index;
double *ptr = ((arg_t*)arg)->array_ptr;
for(unsigned long i=0; i<1000000000; i++)
{
//it doesn't matter which of these is used
// as long we are hitting adjacent positions
ptr[array_index] ++;
// ptr[array_index] += 1.0e5 * 4;
}
return NULL;
}
int main()
{
pthread_t tid[2];
srand(time(NULL));
static int cpu0 = 0;
static int cpu6 = 6; //change this to say 11 to run with threads 0 and 11
CPU_ZERO(&cpuset);
CPU_SET(cpu0, &cpuset);
CPU_SET(cpu6, &cpuset);
double array[8];
for(int i=0; i<8; i++)
array[i] = drand48();
arg_t *arg0 = malloc(sizeof(arg_t));
arg_t *arg1 = malloc(sizeof(arg_t));
arg0->index = 0; arg0->array_ptr = array;
arg1->index = 1; arg1->array_ptr = array;
clock_gettime(CLOCK_REALTIME, &tstart);
pthread_create(&tid[0], NULL, work, (void*)arg0);
pthread_create(&tid[1], NULL, work, (void*)arg1);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
clock_gettime(CLOCK_REALTIME, &tend);
}
私はGCC 10.2.0 Compilingを次のように使用していますgcc -pthread p.c -o p
次に、perf record ./p --cpu=0,6
スレッド 0,6 と 0,11 をそれぞれ使用する場合、 --cpu=0,11 で実行するか、同じことを実行します。
perf stat -d ./p --cpu=0,6
他の場合は --cpu=0,11 で実行中または同じ
スレッド0と6で実行:
Performance counter stats for './p --cpu=0,6':
9437,29 msec task-clock # 1,997 CPUs utilized
64 context-switches # 0,007 K/sec
2 cpu-migrations # 0,000 K/sec
912 page-faults # 0,097 K/sec
39569031046 cycles # 4,193 GHz (75,00%)
5925158870 stalled-cycles-frontend # 14,97% frontend cycles idle (75,00%)
2300826705 stalled-cycles-backend # 5,81% backend cycles idle (75,00%)
24052237511 instructions # 0,61 insn per cycle
# 0,25 stalled cycles per insn (75,00%)
2010923861 branches # 213,083 M/sec (75,00%)
357725 branch-misses # 0,02% of all branches (75,03%)
16930828846 L1-dcache-loads # 1794,034 M/sec (74,99%)
396121055 L1-dcache-load-misses # 2,34% of all L1-dcache accesses (74,96%)
<not supported> LLC-loads
<not supported> LLC-load-misses
4,725786281 seconds time elapsed
9,429749000 seconds user
0,000000000 seconds sys
スレッド0と11で実行:
Performance counter stats for './p --cpu=0,11':
18693,31 msec task-clock # 1,982 CPUs utilized
114 context-switches # 0,006 K/sec
1 cpu-migrations # 0,000 K/sec
903 page-faults # 0,048 K/sec
78404951347 cycles # 4,194 GHz (74,97%)
1763001213 stalled-cycles-frontend # 2,25% frontend cycles idle (74,98%)
71054052070 stalled-cycles-backend # 90,62% backend cycles idle (74,98%)
24055983565 instructions # 0,31 insn per cycle
# 2,95 stalled cycles per insn (74,97%)
2012326306 branches # 107,650 M/sec (74,96%)
553278 branch-misses # 0,03% of all branches (75,07%)
15715489973 L1-dcache-loads # 840,701 M/sec (75,09%)
118455010 L1-dcache-load-misses # 0,75% of all L1-dcache accesses (74,98%)
<not supported> LLC-loads
<not supported> LLC-load-misses
9,430223356 seconds time elapsed
18,675328000 seconds user
0,000000000 seconds sys