Ist der L2 HW Prefetcher wirklich hilfreich?

10

Ich bin am Whiskey Lake i7-8565U und analysiere die Leistungsindikatoren und die Zeit für das Kopieren von 512 KiB Daten (doppelt so viel wie die L2-Cache-Größe) und habe einige Missverständnisse in Bezug auf die Arbeit des L2 HW-Prefetchers.

Im Intel Manual Vol.4 MSR gibt es MSR, 0x1A4dessen Bit 0 zur Steuerung des L2 HW-Prefetcher dient (1 zum Deaktivieren).


Betrachten Sie den folgenden 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++;
    }
}

Betrachten Sie 2 Läufe des kompilierten main.c

Ich .

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

Ich bemerkte den Schalter:

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

Dies deutet darauf hin, dass der MSR-Deaktivierungs-L2-HW-Prefetcher angewendet wird. Auch andere l2 / LLC-bezogene Dinge unterscheiden sich erheblich. Der Unterschied ist über verschiedene Läufe reproduzierbar . Das Problem ist, dass es fast keinen Unterschied in total timeund Zyklen gibt:

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

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

FRAGE: Werden
L2-Fehler von anderen Leistungsbegrenzern verdeckt?
Wenn ja, können Sie vorschlagen, welche Zähler zu betrachten sind, um sie zu verstehen?

St.Antario
quelle
4
Als Faustregel gilt: Jede nicht abgrundtief implementierte Speicherkopie ist speichergebunden. Auch wenn es nur den L1-Cache trifft. Der Overhead eines Speicherzugriffs ist einfach so viel höher als der, den eine CPU benötigt, um zwei und zwei zusammen zu addieren. In Ihrem Fall verwenden Sie sogar AVX-Anweisungen, um die Anzahl der Anweisungen pro kopiertem Byte zu verringern. Wo immer Ihre Daten gefunden werden (L1, L2, LLC, Speicher), ist der Durchsatz der zugehörigen Speicherkomponente Ihr Engpass.
cmaster

Antworten:

5

Ja, der L2-Streamer ist die meiste Zeit sehr hilfreich.

memcpy hat keine Rechenlatenz zum Ausblenden, daher kann es sich wohl leisten, OoO Exec-Ressourcen (ROB-Größe) die zusätzliche Lastlatenz zu überlassen, die Sie durch mehr L2-Fehler erhalten, zumindest in diesem Fall, wenn Sie alle L3-Treffer erhalten Bei Verwendung eines mittelgroßen Arbeitssatzes (1 MB), der in L3 passt, ist kein Vorabruf erforderlich, um L3-Treffer zu erzielen.

Die einzigen Anweisungen sind Laden / Speichern (und Schleifen-Overhead), sodass das OoO-Fenster Bedarfslasten für ziemlich weit voraus enthält.

IDK, wenn der räumliche L2-Prefetcher und der L1d-Prefetcher hier helfen.


Vorhersage zum Testen dieser Hypothese : Vergrößern Sie Ihr Array, damit Sie L3-Fehler erhalten, und Sie werden wahrscheinlich einen Unterschied in der Gesamtzeit feststellen, wenn OoO exec nicht ausreicht, um die Ladelatenz bis zum DRAM zu verbergen. HW-Prefetch-Triggerung weiter vorne kann einigen helfen.

Die anderen großen Vorteile des HW-Prefetching liegen darin, dass es mit Ihrer Berechnung Schritt halten kann , sodass Sie L2-Treffer erhalten. (In einer Schleife, die mit einer mittellangen, aber nicht schleifengetragenen Abhängigkeitskette berechnet wurde.)

Demand Loads und OoO Exec können viel dazu beitragen, die verfügbare Speicherbandbreite (Single Threaded) zu nutzen, wenn kein anderer Druck auf die ROB-Kapazität ausgeübt wird.


Beachten Sie außerdem, dass auf Intel-CPUs jeder Cache- Fehler eine Back-End-Wiedergabe (vom RS / Scheduler) abhängiger Uops kosten kann , jeweils eine für L1d- und L2- Fehler , wenn die Daten voraussichtlich eintreffen. Und danach spammt der Kern anscheinend optimistisch, während er darauf wartet, dass Daten von L3 eintreffen.

(Siehe https://chat.stackoverflow.com/rooms/206639/discussion-on-question-by-beeonrope-are-load-ops-deallocated-from-the-rs-when-th und Are load ops freigegeben von der RS wenn sie versenden, vervollständigen oder zu einem anderen Zeitpunkt? )

Nicht das Cache-Miss-Laden selbst; In diesem Fall wäre es die Geschäftsanweisung. Genauer gesagt, die Speicherdaten für Port 4. Das spielt hier keine Rolle. Die Verwendung von 32-Byte-Speichern und Engpässen bei der L3-Bandbreite bedeutet, dass wir nicht in der Nähe von 1 Port 4 UOP pro Takt sind.

Peter Cordes
quelle
2
@ St.Antario: nicht wahr? Das macht keinen Sinn; Sie sind speichergebunden, sodass Sie keinen Front-End-Engpass haben, sodass das LSD irrelevant ist. (Dadurch wird vermieden, dass sie erneut aus dem UOP-Cache abgerufen werden, wodurch Strom gespart wird.) Sie nehmen noch Platz im ROB ein, bis sie in Rente gehen können. Sie sind nicht so bedeutend, aber auch nicht zu vernachlässigen.
Peter Cordes
2
Wenn Sie Ihr Array vergrößern, damit Sie L3-Fehler erhalten, werden Sie wahrscheinlich einen Unterschied feststellen. Ich habe eine Reihe von Tests mit 16MiBPuffer und 10Iterationen durchgeführt und tatsächlich 14,186868883 secondsvs 43,731360909 secondsund 46,76% of all LL-cache hitsvs erhalten 99,32% of all LL-cache hits. 1 028 664 372 LLC-loadsvs 1 587 454 298 LLC-loads .
St.Antario
4
@ St.Antario: durch Umbenennen des Registers! Dies ist eines der wichtigsten Elemente von OoO exec, insbesondere auf einem registerarmen ISA wie x86. Siehe Warum dauert Mulss auf Haswell nur 3 Zyklen, anders als in Agners Anweisungstabellen? (Abrollen von FP-Schleifen mit mehreren Akkumulatoren) . Übrigens möchten Sie normalerweise 2 Ladevorgänge ausführen, dann 2 Speicher, nicht Laden / Speichern, Laden / Speichern. Bessere Chance, 4k-Aliasing-Stalls zu vermeiden oder zu verringern, da die späteren Ladevorgänge (die der HW als überlappend mit den vorherigen Speichern erkennen muss oder nicht) weiter entfernt sind.
Peter Cordes
2
@ St.Antario: ja natürlich. Der Optimierungsleitfaden von Agner Fog erklärt auch OoO exec mit der Umbenennung von Registern, ebenso wie Wikipedia. Übrigens vermeidet das Umbenennen von Registern auch WAW-Gefahren und lässt nur echte Abhängigkeiten (RAW) übrig. So Lasten können auch Auftrag vervollständigen aus, ohne eine vorherige Last bis zum Ende zu warten , schreibt die gleichen Architekturregister. Und ja, die einzige schleifenübertragene Dep-Kette ist über RCX, so dass die Kette vorauslaufen kann. Aus diesem Grund können Adressen frühzeitig bereit sein, während Lade- / Speicher-Uops beim Durchsatz von Port 2/3 immer noch Engpässe aufweisen.
Peter Cordes
3
Ich bin überrascht, dass das Prefetching für das Memcpy in L3 nicht geholfen hat. Ich denke, die 10/12 LFBs sind in diesem Fall "genug". Scheint aber komisch: Was ist der limitierende Faktor dort? Die Kernzeit -> L2 sollte kürzer sein als die Zeit L2 -> L3. In meinem mentalen Modell sollte es also hilfreich sein, mehr Puffer (mehr Gesamtbelegung) für das Rückspiel zu haben.
BeeOnRope
3

Ja, der L2 HW Prefetcher ist sehr hilfreich!

Die folgenden Ergebnisse finden Sie beispielsweise auf meinem Computer (i7-6700HQ), auf dem tinymembench ausgeführt wird . In der ersten Ergebnisspalte sind alle Prefetchers aktiviert, in der zweiten Ergebnisspalte ist der L2-Streamer ausgeschaltet (alle anderen Prefetchers sind jedoch noch aktiviert).

Bei diesem Test werden 32-MiB-Quell- und Zielpuffer verwendet, die viel größer als der L3 auf meinem Computer sind. Daher werden hauptsächlich DRAM-Fehler getestet.

==========================================================================
== 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

Bei diesen Tests ist der L2-Streamer nie langsamer und oft fast doppelt so schnell.

Im Allgemeinen stellen Sie möglicherweise die folgenden Muster in den Ergebnissen fest:

  • Kopien scheinen im Allgemeinen stärker betroffen zu sein als Füllungen.
  • Die standard memsetund STOSB fill(diese laufen auf dieser Plattform auf dasselbe hinaus) sind am wenigsten betroffen, wobei das vorabgerufene Ergebnis nur wenige% schneller ist als ohne.
  • Standard memcpyist hier wahrscheinlich die einzige Kopie, die 32-Byte-AVX-Anweisungen verwendet, und sie gehört zu den am wenigsten betroffenen Kopien - aber das Vorabrufen ist immer noch ~ 40% schneller als ohne.

Ich habe auch versucht, die anderen drei Prefetchers ein- und auszuschalten, aber sie hatten im Allgemeinen fast keinen messbaren Effekt für diesen Benchmark.

BeeOnRope
quelle
( vmovdqaUnterhaltsame Tatsache: Ist AVX1 trotz "Ganzzahl".) Glauben Sie, dass die OP-Schleife eine geringere Bandbreite als glibc memcpy bietet? Und deshalb waren 12 LFBs genug, um mit der Nachfrage nach L3 Schritt zu halten, ohne den zusätzlichen MLP aus der L2 <-> L3-Superqueue zu nutzen, den der L2-Streamer belegen kann? Das ist vermutlich der Unterschied in Ihrem Test. L3 sollte mit der gleichen Geschwindigkeit wie der Kern laufen. Sie haben beide Quad-Core-Skylake-Client-äquivalente Mikroarchitekturen, also wahrscheinlich eine ähnliche L3-Latenz?
Peter Cordes
@PeterCordes - Entschuldigung, ich hätte wahrscheinlich klar sein sollen: Dieser Test lag zwischen 32 MiB-Puffern, also werden DRAM-Treffer und keine L3-Treffer getestet. Ich habe zwar tmb die Puffergröße ausgegeben, aber ich sehe es nicht - oops! Das war beabsichtigt: Ich habe nicht versucht, das 512-KiB-Szenario des OP genau zu erklären, sondern nur die Hauptfrage zu beantworten, ob der L2-Streamer mit einem Szenario nützlich ist, das dies zeigt. Ich denke, ich habe eine kleinere Puffergröße verwendet, mit der ich die Ergebnisse mehr oder weniger reproduzieren konnte (ich habe bereits ein ähnliches Ergebnis uarch-benchin den Kommentaren gesehen).
BeeOnRope
1
Ich habe der Antwort die Puffergröße hinzugefügt.
BeeOnRope
1
@ St.Antario: Nein, das ist kein Problem. Keine Ahnung, warum Sie denken, dass es ein Problem sein könnte; Es ist nicht so, dass das Mischen von AVX1- und AVX2-Anweisungen eine Strafe darstellt. Der Punkt meines Kommentars war, dass diese Schleife nur AVX1 erfordert, in dieser Antwort wird jedoch die Verwendung von AVX2-Anweisungen erwähnt. Intel hat die L1d-Lade- / Speicherdatenpfade gleichzeitig mit der Einführung von AVX2 auf 32 Byte erweitert. Daher können Sie die Verfügbarkeit von AVX2 als Teil der Auswahl einer memcpy-Implementierung verwenden, wenn Sie einen Laufzeitversand durchführen ...
Peter Cordes
1
Wie haben Sie den Prefetcher ausgeschaltet und welchen? War es software.intel.com/en-us/articles/… ? Forum software.intel.com/en-us/forums/intel-isa-extensions/topic/… sagt, dass einige Bits unterschiedliche Bedeutungen haben.
Osgx