Grundlagen

Speicherhierarchie

Speicher ist schnell, groß oder billig – nie alles zugleich. Warum es deshalb Register, drei Cache-Stufen, RAM und SSD gibt, was eine Cache-Line ist und warum ein int[] eine LinkedList so oft deklassiert.

Lesestand

Speicher kann schnell, groß oder billig sein – nie alles zugleich. Deshalb hat jeder Computer eine Speicherhierarchie: winzige, extrem schnelle Register und Caches direkt im Prozessor, darunter der größere, langsamere Arbeitsspeicher, darunter die riesige, noch langsamere SSD.

Das funktioniert, weil Programme Lokalität zeigen: Was gerade gebraucht wurde, wird bald wieder gebraucht, und sein Nachbar im Speicher auch. Caches laden deshalb nie einzelne Bytes, sondern ganze Cache-Lines von 64 Byte. Wer seine Daten so anordnet, dass sie nebeneinander liegen, bekommt Geschwindigkeit geschenkt.

Die Pyramide

Stell dir vor, du arbeitest an einem Bericht. Was du gerade brauchst, liegt auf dem Schreibtisch – wenig Platz, aber sofort greifbar. Das Regal hinter dir fasst mehr, kostet aber eine Drehung. Die Bibliothek im Haus noch mehr, kostet aber einen Gang. Und das Archiv in einer anderen Stadt hat alles – aber eine Anfrage dauert Tage. Genau so ist der Speicher eines Computers organisiert. Klick auf eine Stufe:

Von schnell und klein zu langsam und groß
Auf eine Stufe klicken.Größen und Zeiten sind typische Werte für einen aktuellen Laptop- oder Desktop-Prozessor.

Warum baut man nicht einfach alles so schnell wie den L1-Cache? Zwei Gründe:

  • Technik: Caches bestehen aus SRAM – sechs Transistoren pro Bit, schnell, aber groß und teuer. Arbeitsspeicher ist DRAM – ein Transistor und ein winziger Kondensator pro Bit, viel dichter und billiger, aber langsamer, und er muss ständig aufgefrischt werden, weil die Ladung verrinnt. Eine SSD speichert in Flash-Zellen, die auch ohne Strom ihren Zustand halten.
  • Physik: Je größer ein Speicher, desto länger die Wege. Bei 4 GHz legt ein elektrisches Signal in einem Takt nur wenige Zentimeter zurück. Der L1-Cache sitzt deshalb direkt neben dem Rechenwerk, der Arbeitsspeicher auf einem eigenen Riegel, Zentimeter entfernt.

Lokalität: warum Caches überhaupt funktionieren

Ein Cache mit 48 KB kann unmöglich die Daten eines Programms mit Gigabytes halten. Dass er trotzdem in den meisten Fällen trifft – oft in weit über 90 Prozent der Zugriffe –, liegt an zwei Eigenschaften fast aller Programme:

⟳

Zeitliche Lokalität

Was gerade benutzt wurde, wird bald wieder benutzt: die Schleifenvariable, der Zähler, die Methode, die immer wieder aufgerufen wird.

↔

Räumliche Lokalität

Was neben dem gerade Benutzten liegt, wird bald auch benutzt: das nächste Element im Array, die nächste Zeile im Code.

64

Cache-Line

Deshalb lädt der Cache nie ein einzelnes Byte, sondern immer einen Block von 64 Byte. Liest du ein int, kommen 15 Nachbarn gratis mit.

Dazu kommt der Prefetcher: Erkennt der Prozessor ein Muster – etwa, dass ein Programm Adresse für Adresse vorwärts liest –, lädt er die nächsten Cache-Lines schon, bevor sie verlangt werden. Bei einem Array funktioniert das hervorragend. Bei einer verketteten Liste nicht: Wo der nächste Knoten liegt, steht erst im aktuellen Knoten.

Cache-Simulator

Arbeitsspeicher – jede Zeile eine Cache-Line mit 8 Werten

Datenaktueller Zugriffschon gelesenZeile liegt im Cache

Cache – 4 Lines

Vereinfacht: echte Caches sind viel größer, haben mehrere Stufen und einen Prefetcher. Zeitannahme: Treffer 1 ns, Fehlschlag 100 ns. Ersetzt wird die am längsten nicht benutzte Line (LRU).

Alle vier Muster lesen dieselben 64 Werte, die Rechenarbeit ist identisch. Trotzdem liegen zwischen dem besten und dem schlechtesten Muster Welten. Das ist die wichtigste Lektion dieser Seite: Die Reihenfolge der Speicherzugriffe kann wichtiger sein als die Zahl der Rechenschritte.

Wie Java-Daten im Speicher liegen

In Java entscheidet man selten bewusst über Speicherlayout – aber man entscheidet es trotzdem, mit jeder Wahl einer Datenstruktur. Dieselben vier Zahlen, dreimal gespeichert:

int[]

Die Werte liegen direkt hintereinander, 4 Byte pro Zahl. Eine Cache-Line fasst 16 davon. Ideal.

ArrayList<Integer>

Das Array enthält nur Verweise. Jede Zahl ist ein eigenes Objekt mit eigenem Kopf irgendwo im Heap – ein zusätzlicher Sprung pro Element, und mehrfacher Speicherbedarf.

LinkedList<Integer>

Jeder Knoten ist ein Objekt mit Verweisen auf Vorgänger, Nachfolger und den Wert – der wiederum ein eigenes Integer ist. Um zum nächsten Element zu kommen, muss erst der aktuelle Knoten geladen sein. Der Prefetcher kann nicht vorausladen.

Vereinfacht. Wo genau die Objekte liegen, entscheidet die JVM; direkt nacheinander erzeugte Objekte liegen oft nah beieinander – nach einigen Garbage Collections nicht mehr unbedingt.

Caches und mehrere Kerne

Jeder Kern hat eigene L1- und L2-Caches. Was passiert, wenn zwei Kerne dieselbe Speicherstelle in ihren Caches haben und einer schreibt? Ein Kohärenzprotokoll sorgt dafür, dass die Kopie des anderen ungültig wird – bekannt ist etwa MESI, benannt nach den Zuständen einer Line: Modified, Exclusive, Shared, Invalid. Das kostet Zeit, und es arbeitet auf ganzen Cache-Lines.

Daraus entsteht eine berüchtigte Bremse, False Sharing: Zwei Threads schreiben auf zwei verschiedene Variablen, die zufällig in derselben Cache-Line liegen. Logisch teilen sie nichts, physisch reißen sie sich ständig gegenseitig die Line weg. Die JDK-Klasse LongAdder aus dem Thread-Eintrag vermeidet genau das: Sie verteilt ihre Teilzähler mit Abstand auf verschiedene Cache-Lines.

class Zaehler {
    volatile long a;   // Thread 1 schreibt nur a
    volatile long b;   // Thread 2 schreibt nur b
}   // a und b liegen 8 Byte auseinander – dieselbe Cache-Line, ständiges Hin und Her

Die unteren Stufen: RAM, SSD, Netzwerk

Unterhalb der Caches gilt dasselbe Prinzip, nur mit größeren Blöcken und längeren Wartezeiten. Das Betriebssystem verwaltet den Arbeitsspeicher in Seiten von meist 4 KB und benutzt freien RAM als Zwischenspeicher für Dateien. Eine SSD liest ebenfalls in Blöcken; zusammenhängende Daten am Stück zu lesen, ist auch hier viel schneller als verstreute.

Und die Hierarchie endet nicht am Gehäuse. Eine Datenbank hält häufig benutzte Seiten im RAM, ein Server legt Ergebnisse in Redis ab, ein CDN hält Bilder in der Nähe des Nutzers, der Browser speichert Dateien lokal. Überall dieselbe Idee: Halte das Benutzte nah. Und überall dasselbe Problem: Wann ist die Kopie veraltet? Nicht umsonst gilt Cache-Invalidierung als eines der schwersten Probleme der Informatik.

Für Entwickler übersetzt

int[][] m = new int[4096][4096];
long s = 0;

for (int r = 0; r < 4096; r++)          // zeilenweise: Nachbarn im Speicher
    for (int c = 0; c < 4096; c++) s += m[r][c];

for (int c = 0; c < 4096; c++)          // spaltenweise: jeder Zugriff eine andere Zeile
    for (int r = 0; r < 4096; r++) s += m[r][c];

// Gleiche Rechenarbeit, oft ein Vielfaches an Laufzeit.
// Messen mit JMH – nicht mit System.nanoTime() um eine Schleife.
EntscheidungFolge für den Cache
int[] statt List<Integer>Werte statt Verweise: dichter, kein Sprung pro Element. Bei Streams: IntStream statt Stream<Integer>
ArrayList statt LinkedListfast immer schneller, oft sogar beim Einfügen in der Mitte – das Verschieben eines zusammenhängenden Blocks ist billiger als das Suchen der Einfügestelle in einer Kette
Objektköpfejedes Objekt trägt einen Kopf von typischerweise 12 Byte; seit Java 25 lässt er sich mit -XX:+UseCompactObjectHeaders auf 8 Byte verkleinern. Viele kleine Objekte heißt viel Kopf
Garbage Collectorverschiebt lebende Objekte zusammen – das verbessert nebenbei die Lokalität
Spaltenorientierte Datenstatt Person[] mit vielen Feldern mehrere Arrays (int[] alter, double[] gehalt), wenn man meist nur ein Feld über alle Einträge liest – so arbeiten analytische Datenbanken

Wer genau wissen will, wie ein Objekt im Speicher liegt, kann es mit dem Werkzeug JOL (Java Object Layout) aus dem OpenJDK-Projekt ausgeben lassen. Und das langfristige Ziel von Projekt Valhalla ist, dass eigene Klassen so dicht im Array liegen können wie heute nur int und double.

Häufige Irrtümer

„Die O-Notation sagt mir, was schneller ist.“ – Sie sagt, wie etwas mit der Größe wächst. Konstante Faktoren wie Cache-Fehlschläge ignoriert sie – und die können einen Faktor 100 ausmachen.
„Mehr RAM macht den Rechner schneller.“ – Nur wenn er vorher auslagern musste. Sonst liegt der zusätzliche Speicher als Datei-Zwischenspeicher bereit, und das war's.
„Eine SSD ist fast so schnell wie RAM.“ – Sie ist viel schneller als eine Festplatte. Aber ein Zugriff dauert immer noch etwa tausendmal länger als einer auf den Arbeitsspeicher.
„Der JIT-Compiler optimiert das schon.“ – Er kann Schleifen umbauen, Werte in Registern halten und Objekte sogar ganz wegoptimieren. Aber er kann aus einer LinkedList kein Array machen.
„Cache ist das, was man im Browser löscht.“ – Das ist einer von vielen. Prozessor-Caches arbeiten unsichtbar in Hardware; das Prinzip ist dasselbe.

Verwandte Begriffe