8-16-32バイトステップの単純なmemcpyループ、L1キャッシュ不足、バックエンドサイクルが中断される原因は何ですか?

8-16-32バイトステップの単純なmemcpyループ、L1キャッシュ不足、バックエンドサイクルが中断される原因は何ですか?

シングルプロデューサシングルコンシューマキューアルゴリズムで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_SIZE8、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_SIZE32バイトから0.02%にわずかに増加しました。しかし、バックエンドの低迷が2.6%から38%に急増した理由を説明できますか?そうでない場合、CPUバックエンドが停止するもう1つの原因は何ですか?

memcpyストライドが大きいほど、ループがあるL1キャッシュラインから別のL1キャッシュラインに速く移動することを意味します。ただし、行がすでにキャッシュにあり、実際にL1欠落イベント(報告されているように)がないperf場合他のキャッシュラインにアクセスするとバックエンドの中断が発生するのはなぜですか?CPUが命令を並列に発行する方法に関連していますか?たぶん同時に他のキャッシュラインにアクセスするコマンドを発行できませんか?

PACKET_SIZE下のグラフは、最大1024バイトの操作を示しています。

さまざまなPACKET_SIZE値を使用したmemcpyテストプログラムの統計:Mops VS L1キャッシュ不足率、CPUバックエンド、およびフロントエンドサイクル停止。

データポイントの数字は、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_SIZEL1ミスはほぼ同じです。 256はバックエンドでサイクルの約77%が停止し、1024はその逆です。サイクルの77%がフロントエンドで停止し、バックエンドでは0%が停止しました。しかしながら、1024はサイクル当たりずっと少ない数の命令を実行するので、はるかに遅い。これは1024ランで約0.42、256ランで1.28です。

したがって、CPUがフロントエンドで停止すると、バックエンドで停止したときよりも1サイクルあたり少ない命令が発行されます。これがフロントエンドとバックエンドが機能する方法のようです。つまり、バックエンドをさらに並列に実行できます。誰もがこの推測を確認または修正できれば幸いです。しかし、もっと重要な質問はフロントエンドが停滞する理由です。フロントエンドはコマンドをデコードする必要があります。PACKET_SIZE256または1024に設定すると、アセンブリは実際には変更されません。では、フロントエンドがストライド256より1024で停止する原因は何ですか?

すべての実行における各MopsのIPCプロットPACKET_SIZE:

すべての PACKET_SIZE 値の Mops ごとの IPC

8つの実行は、PACKET_SIZEより多くのMopsのパスから少し外れます。つまり、他の値よりも速い傾向が見られます。これは、ガイドラインがより効率的であるためです。

関連情報