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':

    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

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.