Lektion
Die Theorie — Cache-Lokalität: Warum die Lesereihenfolge zählt
Ein Cache ist ein kleiner, schneller Speicher. Er hält Kopien von Daten aus dem Hauptspeicher in Cachezeilen. Jede Cachezeile enthält einen ausgerichteten Speicherblock von L Byte. Liegt der Block eines Lesezugriffs bereits im Cache, ist der Zugriff ein Treffer. Andernfalls ist er ein Fehlzugriff, und der gesamte Block wird geladen. In einem direkt abbildenden Cache mit S Cachezeilen ist für den Block, der die Adresse a enthält, nur eine Cachezeile vorgesehen: ⌊a / L⌋ mod S. Liest eine Schleife benachbarte Adressen nacheinander, kann sie jeden geladenen Block für mehrere Zugriffe nutzen. Das nennt man räumliche Lokalität.
Was die einzelnen Symbole bedeuten
L- die Größe einer Cachezeile in Byte: hier 16, bei den meisten Desktop- und Serverprozessoren 64
S- die Anzahl der Cachezeilen: hier 8
- Setzt voraus
- Wir betrachten einen anfangs leeren, direkt abbildenden Cache, int-Werte von 4 Byte und eine zeilenweise gespeicherte Matrix, wie in C üblich. Fortran speichert Matrizen spaltenweise; dort ist die andere Reihenfolge schneller. Gezählt werden nur die Lesezugriffe auf die Matrix. Reale Datencaches der ersten Ebene sind größer und satzassoziativ, typischerweise 8-fach: Ein Block kann in einer beliebigen der 8 Cachezeilen seines Satzes liegen. Das verhindert die meisten der hier gezeigten Konflikte, behebt aber keinen Platzmangel. Hardware-Prefetching, das die nächste Cachezeile schon vor dem ersten Zugriff lädt, berücksichtigen wir hier nicht.
Aufgabe vollständig gelöst
-
Wie viel zusätzlichen Speicherverkehr verursacht die ungünstige Reihenfolge? 4 Schritte
Klick auf 8 × 8 spaltenweise und erkläre, warum es 0 Treffer gibt. Betrachte anschließend eine 1.024 × 1.024-Matrix aus int-Werten von 4 Byte und einen direkt abbildenden Cache mit 32 KiB und Cachezeilen von 64 Byte. Wie viele Bytes werden aus dem Hauptspeicher übertragen, wenn du die Matrix zeilenweise summierst? Wie viele sind es beim spaltenweisen Summieren? Hilft es, jede Matrixzeile um eine Cachezeile aufzufüllen?
-
In der Simulation umfasst eine Matrixzeile 8 × 4 = 32 Byte, also zwei Cachezeilen. Matrixzeile i beginnt deshalb in Cachezeile 2i mod 8, und Spalte 0 nutzt nur die Cachezeilen 0, 2, 4 und 6. a[4][0] belegt die Cachezeile, in der zuvor a[0][0] lag. Wenn die Schleife a[0][1] erreicht, ist dessen Block bereits verdrängt: 0 Treffer bei 64 Zugriffen. Beim zeilenweisen Lesen gibt es pro 16-Byte-Block einen Fehlzugriff, insgesamt 16. Damit bleiben 48 Treffer, also 1 − 4/16 = 75 %.
-
Der große Cache hat 32.768 / 64 = 512 Cachezeilen. Beim zeilenweisen Lesen enthält jede 64-Byte-Cachezeile 16 int-Werte. Nur der jeweils erste Zugriff ist ein Fehlzugriff. Das ergibt 1.048.576 / 16 = 65.536 Fehlzugriffe, eine Trefferquote von 1 − 4/64 = 93,75 % und 65.536 × 64 = 4.194.304 übertragene Bytes. Die Matrix wird genau einmal vollständig geladen.
-
Beim spaltenweisen Lesen umfasst eine Matrixzeile 4.096 Byte, also 64 Cachezeilen. Matrixzeile i beginnt daher in Cachezeile 64i mod 512. Eine Spalte nutzt nur 8 der 512 Cachezeilen. a[8][0] verdrängt den Block von a[0][0]. Lange bevor die Schleife zu a[0][1] zurückkehrt, ist dessen Block verschwunden. Jeder der 1.048.576 Lesezugriffe ist ein Fehlzugriff: 67.108.864 übertragene Bytes, also 16-mal so viel.
-
Mit 16 zusätzlichen int-Werten pro Matrixzeile wächst ihre Länge auf 4.160 Byte, also 65 Cachezeilen. 65 und 512 sind teilerfremd. Eine Spalte verteilt sich nun auf alle 512 Cachezeilen. Trotzdem gibt es keinen Treffer: Eine Spalte greift auf 1.024 Blöcke zu, der Cache fasst aber nur 512. Jeder Block wird verdrängt, bevor die nächste Spalte ihn wieder benötigt.
Antwort
4 MiB beim zeilenweisen Lesen, 64 MiB beim spaltenweisen Lesen. Das Auffüllen verteilt die Blöcke einer Spalte über den gesamten Cache, bringt aber weiterhin keinen Treffer. Es beseitigt die Konflikte. Die Daten jeder einzelnen Spalte dieser Matrix brauchen jedoch mehr Platz, als der gesamte Cache bietet.
-
Lernpfad
Wie ein Computer ein Programm ausführt
Quellen (1)
- The first insight's measured case, the line size in the definition, and the 8-way first-level caches in `assumes`: Ulrich Drepper, "What Every Programmer Should Know About Memory," Red Hat, Inc., version 1.0, 21 November 2007. §3.2 (p. 15), §3.3.1 (pp. 17–18), §6.2.1 (pp. 49–50).