Czy moduł wstępny L2 HW jest naprawdę pomocny?

10

Jestem na Whisky Lake i7-8565U i analizuję liczniki perf oraz czas na skopiowanie 512 KiB danych (dwa razy więcej niż rozmiar pamięci podręcznej L2) i napotkałem pewne nieporozumienia dotyczące pracy modułu pobierania wstępnego L2 HW.

W Intel Manual Vol.4 MSR jest MSR, 0x1A4którego bit 0 służy do kontrolowania modułu wstępnego pobierania L2 HW (1, aby wyłączyć).


Rozważ następujący punkt odniesienia:

memcopy.h:

void *avx_memcpy_forward_lsls(void *restrict, const void *restrict, size_t);

memcopy.S:

avx_memcpy_forward_lsls:
    shr rdx, 0x3
    xor rcx, rcx
avx_memcpy_forward_loop_lsls:
    vmovdqa ymm0, [rsi + 8*rcx]
    vmovdqa [rdi + rcx*8], ymm0
    vmovdqa ymm1, [rsi + 8*rcx + 0x20]
    vmovdqa [rdi + rcx*8 + 0x20], ymm1
    add rcx, 0x08
    cmp rdx, rcx
    ja avx_memcpy_forward_loop_lsls
    ret

main.c:

#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <x86intrin.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include "memcopy.h"

#define ITERATIONS 1000
#define BUF_SIZE 512 * 1024

_Alignas(64) char src[BUF_SIZE];
_Alignas(64) char dest[BUF_SIZE];

static void __run_benchmark(unsigned runs, unsigned run_iterations,
                    void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz);

#define run_benchmark(runs, run_iterations, fn, dest, src, sz) \
    do{\
        printf("Benchmarking " #fn "\n");\
        __run_benchmark(runs, run_iterations, fn, dest, src, sz);\
    }while(0)

int main(void){
    int fd = open("/dev/urandom", O_RDONLY);
    read(fd, src, sizeof src);
    run_benchmark(20, ITERATIONS, avx_memcpy_forward_lsls, dest, src, BUF_SIZE);
}

static inline void benchmark_copy_function(unsigned iterations, void *(*fn)(void *, const void *, size_t),
                                               void *restrict dest, const void *restrict src, size_t sz){
    while(iterations --> 0){
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
        fn(dest, src, sz);
    }
}

static void __run_benchmark(unsigned runs, unsigned run_iterations,
                    void *(*fn)(void *, const void*, size_t), void *dest, const void* src, size_t sz){
    unsigned current_run = 1;
    while(current_run <= runs){
        benchmark_copy_function(run_iterations, fn, dest, src, sz);
        printf("Run %d finished\n", current_run);
        current_run++;
    }
}

Rozważ 2 serie skompilowanych main.c

Ja .

MSR:

$ sudo rdmsr -p 0 0x1A4
0

Run:

$ taskset -c 0 sudo ../profile.sh ./bin 

 Performance counter stats for './bin':

    10486164071      L1-dcache-loads                                               (12,13%)
    10461354384      L1-dcache-load-misses     #   99,76% of all L1-dcache hits    (12,05%)
    10481930413      L1-dcache-stores                                              (12,05%)
    10461136686      l1d.replacement                                               (12,12%)
    31466394422      l1d_pend_miss.fb_full                                         (12,11%)
   211853643294      l1d_pend_miss.pending                                         (12,09%)
     1759204317      LLC-loads                                                     (12,16%)
            31007      LLC-load-misses           #    0,00% of all LL-cache hits     (12,16%)
     3154901630      LLC-stores                                                    (6,19%)
    15867315545      l2_rqsts.all_pf                                               (9,22%)
                 0      sw_prefetch_access.t1_t2                                      (12,22%)
         1393306      l2_lines_out.useless_hwpf                                     (12,16%)
     3549170919      l2_rqsts.pf_hit                                               (12,09%)
    12356247643      l2_rqsts.pf_miss                                              (12,06%)
                 0      load_hit_pre.sw_pf                                            (12,09%)
     3159712695      l2_rqsts.rfo_hit                                              (12,06%)
     1207642335      l2_rqsts.rfo_miss                                             (12,02%)
     4366526618      l2_rqsts.all_rfo                                              (12,06%)
     5240013774      offcore_requests.all_data_rd                                     (12,06%)
    19936657118      offcore_requests.all_requests                                     (12,09%)
     1761660763      offcore_response.demand_data_rd.any_response                                     (12,12%)
       287044397      bus-cycles                                                    (12,15%)
    36816767779      resource_stalls.any                                           (12,15%)
    36553997653      resource_stalls.sb                                            (12,15%)
    38035066210      uops_retired.stall_cycles                                     (12,12%)
    24766225119      uops_executed.stall_cycles                                     (12,09%)
    40478455041      uops_issued.stall_cycles                                      (12,05%)
    24497256548      cycle_activity.stalls_l1d_miss                                     (12,02%)
    12611038018      cycle_activity.stalls_l2_miss                                     (12,09%)
        10228869      cycle_activity.stalls_l3_miss                                     (12,12%)
    24707614483      cycle_activity.stalls_mem_any                                     (12,22%)
    24776110104      cycle_activity.stalls_total                                     (12,22%)
    48914478241      cycles                                                        (12,19%)

      12,155774555 seconds time elapsed

      11,984577000 seconds user
       0,015984000 seconds sys

II.

MSR:

$ sudo rdmsr -p 0 0x1A4
1

Run:

$ taskset -c 0 sudo ../profile.sh ./bin

 Performance counter stats for './bin':

    10508027832      L1-dcache-loads                                               (12,05%)
    10463643206      L1-dcache-load-misses     #   99,58% of all L1-dcache hits    (12,09%)
    10481296605      L1-dcache-stores                                              (12,12%)
    10444854468      l1d.replacement                                               (12,15%)
    29287445744      l1d_pend_miss.fb_full                                         (12,17%)
   205569630707      l1d_pend_miss.pending                                         (12,17%)
     5103444329      LLC-loads                                                     (12,17%)
            33406      LLC-load-misses           #    0,00% of all LL-cache hits     (12,17%)
     9567917742      LLC-stores                                                    (6,08%)
     1157237980      l2_rqsts.all_pf                                               (9,12%)
                 0      sw_prefetch_access.t1_t2                                      (12,17%)
           301471      l2_lines_out.useless_hwpf                                     (12,17%)
       218528985      l2_rqsts.pf_hit                                               (12,17%)
       938735722      l2_rqsts.pf_miss                                              (12,17%)
                 0      load_hit_pre.sw_pf                                            (12,17%)
         4096281      l2_rqsts.rfo_hit                                              (12,17%)
     4972640931      l2_rqsts.rfo_miss                                             (12,17%)
     4976006805      l2_rqsts.all_rfo                                              (12,17%)
     5175544191      offcore_requests.all_data_rd                                     (12,17%)
    15772124082      offcore_requests.all_requests                                     (12,17%)
     5120635892      offcore_response.demand_data_rd.any_response                                     (12,17%)
       292980395      bus-cycles                                                    (12,17%)
    37592020151      resource_stalls.any                                           (12,14%)
    37317091982      resource_stalls.sb                                            (12,11%)
    38121826730      uops_retired.stall_cycles                                     (12,08%)
    25430699605      uops_executed.stall_cycles                                     (12,04%)
    41416190037      uops_issued.stall_cycles                                      (12,04%)
    25326579070      cycle_activity.stalls_l1d_miss                                     (12,04%)
    25019148253      cycle_activity.stalls_l2_miss                                     (12,03%)
         7384770      cycle_activity.stalls_l3_miss                                     (12,03%)
    25442709033      cycle_activity.stalls_mem_any                                     (12,03%)
    25406897956      cycle_activity.stalls_total                                     (12,03%)
    49877044086      cycles                                                        (12,03%)

      12,231406658 seconds time elapsed

      12,226386000 seconds user
       0,004000000 seconds sys

Zauważyłem licznik:

12 611 038 018 cycle_activity.stalls_l2_miss vs
25 019 148 253 cycle_activity.stalls_l2_miss

sugerując, że zastosowano MSR wyłączający moduł wstępnego pobierania L2 HW. Również inne rzeczy związane z l2 / LLC różnią się znacznie. Różnica jest odtwarzalna dla różnych przebiegów . Problem polega na tym, że nie ma prawie żadnej różnicy total timei cykli:

48 914 478 241 cycles vs
49 877 044 086 cycles

12,155774555 seconds time elapsed vs
12,231406658 seconds time elapsed

PYTANIE:
Czy brak L2 jest ukryty przez inne ograniczniki wydajności?
Jeśli tak, czy możesz zasugerować, na jakie liczniki spojrzeć, aby to zrozumieć?

St.Antario
źródło
4
Zasadą jest, że każda nieumiejętnie zaimplementowana kopia pamięci jest związana z pamięcią. Nawet jeśli trafi tylko do pamięci podręcznej L1. Koszty ogólne związane z dostępem do pamięci są po prostu o wiele wyższe niż procesor, aby dodać dwa do dwóch. W twoim przypadku używasz nawet instrukcji AVX, aby zmniejszyć liczbę instrukcji na skopiowany bajt. Gdziekolwiek zostaną znalezione twoje dane (L1, L2, LLC, pamięć), przepustowość powiązanego komponentu pamięci będzie wąskim gardłem.
cmaster

Odpowiedzi:

5

Tak, streamer L2 jest bardzo pomocny przez większość czasu.

memcpy nie ma żadnych ukrytych opóźnień obliczeniowych, więc sądzę, że stać go na to, aby pozwolić zasobom wykonawczym OoO (rozmiar ROB) obsłużyć dodatkowe opóźnienie obciążenia, które otrzymujesz z większej liczby braków L2, przynajmniej w tym przypadku, gdy otrzymujesz wszystkie trafienia L3 z przy użyciu średniej wielkości zestawu roboczego (1MiB), który pasuje do L3, nie jest wymagane wstępne pobieranie, aby nastąpiło trafienie w L3.

A jedynymi instrukcjami są ładowanie / przechowywanie (i narzut pętli), więc okno OoO zawiera obciążenia popytu na całkiem daleką przyszłość.

IDK, jeśli prefiks przestrzenny L2 i preetcher L1d tutaj pomagają.


Prognozy do przetestowania tej hipotezy : powiększ swoją tablicę, aby uzyskać brakujące L3 i prawdopodobnie zobaczysz różnicę w całkowitym czasie, gdy OoO exec nie wystarczy, aby ukryć opóźnienie ładowania aż do DRAM. Wywołanie poboru CWU uruchamiane dalej może pomóc niektórym.

Inne duże zalety pobierania wstępnego HW wynika z tego, że może nadążyć za obliczeniami, dzięki czemu otrzymujesz trafienia L2. (W pętli, która ma obliczenia z łańcuchem zależności średniej długości, ale nie z pętlą.)

Obciążenia na żądanie i OoO exec mogą wiele zrobić, jeśli chodzi o wykorzystanie dostępnej przepustowości pamięci (jednowątkowej), gdy nie ma innego nacisku na pojemność ROB.


Należy również pamiętać, że w przypadku procesorów Intel każda brakująca pamięć podręczna może kosztować powtórne odtwarzanie (z RS / harmonogramu) zależnych upops , po jednym dla L1d i L2 nieudanych, gdy oczekuje się, że dane dotrą. A potem, najwyraźniej rdzeń optymistycznie wysyła spam podczas oczekiwania na dane z L3.

(Zobacz https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th and Are load ops dealocated from the RS, gdy wysyłają, kompletują lub w innym terminie? )

Nie samo ładowanie pamięci podręcznej; w tym przypadku byłyby to instrukcje sklepu. Mówiąc dokładniej, kupa danych sklepu dla portu 4. To nie ma znaczenia tutaj; korzystanie z 32-bajtowych sklepów i wąskie gardło w przepustowości L3 oznacza, że ​​nie jesteśmy blisko 1 portu 4 UOP na zegar.

Peter Cordes
źródło
2
@ St.Antario: huh? To nie ma sensu; jesteś związany pamięcią, więc nie masz wąskiego gardła, więc LSD nie ma znaczenia. (Pozwala to uniknąć ponownego pobierania ich z pamięci podręcznej uop, oszczędzając trochę energii). Nadal zajmują miejsce w ROB, dopóki nie przejdą na emeryturę. Nie są tak znaczące, ale też nie są nieistotne.
Peter Cordes
2
powiększ swoją tablicę, aby uzyskać brakujące L3 i prawdopodobnie zobaczysz różnicę. Przeprowadziłem wiele testów z 16MiBbuforem i 10iteracjami i rzeczywiście dostałem 14,186868883 secondsvs 43,731360909 secondsi 46,76% of all LL-cache hitsvs 99,32% of all LL-cache hits; 1 028 664 372 LLC-loadsvs 1 587 454 298 LLC-loads .
St.Antario
4
@ St.Antario: zmieniając nazwę rejestru! Jest to jeden z najbardziej kluczowych elementów OoO exec, szczególnie na ubogim rejestrze ISA, takim jak x86. Zobacz, dlaczego Mulsy mają tylko 3 cykle na Haswell, w odróżnieniu od tabel instrukcji Agnera? (Rozwijanie pętli FP z wieloma akumulatorami) . I BTW, zwykle chcesz zrobić 2 ładunki, a następnie 2 sklepy, a nie ładować / przechowywać ładować / przechowywać. Większa szansa na uniknięcie lub złagodzenie przeciągnięć aliasingu 4k, ponieważ późniejsze obciążenia (które HW musi wykryć jako nakładające się na poprzednie sklepy lub nie) są dalej.
Peter Cordes
2
@ St.Antario: tak, oczywiście. Przewodnik po optymalizacji Agner Fog wyjaśnia również OoO exec przy zmianie nazw rejestrów, podobnie jak wikipedia. BTW, zmiana nazwy rejestru pozwala również uniknąć zagrożeń związanych z WAW, pozostawiając jedynie prawdziwe zależności (RAW). Tak więc ładunki mogą nawet zakończyć się nieczynne, bez czekania, aż poprzednie ładowanie zakończy zapisywanie tego samego rejestru architektonicznego. I tak, jedynym łańcuchem depresyjnym prowadzonym przez pętlę jest RCX, dzięki czemu łańcuch może biec przed siebie. Dlatego adresy mogą być gotowe wcześnie, podczas gdy ładowanie / przechowywanie nie jest jeszcze ograniczone w przepustowości portu 2/3.
Peter Cordes
3
Dziwi mnie, że pobieranie wstępne nie pomogło memcpy w L3. Myślę, że w takim przypadku LFB 10/12 są „wystarczające”. Wydaje się to dziwne: jaki jest tam czynnik ograniczający? Rdzeń -> czas L2 powinien być krótszy niż czas L2 -> L3, więc w moim modelu mentalnym posiadanie większej ilości buforów (więcej całkowitego zajętości) na drugą nogę powinno pomóc.
BeeOnRope
3

Tak, moduł wstępny L2 HW jest bardzo pomocny!

Na przykład znajdź poniżej wyniki na moim komputerze (i7-6700HQ) z uruchomionym programem tinymembench . Pierwsza kolumna wyników jest z włączonymi wszystkimi modułami pobierania wstępnego, druga kolumna wyników jest z wyłączonym streamerem L2 (ale wszystkie pozostałe moduły pobierania są nadal włączone).

Ten test wykorzystuje 32 źródłowe i docelowe bufory MiB, które są znacznie większe niż L3 na moim komputerze, więc będzie testować głównie brakujące dane do DRAM.

==========================================================================
== Memory bandwidth tests                                               ==
==                                                                      ==
== Note 1: 1MB = 1000000 bytes                                          ==
== Note 2: Results for 'copy' tests show how many bytes can be          ==
==         copied per second (adding together read and writen           ==
==         bytes would have provided twice higher numbers)              ==
== Note 3: 2-pass copy means that we are using a small temporary buffer ==
==         to first fetch data into it, and only then write it to the   ==
==         destination (source -> L1 cache, L1 cache -> destination)    ==
== Note 4: If sample standard deviation exceeds 0.1%, it is shown in    ==
==         brackets                                                     ==
==========================================================================

                                                       L2 streamer ON            OFF
 C copy backwards                                     :   7962.4 MB/s    4430.5 MB/s
 C copy backwards (32 byte blocks)                    :   7993.5 MB/s    4467.0 MB/s
 C copy backwards (64 byte blocks)                    :   7989.9 MB/s    4438.0 MB/s
 C copy                                               :   8503.1 MB/s    4466.6 MB/s
 C copy prefetched (32 bytes step)                    :   8729.2 MB/s    4958.4 MB/s
 C copy prefetched (64 bytes step)                    :   8730.7 MB/s    4958.4 MB/s
 C 2-pass copy                                        :   6171.2 MB/s    3368.7 MB/s
 C 2-pass copy prefetched (32 bytes step)             :   6193.1 MB/s    4104.2 MB/s
 C 2-pass copy prefetched (64 bytes step)             :   6198.8 MB/s    4101.6 MB/s
 C fill                                               :  13372.4 MB/s   10610.5 MB/s
 C fill (shuffle within 16 byte blocks)               :  13379.4 MB/s   10547.5 MB/s
 C fill (shuffle within 32 byte blocks)               :  13365.8 MB/s   10636.9 MB/s
 C fill (shuffle within 64 byte blocks)               :  13588.7 MB/s   10588.3 MB/s
 -
 standard memcpy                                      :  11550.7 MB/s    8216.3 MB/s
 standard memset                                      :  23188.7 MB/s   22686.8 MB/s
 -
 MOVSB copy                                           :   9458.4 MB/s    6523.7 MB/s
 MOVSD copy                                           :   9474.5 MB/s    6510.7 MB/s
 STOSB fill                                           :  23329.0 MB/s   22901.5 MB/s
 SSE2 copy                                            :   9073.1 MB/s    4970.3 MB/s
 SSE2 nontemporal copy                                :  12647.1 MB/s    7492.5 MB/s
 SSE2 copy prefetched (32 bytes step)                 :   9106.0 MB/s    5069.8 MB/s
 SSE2 copy prefetched (64 bytes step)                 :   9113.5 MB/s    5063.1 MB/s
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11770.8 MB/s    7453.4 MB/s
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11937.1 MB/s    7712.1 MB/s
 SSE2 2-pass copy                                     :   7092.8 MB/s    4355.2 MB/s
 SSE2 2-pass copy prefetched (32 bytes step)          :   7001.4 MB/s    4585.1 MB/s
 SSE2 2-pass copy prefetched (64 bytes step)          :   7055.1 MB/s    4557.9 MB/s
 SSE2 2-pass nontemporal copy                         :   5043.2 MB/s    3263.3 MB/s
 SSE2 fill                                            :  14087.3 MB/s   10947.1 MB/s
 SSE2 nontemporal fill                                :  33134.5 MB/s   32774.3 MB/s

W tych testach posiadanie streamera L2 nigdy nie jest wolniejsze i często prawie dwa razy szybsze.

Zasadniczo w wynikach można zauważyć następujące wzorce:

  • Wydaje się, że na kopie ma to większy wpływ niż wypełnienia.
  • Te standard memseti STOSB fill(sprowadzają się do tej samej rzeczy na tej platformie) są najmniej dotknięte, a wstępnie wybrany wynik jest tylko o kilka% szybszy niż bez.
  • Standard memcpyjest prawdopodobnie jedyną kopią, która używa 32-bajtowych instrukcji AVX, i jest jedną z najmniej dotkniętych kopii - ale pobieranie wstępne jest nadal ~ 40% szybsze niż bez.

Próbowałem także włączać i wyłączać pozostałe trzy moduły pobierania wstępnego, ale generalnie nie miały one prawie żadnego wymiernego wpływu na ten test porównawczy.

BeeOnRope
źródło
(Ciekawostka: vmovdqaczy AVX1 jest „liczbą całkowitą”). Czy uważasz, że pętla OP zapewniała mniejszą przepustowość niż memcpy glibc? I dlatego 12 LFB wystarczyło, aby nadążyć za obciążeniem popytu idącym do L3, bez korzystania z dodatkowej MLP z kolejki L2 <-> L3, którą streamer L2 może utrzymać? To prawdopodobnie różnica w twoim teście. L3 powinien pracować z tą samą prędkością co rdzeń; oboje macie czterordzeniowe odpowiedniki mikroarchitektury klienta Skylake, więc prawdopodobnie podobne opóźnienie L3?
Peter Cordes
@PeterCordes - przepraszam, pewnie powinienem był to wyjaśnić: ten test był między 32 buforami MiB, więc testuje trafienia DRAM, a nie trafienia L3. Myślałem, że tmb wyprowadza rozmiar bufora, ale widzę, że nie robi - ups! To było zamierzone: nie próbowałem dokładnie wyjaśnić scenariusza 512 KiB OP, ale po prostu odpowiedzą na główne pytanie, czy streamer L2 jest przydatny w scenariuszu, który to pokazuje. Wydaje mi się, że użyłem mniejszego rozmiaru bufora, mogłem mniej więcej odtworzyć wyniki (już widziałem podobny wynik uarch-benchwspomniany w komentarzach).
BeeOnRope
1
Do odpowiedzi dodałem rozmiar bufora.
BeeOnRope
1
@ St.Antario: Nie, to nie jest problem. Nie mam pojęcia, dlaczego uważasz, że może to stanowić problem; to nie jest jakaś kara za miksowanie instrukcji AVX1 i AVX2. Chodzi mi o to, że ta pętla wymaga tylko AVX1, ale ta odpowiedź wspomina przy użyciu instrukcji AVX2. Intel rozszerzył ścieżki ładowania / przechowywania L1d do 32 bajtów jednocześnie z wprowadzeniem AVX2, więc możesz użyć dostępności AVX2 jako części wyboru implementacji memcpy, jeśli robisz wysyłanie w czasie wykonywania ...
Peter Cordes
1
Jak wyłączyłeś moduł pobierania wstępnego i który? Czy to było software.intel.com/en-us/articles/… ? Forum software.intel.com/en-us/forums/intel-isa-extensions/topic/… mówi, że niektóre bity mają inne znaczenie.
osgx