Il prefetcher L2 HW è davvero utile?


10

Sono su Whiskey Lake i7-8565U e analizzo i contatori di perf e il tempo per copiare 512 KiB di dati (due volte più della dimensione della cache L2) e ho dovuto affrontare alcuni malintesi sul lavoro del prefetcher L2 HW.

Nel manuale Intel MSR Vol.4 c'è MSR 0x1A4il bit 0 di è per il controllo del prefetcher L2 HW (1 da disabilitare).


Considera il seguente benchmark:

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++;
    }
}

Considera 2 esecuzioni del compilato main.c

Io .

MSR:

$ sudo rdmsr -p 0 0x1A4
0

Run:

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

 Performance counter stats for './bin':

    10 486 164 071      L1-dcache-loads                                               (12,13%)
    10 461 354 384      L1-dcache-load-misses     #   99,76% of all L1-dcache hits    (12,05%)
    10 481 930 413      L1-dcache-stores                                              (12,05%)
    10 461 136 686      l1d.replacement                                               (12,12%)
    31 466 394 422      l1d_pend_miss.fb_full                                         (12,11%)
   211 853 643 294      l1d_pend_miss.pending                                         (12,09%)
     1 759 204 317      LLC-loads                                                     (12,16%)
            31 007      LLC-load-misses           #    0,00% of all LL-cache hits     (12,16%)
     3 154 901 630      LLC-stores                                                    (6,19%)
    15 867 315 545      l2_rqsts.all_pf                                               (9,22%)
                 0      sw_prefetch_access.t1_t2                                      (12,22%)
         1 393 306      l2_lines_out.useless_hwpf                                     (12,16%)
     3 549 170 919      l2_rqsts.pf_hit                                               (12,09%)
    12 356 247 643      l2_rqsts.pf_miss                                              (12,06%)
                 0      load_hit_pre.sw_pf                                            (12,09%)
     3 159 712 695      l2_rqsts.rfo_hit                                              (12,06%)
     1 207 642 335      l2_rqsts.rfo_miss                                             (12,02%)
     4 366 526 618      l2_rqsts.all_rfo                                              (12,06%)
     5 240 013 774      offcore_requests.all_data_rd                                     (12,06%)
    19 936 657 118      offcore_requests.all_requests                                     (12,09%)
     1 761 660 763      offcore_response.demand_data_rd.any_response                                     (12,12%)
       287 044 397      bus-cycles                                                    (12,15%)
    36 816 767 779      resource_stalls.any                                           (12,15%)
    36 553 997 653      resource_stalls.sb                                            (12,15%)
    38 035 066 210      uops_retired.stall_cycles                                     (12,12%)
    24 766 225 119      uops_executed.stall_cycles                                     (12,09%)
    40 478 455 041      uops_issued.stall_cycles                                      (12,05%)
    24 497 256 548      cycle_activity.stalls_l1d_miss                                     (12,02%)
    12 611 038 018      cycle_activity.stalls_l2_miss                                     (12,09%)
        10 228 869      cycle_activity.stalls_l3_miss                                     (12,12%)
    24 707 614 483      cycle_activity.stalls_mem_any                                     (12,22%)
    24 776 110 104      cycle_activity.stalls_total                                     (12,22%)
    48 914 478 241      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':

    10 508 027 832      L1-dcache-loads                                               (12,05%)
    10 463 643 206      L1-dcache-load-misses     #   99,58% of all L1-dcache hits    (12,09%)
    10 481 296 605      L1-dcache-stores                                              (12,12%)
    10 444 854 468      l1d.replacement                                               (12,15%)
    29 287 445 744      l1d_pend_miss.fb_full                                         (12,17%)
   205 569 630 707      l1d_pend_miss.pending                                         (12,17%)
     5 103 444 329      LLC-loads                                                     (12,17%)
            33 406      LLC-load-misses           #    0,00% of all LL-cache hits     (12,17%)
     9 567 917 742      LLC-stores                                                    (6,08%)
     1 157 237 980      l2_rqsts.all_pf                                               (9,12%)
                 0      sw_prefetch_access.t1_t2                                      (12,17%)
           301 471      l2_lines_out.useless_hwpf                                     (12,17%)
       218 528 985      l2_rqsts.pf_hit                                               (12,17%)
       938 735 722      l2_rqsts.pf_miss                                              (12,17%)
                 0      load_hit_pre.sw_pf                                            (12,17%)
         4 096 281      l2_rqsts.rfo_hit                                              (12,17%)
     4 972 640 931      l2_rqsts.rfo_miss                                             (12,17%)
     4 976 006 805      l2_rqsts.all_rfo                                              (12,17%)
     5 175 544 191      offcore_requests.all_data_rd                                     (12,17%)
    15 772 124 082      offcore_requests.all_requests                                     (12,17%)
     5 120 635 892      offcore_response.demand_data_rd.any_response                                     (12,17%)
       292 980 395      bus-cycles                                                    (12,17%)
    37 592 020 151      resource_stalls.any                                           (12,14%)
    37 317 091 982      resource_stalls.sb                                            (12,11%)
    38 121 826 730      uops_retired.stall_cycles                                     (12,08%)
    25 430 699 605      uops_executed.stall_cycles                                     (12,04%)
    41 416 190 037      uops_issued.stall_cycles                                      (12,04%)
    25 326 579 070      cycle_activity.stalls_l1d_miss                                     (12,04%)
    25 019 148 253      cycle_activity.stalls_l2_miss                                     (12,03%)
         7 384 770      cycle_activity.stalls_l3_miss                                     (12,03%)
    25 442 709 033      cycle_activity.stalls_mem_any                                     (12,03%)
    25 406 897 956      cycle_activity.stalls_total                                     (12,03%)
    49 877 044 086      cycles                                                        (12,03%)

      12,231406658 seconds time elapsed

      12,226386000 seconds user
       0,004000000 seconds sys

Ho notato il contatore:

12 611 038 018 cycle_activity.stalls_l2_miss v / s
25 019 148 253 cycle_activity.stalls_l2_miss

suggerendo che il MSR disabilita il prefetcher L2 HW viene applicato. Anche altre cose relative a l2 / LLC differiscono in modo significativo. La differenza è riproducibile su diverse esecuzioni . Il problema è che non ci sono quasi differenze total timee cicli:

48 914 478 241 cycles v / s
49 877 044 086 cycles

12,155774555 seconds time elapsed v / s
12,231406658 seconds time elapsed

DOMANDA: Gli
errori L2 sono nascosti da altri limitatori di prestazioni?
In tal caso, puoi suggerire quali contatori guardare per capirlo?


4
Come regola generale: qualsiasi copia di memoria implementata in modo non abissale è associata alla memoria. Anche quando colpisce solo la cache L1. I costi generali di qualsiasi accesso alla memoria sono semplicemente molto più elevati di quelli necessari a una CPU per aggiungere due e due insieme. Nel tuo caso, stai persino utilizzando le istruzioni AVX per ridurre la quantità di istruzioni per byte copiato. Ovunque vengano trovati i tuoi dati (L1, L2, LLC, memoria), il throughput del componente di memoria associato sarà il tuo collo di bottiglia.
— cmaster - ripristina monica il

Risposte:


5

Sì, lo streamer L2 è davvero utile per la maggior parte del tempo.

memcpy non ha alcuna latenza computazionale da nascondere, quindi immagino che possa permettersi di lasciare risorse OoO exec (dimensione ROB) per gestire la latenza di carico aggiuntiva che si ottiene da più missioni L2, almeno in questo caso da dove si ottengono tutti i successi L3 utilizzando un set di lavoro di medie dimensioni (1 MiB) che si adatta a L3, non è necessario il prefetch per far sì che si verifichino hit L3.

E le uniche istruzioni sono load / store (e loop overhead), quindi la finestra di OoO include carichi di domanda per molto più avanti.

IDK se il prefetcher spaziale L2 e il prefetcher L1d stanno aiutando qualcuno qui.


Pronostico per testare questa ipotesi : ingrandisci il tuo array in modo da ottenere mancati L3 e probabilmente vedrai una differenza nel tempo complessivo una volta che il exec OoO non è abbastanza per nascondere la latenza di carico di andare fino alla DRAM. Il prefetch HW che si avvia più avanti può aiutare alcuni.

Gli altri grandi vantaggi del prefetching HW arrivano quando può tenere il passo con il tuo calcolo, in modo da ottenere hit L2. (In un ciclo che ha il calcolo con una catena di dipendenza di media lunghezza ma non portata dal ciclo.)

I carichi di domanda e i dirigenti OoO possono fare molto per quanto riguarda l'utilizzo della larghezza di banda di memoria disponibile (a thread singolo), quando non c'è altra pressione sulla capacità di ROB.


Si noti inoltre che sulle CPU Intel, ogni mancanza di cache può costare un replay back-end (dall'RS / scheduler) di uops dipendenti , uno ciascuno per L1d e L2 mancano quando si prevede che i dati arrivino. Dopodiché, a quanto pare, il core esegue lo spam in modo ottimistico mentre attende che i dati arrivino da L3.

(Vedi https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th e le operazioni di carico sono allocate dal RS quando spediscono, completano o qualche altra volta? )

Non il caricamento cache-miss stesso; in questo caso sarebbero le istruzioni del negozio. Più specificamente, i dati dello store acquistano per la porta 4. Non importa qui; l'utilizzo di archivi a 32 byte e il collo di bottiglia sulla larghezza di banda L3 significa che non siamo vicini a 1 porta 4 in punto per clock.


2
@Antario: eh? Non ha senso; sei legato alla memoria, quindi non hai un collo di bottiglia sul front-end, quindi l'LSD è irrilevante. (Evita di recuperarli dalla cache uop, risparmiando un po 'di energia). Occupano ancora spazio nel ROB fino a quando non possono ritirarsi. Non sono così significativi, ma non trascurabili.
— Peter Cordes,

2
ingrandisci il tuo array in modo da ottenere miss L3 e probabilmente vedrai la differenza che ho eseguito una serie di test con 16MiBbuffer e 10iterazioni e in effetti ho ottenuto 14,186868883 secondsvs 43,731360909 secondse 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 .
— Sant'Antario,

4
@Antario: per rinominare il registro! Questo è uno dei pezzi più importanti di OoO exec, specialmente su un ISA povero di registro come x86. Vedi Perché mulss prende solo 3 cicli su Haswell, diverso dalle tabelle di istruzioni di Agner? (Svolgimento di circuiti FP con accumulatori multipli) . E a proposito, normalmente vorresti fare 2 carichi quindi 2 negozi, non caricare / memorizzare carico / negozio. Maggiori possibilità di evitare o mitigare le bancarelle di aliasing 4K perché i carichi successivi (che l'HW deve rilevare come sovrapposti o precedenti nei negozi precedenti) sono più lontani.
— Peter Cordes,

2
@Antario: sì, certo. La guida all'ottimizzazione di Agner Fog spiega anche i dirigenti di OoO con la ridenominazione dei registri, così come Wikipedia. A proposito, la ridenominazione del registro evita anche i rischi di WAW, lasciando solo vere dipendenze (RAW). In questo modo i carichi possono anche essere completati fuori servizio, senza attendere che un carico precedente finisca di scrivere lo stesso registro architettonico. E sì, l'unica catena dep trasportata in loop è attraverso RCX, quindi quella catena può andare avanti. Ecco perché gli indirizzi possono essere pronti in anticipo, mentre i carichi di carico / archivio sono ancora strozzati sul throughput della porta 2/3.
— Peter Cordes,

3
Sono sorpreso che il prefetching non abbia aiutato il memcpy in L3. Immagino che gli LFB 10/12 siano "sufficienti" in quel caso. Sembra strano però: qual è il fattore limitante lì? Il tempo core -> L2 dovrebbe essere inferiore al tempo L2 -> L3, quindi nel mio modello mentale avere più buffer (più occupazione totale) per la seconda tappa dovrebbe aiutare.
— BeeOnRope

3

Sì, il prefetcher L2 HW è molto utile!

Ad esempio, trova i risultati seguenti sulla mia macchina (i7-6700HQ) con Tinymembench in esecuzione . La prima colonna dei risultati è con tutti i prefetcher attivi, la seconda colonna dei risultati è con lo streamer L2 disattivato (ma tutti gli altri prefetcher sono ancora attivi).

Questo test utilizza 32 buffer MiB di origine e destinazione, che sono molto più grandi di L3 sul mio computer, quindi testerà principalmente i mancati 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

In questi test avere lo streamer L2 non è mai più lento ed è spesso quasi il doppio più veloce.

In generale, potresti notare i seguenti modelli nei risultati:

  • Le copie in genere sembrano essere più interessate dei riempimenti.
  • Il standard memsete STOSB fill(questi si riducono alla stessa cosa su questa piattaforma) sono i meno colpiti, con il risultato prefetched che è solo un po 'più veloce di senza.
  • Lo standard memcpyè probabilmente l'unica copia qui che utilizza le istruzioni AVX a 32 byte ed è tra le meno colpite delle copie, ma il prefetch su è ancora ~ 40% più veloce che senza.

Ho anche provato ad accendere e spegnere gli altri tre prefetcher, ma generalmente non hanno avuto quasi alcun effetto misurabile per questo benchmark.


(Fatto vmovdqacurioso : AVX1 è nonostante sia "intero".) Pensi che il loop dell'OP stia dando una larghezza di banda inferiore rispetto a glibc memcpy? Ed è per questo che 12 LFB sono stati sufficienti per tenere il passo con i carichi della domanda che andavano a L3, senza sfruttare l'MLP extra dal supercoda L2 <-> L3 che lo streamer L2 può tenere occupato? Questa è presumibilmente la differenza nel tuo test. L3 dovrebbe funzionare alla stessa velocità del core; entrambi avete microarchitetture quad-core equivalenti a client Skylake, quindi probabilmente latenza L3 simile?
— Peter Cordes,

@PeterCordes - scusa, probabilmente avrei dovuto essere chiaro: questo test era tra 32 buffer MiB, quindi sta testando hit DRAM e non L3. Ho pensato che tmb emettesse le dimensioni del buffer, ma vedo che non lo è - oops! Era intenzionale: non stavo cercando di spiegare esattamente lo scenario da 512 KiB dell'OP, ma ho solo risposto alla domanda principale se lo streamer L2 è utile con uno scenario che lo dimostra. Immagino di aver usato una dimensione del buffer più piccola per poter più o meno riprodurre i risultati (ho già visto un risultato simile uarch-benchmenzionato nei commenti).
— BeeOnRope,

1
Ho aggiunto la dimensione del buffer alla risposta.
— BeeOnRope,

1
@Antario: No, non è un problema. Non ho idea del perché pensi che potrebbe essere un problema; non è che ci sia alcuna penalità per la miscelazione delle istruzioni AVX1 e AVX2. Il punto del mio commento è stato che questo loop richiede solo AVX1, tuttavia questa risposta menziona l'utilizzo delle istruzioni AVX2. Intel è riuscita ad ampliare il percorso di caricamento / archiviazione dei dati L1d a 32 byte contemporaneamente all'introduzione di AVX2, quindi potresti utilizzare la disponibilità di AVX2 come parte di come selezioni un'implementazione memcpy se stai eseguendo l'invio di runtime ...
— Peter Cordes

1
Come hai spento il prefetcher e quale? Era software.intel.com/en-us/articles/… ? Il forum software.intel.com/en-us/forums/intel-isa-extensions/topic/… dice che alcuni bit hanno un significato diverso.
— Osgx,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.