シングルプロデューサシングルコンシューマキューアルゴリズムでCPUキャッシュのパフォーマンスを理解しようとしていますが、場合によってはパフォーマンスの低下の原因を正確に見つけることができません。以下の単純化されたテストプログラムは、L1キャッシュの欠落がほとんどなく実行されますが、メモリアクセスパターンがやや希薄な場合、CPUバックエンドで大量のサイクルを消費します。この場合、L1の欠落はほとんどありませんが、CPUバックエンドが停止する原因は何ですか?原因を見つけるために何を測定する必要がありますか?
私はこの質問がLinuxやperf_eventに関するものではなく、CPUとキャッシュのアーキテクチャに関するものであることを知っています。より適切なスタック交換は何ですか? Stackoverflowはソフトウェアにもっと焦点を当てていますか? ServerfaultまたはSuperuserもこれらのトピックを対象としていません。電子スタック交換は完全にCPUアーキテクチャに限定されない。によるとこのメタデータは2013年のものです。、エレクトロニクスが最適です。すべてのテストがLinuxで行われ、経験を通してGillesなどの一部の専門家は、ここで何が起こっているのかを知っていると思ったので、ここにこれを投稿することです。おそらく最良の方法はAMDフォーラムに投稿することです。ただし、実際に投稿を公開しないと、「投稿の超過が検出されました(ユーザーが600秒以内に2つ以上のメッセージを投稿しようとしました)」というエラーが発生するため、そこに投稿を投稿できません。彼らのフォーラムがとても静かであることは当然です。
私のCPUは、192KiB L1dキャッシュを備えたZen 2 "Renoir"であるAMD Ryzen 5 PRO 4650Gです。テストmemcpy
プログラム:
// demo_memcpy_test_speed-gap.c
#include <stdio.h>
#include <stdint.h>
#include <time.h>
#include <cstring>
#define PACKET_SIZE 8 // 16 32 // <--- it is really the stride of the memcpy over the mem array
#define SIZE_TO_MEMCPY 8 // memcpy only the first 8 bytes of the "packet"
const static long long unsigned n_packets = 512; // use few packets, to fit in L2 etc
static long long unsigned repeat = 1000*1000 * 2 * 2; // repeat many times to get enough stats in perf
const static long long unsigned n_max_data_bytes = n_packets * PACKET_SIZE;
#define CACHE_LINE_SIZE 64 // align explicitly just in case
alignas(CACHE_LINE_SIZE) uint8_t data_in [n_max_data_bytes];
alignas(CACHE_LINE_SIZE) uint8_t data_out [n_packets][PACKET_SIZE];
int main(int argc, char* argv[])
{
printf("memcpy_test.c standard\n");
printf("PACKET_SIZE %d SIZE_TO_MEMCPY %d\n", PACKET_SIZE, SIZE_TO_MEMCPY);
//
// warmup the memory
// i.e. access the memory to make sure Linux has set up the virtual mem tables
...
{
printf("\nrun memcpy\n");
long long unsigned n_bytes_copied = 0;
long long unsigned memcpy_ops = 0;
start_setup = clock();
for (unsigned rep=0; rep<repeat; rep++) {
uint8_t* data_in_ptr = &data_in [0];
for (unsigned long long i_packet=0; i_packet<n_packets; i_packet++) {
// copy only SIZE_TO_MEMCPY of the in data array to the out
uint8_t* data_out_ptr = &(data_out [i_packet][0]);
memcpy(data_out_ptr, data_in_ptr, SIZE_TO_MEMCPY*sizeof(uint8_t));
memcpy_ops++;
n_bytes_copied += SIZE_TO_MEMCPY;
data_in_ptr += PACKET_SIZE;
}
}
end_setup = clock();
cpu_time_used_setup = ((double) (end_setup - start_setup)) / CLOCKS_PER_SEC;
printf("memcpy() took %f seconds to execute\n", cpu_time_used_setup);
printf("%f Mops\n", memcpy_ops/(1000000*cpu_time_used_setup));
printf("%llu bytes\n", n_bytes_copied);
printf("%f Mbytes/s\n", n_bytes_copied/(1000000*cpu_time_used_setup));
}
} // end of main
-O1
本当に効率的なループを得るために作られましたmemcpy
。
g++ -g -O1 ./demo_memcpy_test_speed-gap.c
memcpy
コメントオプションに示されているように、繰り返しの手順は次のとおりですperf record
。
sudo perf record -F 999 -e stalled-cycles-backend -- ./a.out
sudo perf report
...select main
8に設定すると、PACKET_SIZE
コードは非常に効率的です。
│ for (unsigned long long i_packet=0; i_packet<n_packets; i_packet++) {
│11b:┌─→mov %rbx,%rax
│ │memcpy():
│11e:│ mov (%rcx,%rax,8),%rdx
100.00 │ │ mov %rdx,(%rsi,%rax,8)
│ │main():
│ │ add $0x1,%rax
│ │ cmp $0x200,%rax
│ │↑ jne 11e
│ │for (unsigned rep=0; rep<repeat; rep++) {
│ │ sub $0x1,%edi
│ └──jne 11b
1024に設定すると、PACKET_SIZE
コードは256と同じですが、次のadd $0x100..
ように変更されます0x400
。
│ lea _end,%rsi
│140:┌─→mov %rbp,%rdx
│ │
│ │ lea data_in,%rax
│ │
│ │__fortify_function void *
│ │__NTH (memcpy (void *__restrict __dest, const void *__restrict __src,
│ │size_t __len))
│ │{
│ │return __builtin___memcpy_chk (__dest, __src, __len,
│14a:│ mov (%rax),%rcx
│ │memcpy():
96.31 │ │ mov %rcx,(%rdx)
│ │
1.81 │ │ add $0x400,%rax
0.20 │ │ add $0x400,%rdx
1.12 │ │ cmp %rsi,%rax
0.57 │ │↑ jne 14a
│ │ sub $0x1,%edi
│ └──jne 140
PACKET_SIZE
8、16、32などの値に設定して実行しました。パフォーマンスカウントは8と32です。
sudo perf stat -e task-clock,instructions,cycles,stalled-cycles-frontend,stalled-cycles-backend \
-e L1-dcache-loads,L1-dcache-load-misses,L1-dcache-prefetches \
-e l2_cache_accesses_from_dc_misses,l2_cache_hits_from_dc_misses,l2_cache_misses_from_dc_misses \
-- ./a.out
PACKET_SIZE 8 SIZE_TO_MEMCPY 8
...
Performance counter stats for './a.out':
503.43 msec task-clock # 0.998 CPUs utilized
10,323,618,071 instructions # 4.79 insn per cycle
# 0.01 stalled cycles per insn (29.11%)
2,154,694,815 cycles # 4.280 GHz (29.91%)
5,148,993 stalled-cycles-frontend # 0.24% frontend cycles idle (30.70%)
55,922,538 stalled-cycles-backend # 2.60% backend cycles idle (30.99%)
4,091,862,625 L1-dcache-loads # 8.128 G/sec (30.99%)
24,211 L1-dcache-load-misses # 0.00% of all L1-dcache accesses (30.99%)
18,745 L1-dcache-prefetches # 37.234 K/sec (30.37%)
30,749 l2_cache_accesses_from_dc_misses # 61.079 K/sec (29.57%)
21,046 l2_cache_hits_from_dc_misses # 41.805 K/sec (28.78%)
9,095 l2_cache_misses_from_dc_misses # 18.066 K/sec (28.60%)
PACKET_SIZE 32 SIZE_TO_MEMCPY 8
...
Performance counter stats for './a.out':
832.83 msec task-clock # 0.999 CPUs utilized
12,289,501,297 instructions # 3.46 insn per cycle
# 0.11 stalled cycles per insn (29.42%)
3,549,297,932 cycles # 4.262 GHz (29.64%)
5,552,837 stalled-cycles-frontend # 0.16% frontend cycles idle (30.12%)
1,349,663,970 stalled-cycles-backend # 38.03% backend cycles idle (30.25%)
4,144,875,512 L1-dcache-loads # 4.977 G/sec (30.25%)
772,968 L1-dcache-load-misses # 0.02% of all L1-dcache accesses (30.24%)
539,481 L1-dcache-prefetches # 647.767 K/sec (30.25%)
532,879 l2_cache_accesses_from_dc_misses # 639.839 K/sec (30.24%)
461,131 l2_cache_hits_from_dc_misses # 553.690 K/sec (30.04%)
14,485 l2_cache_misses_from_dc_misses # 17.392 K/sec (29.55%)
L1キャッシュミスは8バイトから0%からPACKET_SIZE
32バイトから0.02%にわずかに増加しました。しかし、バックエンドの低迷が2.6%から38%に急増した理由を説明できますか?そうでない場合、CPUバックエンドが停止するもう1つの原因は何ですか?
memcpy
ストライドが大きいほど、ループがあるL1キャッシュラインから別のL1キャッシュラインに速く移動することを意味します。ただし、行がすでにキャッシュにあり、実際にL1欠落イベント(報告されているように)がないperf
場合他のキャッシュラインにアクセスするとバックエンドの中断が発生するのはなぜですか?CPUが命令を並列に発行する方法に関連していますか?たぶん同時に他のキャッシュラインにアクセスするコマンドを発行できませんか?
PACKET_SIZE
下のグラフは、最大1024バイトの操作を示しています。
データポイントの数字は、PACKET_SIZE
実行パラメータ、つまりアクセスパターンのストライドを表しますmemcpy
。 x軸は毎秒数百万のジョブ(Mops)で、1つの「ジョブ」= 1 memcpyです。 Y軸には、L1アクセス損失率、バックエンドとフロントエンドが停止した周期比率などのパフォーマンス指標が含まれています。
これらのすべての実行では、L2アクセスは事実上失われません。つまり、l2_cache_misses_from_dc_misses
指標は常に非常に低いです。完全性のためにアナンド・テクノロジーズZen 2アーキテクチャのL1レイテンシは4サイクル、L2レイテンシは12サイクルです。
フロントエンドがなぜ立ち往生しているのかよくわかりません。ところで、それがperf
報道されました。私はこれが本当だと信じています。これは、フロントエンドの一時停止とバックエンドの一時停止の効果が異なるためです。グラフの実行を256と1024と比較すると、PACKET_SIZE
L1ミスはほぼ同じです。 256はバックエンドでサイクルの約77%が停止し、1024はその逆です。サイクルの77%がフロントエンドで停止し、バックエンドでは0%が停止しました。しかしながら、1024はサイクル当たりずっと少ない数の命令を実行するので、はるかに遅い。これは1024ランで約0.42、256ランで1.28です。
したがって、CPUがフロントエンドで停止すると、バックエンドで停止したときよりも1サイクルあたり少ない命令が発行されます。これがフロントエンドとバックエンドが機能する方法のようです。つまり、バックエンドをさらに並列に実行できます。誰もがこの推測を確認または修正できれば幸いです。しかし、もっと重要な質問はフロントエンドが停滞する理由です。フロントエンドはコマンドをデコードする必要があります。PACKET_SIZE
256または1024に設定すると、アセンブリは実際には変更されません。では、フロントエンドがストライド256より1024で停止する原因は何ですか?
すべての実行における各MopsのIPCプロットPACKET_SIZE
:
8つの実行は、PACKET_SIZE
より多くのMopsのパスから少し外れます。つまり、他の値よりも速い傾向が見られます。これは、ガイドラインがより効率的であるためです。