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 백엔드가 정지되는 또 다른 원인은 무엇입니까?

memcpy스트라이드가 클수록 루프가 한 L1 캐시 라인에서 다른 L1 캐시 라인으로 더 빠르게 이동한다는 것을 의미합니다 . 그러나 행이 이미 캐시에 있고 실제로 L1 누락 이벤트(보고된 대로)가 없는 perf경우다른 캐시 라인에 액세스하면 백엔드 중단이 발생하는 이유는 무엇입니까?CPU가 명령을 병렬로 발행하는 방식과 관련이 있습니까? 어쩌면 동시에 다른 캐시 라인에 액세스하는 명령을 발행할 수 없습니까?

PACKET_SIZE아래 차트는 최대 1024바이트의 작업을 보여줍니다.

다양한 PACKET_SIZE 값을 사용한 memcpy 테스트 프로그램에 대한 통계: Mops VS L1 캐시 누락 비율, CPU 백엔드 및 프런트엔드 주기 정지.

데이터 포인트의 숫자는 PACKET_SIZE실행 매개변수, 즉 액세스 패턴의 보폭을 나타냅니다 memcpy. x축은 초당 수백만 개의 작업(Mops)이며, 하나의 "작업" = 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가 프런트엔드에서 정지되면 백엔드에서 정지될 때보다 주기당 더 적은 명령을 발행합니다. 이것이 프런트엔드와 백엔드가 작동하는 방식인 것 같습니다. 즉, 백엔드가 더 많이 병렬로 실행될 수 있습니다. 누구든지 이 추측을 확인하거나 수정할 수 있다면 감사하겠습니다. 그러나 더 중요한 질문은 프론트엔드가 정체되는 이유입니다. 프런트 엔드는 명령을 디코딩해야 합니다. PACKET_SIZE256 또는 1024로 설정하면 어셈블리가 실제로 변경되지 않습니다.그렇다면 프런트 엔드가 스트라이드 256보다 1024에서 더 많이 멈추는 원인은 무엇일까요?

모든 실행에서 각 Mops에 대한 IPC 플롯 PACKET_SIZE:

모든 PACKET_SIZE 값에 대한 Mops당 IPC

8개의 실행은 PACKET_SIZE더 많은 Mops의 경로에서 약간 벗어납니다. 즉, 다른 값보다 더 빠른 추세를 보입니다. 이는 지침이 더 효율적이기 때문일 것입니다.

관련 정보