wake-up-neo.com

Unerwartet schlechte und bizarre Performance für den Store-Loop auf Intel Skylake

Ich sehe unerwartet schlechte Leistung für eine einfache Store-Schleife, die zwei Stores hat: einen mit einem Vorwärtsschritt von 16 Byte und einer, der immer am selben Ort ist1, so was:

volatile uint32_t value;

void weirdo_cpp(size_t iters, uint32_t* output) {

    uint32_t x = value;
    uint32_t          *rdx = output;
    volatile uint32_t *rsi = output;
    do {
        *rdx    = x;
        *rsi = x;

        rdx += 4;  // 16 byte stride
    } while (--iters > 0);
}

In Assembly wahrscheinlich diese Schleife3 sieht aus wie:

weirdo_cpp:

...

align 16
.top:
    mov    [rdx], eax  ; stride 16
    mov    [rsi], eax  ; never changes

    add    rdx, 16

    dec    rdi
    jne    .top

    ret

Wenn der Speicherbereich, auf den zugegriffen wird, in L2 liegt, würde ich davon ausgehen, dass er mit weniger als 3 Zyklen pro Iteration läuft. Der zweite Speicher trifft immer wieder auf den gleichen Ort und sollte ungefähr einen Zyklus hinzufügen. Der erste Speicher impliziert das Einfügen einer Zeile von L2 und damit auch das Entfernen einer Zeile einmal alle 4 Iterationen. Ich bin nicht sicher, wie Sie die L2-Kosten auswerten, aber selbst wenn Sie konservativ schätzen, dass L1 nur in jedem Zyklus einen der folgenden Schritte ausführen kann: Wenn Sie eine Zeile nach L2 entfernen, erhalten Sie ungefähr 1 + 0,25 + 0,25 = 1,5 Zyklen für den Stride-16-Speicherstrom.

In der Tat, Sie kommentieren einen Speicher aus, Sie erhalten ~ 1,25 Zyklen pro Iteration nur für den ersten Speicher und ~ 1,01 Zyklen pro Iteration für den zweiten Speicher. Daher erscheinen 2,5 Zyklen pro Iteration als eine vorsichtige Schätzung.

Die tatsächliche Leistung ist jedoch sehr seltsam. Hier ist ein typischer Testlauf:

Estimated CPU speed:  2.60 GHz
output size     :   64 KiB
output alignment:   32
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.89 cycles/iter,  1.49 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 4.73 cycles/iter,  1.81 ns/iter, cpu before: 0, cpu after: 0
 7.33 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.33 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.34 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.26 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.31 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.29 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.29 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.27 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.30 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.30 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0

Zwei Dinge sind hier komisch.

Zuerst die bimodalen Timings: Es gibt einen Fast Mode und einen Slow Mode. Wir beginnen mit langsamer Modus, nehmen etwa 7,3 Zyklen pro Iteration und wechseln irgendwann auf etwa 3,9 Zyklen pro Iteration. Dieses Verhalten ist konsistent und reproduzierbar und die beiden Timings sind immer ziemlich konsistent um die beiden Werte herum angeordnet. Der Übergang zeigt sich in beide Richtungen von langsamer Modus zu schneller Modus und umgekehrt (und manchmal mehrere Übergänge in einem Durchlauf).

Das andere Komische ist die wirklich schlechte Leistung. Selbst in Schnellmodus ist die Leistung bei etwa 3,9 Zyklen viel schlechter als die von 1,0 + 1,3 = 2,3 Zyklen, die Sie am meisten erwarten würden, wenn Sie die einzelnen Fälle zu einem einzigen Speicher addieren (und davon ausgehend, dass dies der Fall ist) absolut Null kann bearbeitet werden, wenn beide Speicher in der Schleife sind). Im langsamer Modus ist die Leistung im Vergleich zu dem, was Sie nach den ersten Prinzipien erwarten würden, schrecklich: Es werden 7,3 Zyklen benötigt, um 2 Speicher auszuführen. Wenn Sie die Bandbreite in L2-Speicherbandbreite angeben, ist dies ungefähr 29 Zyklen pro L2-Speicher (da wir nur alle 4 Iterationen eine vollständige Cache-Zeile speichern).

Skylake ist aufgezeichnet mit einem 64B/Zyklus-Durchsatz zwischen L1 und L2, der Weg höher ist als der hier beobachtete Durchsatz (etwa 2 Bytes/Zyklus in langsamer Modus) .

Was erklärt den schlechten Durchsatz und die bimodale Leistung und kann ich dies vermeiden?

Ich bin auch gespannt, ob dies auf anderen Architekturen und sogar auf anderen Skylake-Boxen reproduziert wird. Fühlen Sie sich frei, lokale Ergebnisse in die Kommentare aufzunehmen.

Sie finden den test-Code und das Gurtzeug auf github . Es gibt eine Makefile für Linux oder Unix-ähnliche Plattformen, aber es sollte relativ einfach sein, auch auf Windows aufzubauen. Wenn Sie die Variante asm ausführen möchten, benötigen Sie nasm oder yasm für die Assembly4 - Wenn Sie das nicht haben, können Sie einfach die C++ - Version ausprobieren.

Eliminierte Möglichkeiten

Hier sind einige Möglichkeiten, die ich in Betracht gezogen und weitgehend eliminiert habe. Viele der Möglichkeiten werden durch die einfache Tatsache beseitigt, dass der Leistungsübergang zufällig in der Mitte der Benchmarking-Schleife angezeigt wird, wenn sich viele Dinge einfach nicht geändert haben (z. B. wenn sie mit dem Ausgabearray zusammenhängen) Ausrichtung konnte nicht geändert werden, da derselbe Puffer die ganze Zeit verwendet wird). Ich bezeichne das als default Eliminierung unten (selbst für Dinge, die Default Eliminierung sind, gibt es oft ein anderes Argument).

  • Ausrichtungsfaktoren: Das Ausgabearray ist 16-Byte-ausgerichtet und ich habe bis zu 2 MB Ausrichtung ohne Änderung versucht. Wird auch durch die Standardeliminierung entfernt.
  • Konflikt mit anderen Prozessen auf der Maschine: Der Effekt wird auf einer stillstehenden Maschine mehr oder weniger identisch und sogar auf einer stark belasteten Maschine (z. B. unter Verwendung von stress -vm 4) beobachtet. Der Benchmark selbst sollte ohnehin vollständig kernlokal sein, da er in L2 passt und perf bestätigt, dass nur sehr wenige L2-Fehlschläge pro Iteration vorhanden sind (etwa 1 Fehlschlag alle 300-400 Iterationen, wahrscheinlich im Zusammenhang mit dem printf-Code).
  • TurboBoost: TurboBoost ist vollständig deaktiviert, was durch drei verschiedene MHz-Werte bestätigt wird.
  • Energiesparende Dinge: Der Leistungsregler ist intel_pstate im performance-Modus. Während des Tests werden keine Frequenzschwankungen beobachtet (CPU bleibt im Wesentlichen bei 2,59 GHz gesperrt).
  • TLB-Effekte: Der Effekt ist auch dann vorhanden, wenn sich der Ausgabepuffer auf einer großen 2 MB-Seite befindet. In jedem Fall decken die 64 4k-TLB-Einträge den 128K-Ausgabepuffer mehr als ab. perf meldet kein besonders merkwürdiges TLB-Verhalten.4k-Aliasing: Ältere, komplexere Versionen dieses Benchmarks zeigten etwas 4k-Aliasing. Dieses wurde jedoch eliminiert, da keine Ladungen im Benchmark enthalten sind (Ladungen, die möglicherweise früher Aliasing-Aliasing waren). Wird auch durch die Standardeliminierung entfernt.
  • L2-Assoziativitätskonflikte: Beseitigt durch die Standardeliminierung und durch die Tatsache, dass dies auch bei 2-MB-Seiten nicht der Fall ist, wo wir sicher sein können, dass der Ausgabepuffer im physischen Speicher linear angeordnet ist.
  • Hyperthreading-Effekte: HT ist deaktiviert.
  • Prefetching: Hier könnten nur zwei der Prefetchers beteiligt sein (die "DCU", alias L1 <-> L2-Prefetchers), da alle Daten in L1 oder L2 leben, die Performance ist jedoch gleich, wenn alle Prefetchers aktiviert oder deaktiviert sind. 
  • Interrupts: Kein Zusammenhang zwischen Interrupt-Zählung und Langsam-Modus. Es gibt eine begrenzte Anzahl von Gesamtinterrupts, meistens Uhrentakte.
  • Toplev.py.

ich habe toplev.py verwendet, das Intels Top Down -Analyse-Methode implementiert, und es überrascht nicht, dass der Benchmark als Store-gebunden gilt:

BE Backend_Bound: 82.11 % Slots [ 4.83%] BE/Mem Backend_Bound.Memory_Bound: 59.64 % Slots [ 4.83%] BE/Core Backend_Bound.Core_Bound: 22.47 % Slots [ 4.83%] BE/Mem Backend_Bound.Memory_Bound.L1_Bound: 0.03 % Stalls [ 4.92%] This metric estimates how often the CPU was stalled without loads missing the L1 data cache... Sampling events: mem_load_retired.l1_hit:pp mem_load_retired.fb_hit:pp BE/Mem Backend_Bound.Memory_Bound.Store_Bound: 74.91 % Stalls [ 4.96%] <== This metric estimates how often CPU was stalled due to store memory accesses... Sampling events: mem_inst_retired.all_stores:pp BE/Core Backend_Bound.Core_Bound.Ports_Utilization: 28.20 % Clocks [ 4.93%] BE/Core Backend_Bound.Core_Bound.Ports_Utilization.1_Port_Utilized: 26.28 % CoreClocks [ 4.83%] This metric represents Core cycles fraction where the CPU executed total of 1 uop per cycle on all execution ports... MUX: 4.65 % PerfMon Event Multiplexing accuracy indicator

gcc -O1 (Version 5.4.1) kompilieren, und wahrscheinlich die meisten vernünftigen Compiler (volatile), um das Versinken zu vermeiden -dead zweiten Speicher außerhalb der Schleife).  

 Zweifellos könnten Sie dies mit ein paar kleinen Änderungen in MASM-Syntax konvertieren, da die Assembly so unbedeutend ist. Pull-Anfragen akzeptiert.

23
BeeOnRope

Was ich bisher gefunden habe. Leider bietet es keine wirkliche Erklärung für die schlechte Leistung und überhaupt nicht für die bimodale Verteilung, sondern ist eher ein Satz von Regeln für den Zeitpunkt, zu dem Sie die Leistung und Hinweise zu ihrer Minderung sehen können:

  • Der Speicherdurchsatz in L2 scheint höchstens eine 64-Byte-Cache-Zeile pro drei Zyklen zu betragenLegen Sie eine Obergrenze von ~ 21 Bytes pro Zyklus für den Speicherdurchsatz fest. Anders ausgedrückt, eine Reihe von Speichern, die in L1 fehlen und in L2 treffen, benötigt mindestens drei Zyklen pro Cache-Zeile, die berührt werden.
  • Oberhalb dieser Grundlinie gibt es eine erhebliche Strafe, wenn Geschäfte, die in L2 getroffen wurden, verschachtelt mit Geschäften zu einer anderen Cache-Zeile (unabhängig davon, ob diese Geschäfte in L1 getroffen wurden oder L2).
  • Die Strafe ist anscheinend etwas größer für Geschäfte, die in der Nähe sind (aber immer noch nicht in derselben Cache-Zeile).
  • Das bimodale Verhalten hängt zumindest oberflächlich mit dem obigen Effekt zusammen, da es im nicht verschachtelten Fall nicht vorkommt, obwohl ich keine weitere Erklärung dafür habe.
  • Wenn Sie sicherstellen, dass sich die Cache-Zeile vor dem Speichern bereits in L1 befindet, wird die langsame Leistung durch Prefetch- oder Dummy-Laden aufgehoben und die Leistung ist nicht mehr bimodal.

Details und Bilder

64-Byte-Schritt

Die ursprüngliche Frage verwendete willkürlich einen Schritt von 16, aber beginnen wir mit dem wahrscheinlich einfachsten Fall: einem Schritt von 64, d. H. Einer vollen Cache-Zeile. Wie sich herausstellt, sind die verschiedenen Effekte bei jedem Schritt sichtbar, aber 64 sorgt bei jedem Schritt für einen L2-Cache-Fehler und entfernt so einige Variablen.

Entfernen wir jetzt auch den zweiten Speicher. Wir testen also nur einen einzelnen 64-Byte-Speicher mit mehr als 64 KB Speicher:

top:
mov    BYTE PTR [rdx],al
add    rdx,0x40
sub    rdi,0x1
jne    top

Läuft dies im gleichen Gurt wie oben, bekomme ich ca. 3,05 Zyklen/Speicher2, obwohl es im Vergleich zu dem, was ich bisher gesehen habe, einiges an Varianz gibt (- man kann sogar eine 3.0 darin finden).

Wir wissen also bereits, dass wir wahrscheinlich nicht besser abschneiden werden, wenn es sich nur um L2-Läden handelt1. Während Skylake anscheinend einen 64-Byte-Durchsatz zwischen L1 und L2 hat, muss im Fall eines Stroms von Speichern diese Bandbreite für beide Räumungen von L1 geteilt werden und um die neue Leitung in L1 zu laden. 3 Zyklen erscheinen vernünftig, wenn beispielsweise jeweils 1 Zyklus erforderlich ist, um (a) die Leitung des verschmutzten Opfers von L1 nach L2 zu räumen, (b) L1 mit der neuen Leitung von L2 zu aktualisieren und (c) den Speicher in L1 zu übertragen.

Was passiert, wenn Sie addieren und ein zweites Mal in dieselbe Cache-Zeile (auf das nächste Byte, obwohl es sich als unwichtig herausstellt) in der Schleife schreiben? So was:

top:
mov    BYTE PTR [rdx],al
mov    BYTE PTR [rdx+0x1],al
add    rdx,0x40
sub    rdi,0x1
jne    top

Hier ist ein Histogramm des Timings für 1000 Läufe des Testgurtes für die obige Schleife:

  count   cycles/itr
      1   3.0
     51   3.1
      5   3.2
      5   3.3
     12   3.4
    733   3.5
    139   3.6
     22   3.7
      2   3.8
     11   4.0
     16   4.1
      1   4.3
      2   4.4

Daher sind die meisten Zeiten um 3,5 Zyklen gruppiert. Das bedeutet, dass dieser zusätzliche Speicher dem Timing nur 0,5 Zyklen hinzufügt. Es könnte sein, dass der Speicherpuffer in der Lage ist, zwei Speicher in die L1 zu entleeren, wenn sie sich in derselben Zeile befinden, aber dies geschieht nur etwa zur Hälfte der Zeit.

Beachten Sie, dass der Speicherpuffer eine Reihe von Speichern wie 1, 1, 2, 2, 3, 3 Enthält, wobei 1 Die Cache-Zeile angibt: Die Hälfte der Positionen enthält zwei aufeinanderfolgende Werte aus derselben Cache-Zeile, die andere Hälfte nicht. Da der Speicherpuffer darauf wartet, die Speicher zu entleeren, und der L1 die Leitungen von L2 fleißig ausräumt und akzeptiert, wird der L1 an einem "willkürlichen" Punkt für einen Speicher verfügbar sein, und wenn er sich an der Position 1, 1 vielleicht entleeren sich die Läden in einem Zyklus, aber wenn es bei 1, 2 ist, dauert es zwei Zyklen.

Beachten Sie, dass es einen weiteren Höchstwert von etwa 6% der Ergebnisse bei 3,1 statt 3,5 gibt. Das könnte ein stabiler Zustand sein, in dem wir immer das glückliche Ergebnis erzielen. Es gibt einen weiteren Höchststand von rund 3% bei ~ 4,0-4,1 - die "immer unglückliche" Vereinbarung.

Testen wir diese Theorie, indem wir verschiedene Offsets zwischen dem ersten und dem zweiten Geschäft betrachten:

top:
mov    BYTE PTR [rdx + FIRST],al
mov    BYTE PTR [rdx + SECOND],al
add    rdx,0x40
sub    rdi,0x1
jne    top

Wir probieren alle Werte von FIRST und SECOND von 0 bis 256 in Schritten von 8 aus. Die Ergebnisse variieren mit FIRST Werten auf der vertikalen Achse und SECOND auf der Horizontalen:

cycles/iter for varying store offsets

Wir sehen ein bestimmtes Muster - die Weißwerte sind "schnell" (um die oben diskutierten Werte von 3.0-4.1 für den Versatz von 1). Gelbe Werte sind höher, bis zu 8 Zyklen und rote Werte bis zu 10. Die violetten Ausreißer sind die höchsten und treten normalerweise in dem im OP beschriebenen "langsamen Modus" auf (normalerweise mit 18,0 Zyklen/Iter). Wir bemerken Folgendes:

  • Aus dem Muster der weißen Zellen sehen wir, dass wir das schnelle Ergebnis von ~ 3,5 Zyklen erhalten, solange sich der zweite Speicher in derselben Cache-Zeile befindet oder der nächste relativ zum ersten Speicher. Dies steht im Einklang mit der obigen Idee, dass Speicher in derselben Cache-Zeile effizienter gehandhabt werden. Der Grund dafür, dass der zweite Speicher in der nächsten Cache-Zeile funktioniert, ist, dass das Muster mit Ausnahme des ersten ersten Zugriffs identisch ist: 0, 0, 1, 1, 2, 2, ... Vs 0, 1, 1, 2, 2, ... - wo im zweiten Fall ist der zweite Speicher, der zuerst jede Cache-Zeile berührt. Der Speicherpuffer kümmert sich jedoch nicht darum. Sobald Sie in verschiedene Cache-Zeilen geraten, erhalten Sie ein Muster wie 0, 2, 1, 3, 2, ..., Und anscheinend ist das scheiße?

  • Die violetten "Ausreißer" werden nie in den weißen Bereichen angezeigt, daher sind sie anscheinend auf das bereits langsame Szenario beschränkt (und das langsamere macht es hier etwa 2,5-mal langsamer: von ~ 8 bis 18 Zyklen).

Wir können etwas verkleinern und uns noch größere Offsets ansehen:

offsets up to 2048

Das gleiche Grundmuster, obwohl wir sehen, dass sich die Leistung verbessert (grüner Bereich), wenn der zweite Speicher weiter entfernt ist (vor oder hinter dem ersten), bis er mit einem Versatz von ca. 1700 Byte wieder schlechter wird. Sogar im verbesserten Bereich erreichen wir nur höchstens 5,8 Zyklen/Iteration, die noch viel schlechter sind als die gleiche Linienleistung von 3,5.

Wenn Sie any eine Art Lade- oder Prefetch-Anweisung hinzufügen, die vor Ihnen ausgeführt wird3 Bei den Filialen verschwinden sowohl die allgemeine langsame Leistung als auch die Ausreißer des "langsamen Modus":

all good

Sie können dies auf das ursprüngliche Schritt-für-Schritt-Problem zurückportieren - jede Art von Prefetch oder Last in der Kernschleife, die von der Entfernung ziemlich unempfindlich ist (auch wenn es dahinter ist), behebt das Problem Ausgabe und Sie erhalten 2,3 Zyklen/Iteration, in der Nähe des bestmöglichen Ideals von 2,0 und gleich der Summe der beiden Geschäfte mit separaten Schleifen.

Die Grundregel lautet also, dass Speicher in L2 ohne entsprechende Lasten viel langsamer sind, als wenn Sie sie per Software vorab abrufen - es sei denn, der gesamte Speicherdatenstrom greift in einem einzelnen sequentiellen Muster auf Cache-Zeilen zu. Dies widerspricht der Vorstellung, dass ein lineares Muster wie dieses niemals vom SW-Prefetch profitiert.

Ich habe nicht wirklich eine ausführliche Erklärung, aber sie könnte folgende Faktoren beinhalten:

  • Das Vorhandensein anderer Speicher in den Speicherpuffern kann die Parallelität der Anforderungen verringern, die an L2 gehen. Es ist nicht klar, wann genau Geschäfte, die in L1 fehlen werden, einen Geschäftspuffer zuweisen, aber es tritt möglicherweise in der Nähe des Zeitpunkts auf, zu dem das Geschäft in den Ruhestand geht, und es gibt eine bestimmte Menge von "Lookhead" im Geschäftspuffer, in den Standorte gebracht werden sollen L1, sodass zusätzliche Geschäfte, die in L1 nicht fehlen werden, die Nebenläufigkeit beeinträchtigen, da der Lookahead nicht so viele Anfragen sehen kann, die fehlen werden.
  • Möglicherweise gibt es Konflikte bei L1- und L2-Ressourcen wie Lese- und Schreibports sowie der Bandbreite zwischen den Caches, die bei diesem Speichermuster schlechter sind. Wenn sich beispielsweise Filialen in verschiedenen Zeilen überlappen, können sie möglicherweise nicht so schnell aus der Filialwarteschlange entfernt werden (siehe oben, wo in einigen Szenarien möglicherweise mehr als eine Filiale pro Zyklus entleert wird).

Diese Kommentare von Dr. McCalpin in den Intel-Foren sind auch ziemlich interessant.


 Meist nur mit deaktiviertem L2-Streamer erreichbar, da sonst die zusätzliche Konkurrenz auf dem L2 diese auf ca. 1 Zeile pro 3,5 Zyklen verlangsamt.

1 Vergleichen Sie dies mit Speichern, in denen ich fast genau 1,5 Zyklen pro Last erhalte, bei einer implizierten Bandbreite von ~ 43 Bytes pro Zyklus. Dies macht durchaus Sinn: Die L1 <-> L2-Bandbreite beträgt 64 Bytes, unter der Annahme, dass die L1 entweder eine Leitung von der L2 oder Servicelast akzeptiert Anforderungen vom Kern in jedem Zyklus (aber nicht in beiden parallel), dann haben Sie 3 Zyklen für zwei Ladevorgänge zu verschiedenen L2-Leitungen: 2 Zyklen zum Akzeptieren der Leitungen von L2 und 1 Zyklus zum Erfüllen von zwei Ladebefehlen.

2 Mit Prefetching aus. Wie sich herausstellt, konkurriert der L2-Prefetcher um den Zugriff auf den L2-Cache, wenn er Streaming-Zugriff erkennt: Obwohl er immer die Kandidatenzeilen findet und nicht zu L3 wechselt, verlangsamt dies den Code und erhöht die Variabilität. Die Schlussfolgerungen gelten im Allgemeinen für das Vorabrufen, aber alles ist etwas langsamer (hier ist ein großer Punkt der Ergebnisse mit dem Vorabrufen - Sie sehen ungefähr 3,3 Zyklen pro Ladung, aber mit viel Variabilität).

3 Es muss nicht einmal wirklich im Voraus sein - das Vorabrufen mehrerer Zeilen im Nachhinein funktioniert auch: Ich denke, dass der Vorabruf/die Vorabrufe nur schnell vor den Geschäften ausgeführt werden, die Engpässe aufweisen, sodass sie sowieso weiterkommen. Auf diese Weise ist das Prefetching eine Art Selbstheilung und scheint mit fast jedem Wert zu funktionieren, den Sie eingeben.

11
BeeOnRope

Sandy Bridge verfügt über "L1-Datenhardware-Pre-Fetchers". Dies bedeutet, dass die CPU beim Laden zunächst Daten von L2 nach L1 holen muss; Nachdem dies jedoch mehrmals geschehen ist, bemerkt der Hardware-Vorabruf das Nice-Sequenzmuster und beginnt für Sie, Daten von L2 nach L1 abzurufen, so dass die Daten entweder in L1 oder "auf halbem Weg zu L1" liegen, bevor Ihr Code seinen Code ausführt Geschäft.

0
Brendan