Vor- und Nachteile von Hugepages

English Deutsch

In einem vorherigen Beitrag habe ich darüber geschrieben, wie man transparente Hugepages in Linux global prüft und aktiviert.

Obwohl dieser Beitrag wichtig ist, wenn du tatsächlich einen Anwendungsfall für Hugepages hast, habe ich mehrere Leute gesehen, die sich von der Aussicht täuschen ließen, dass Hugepages magisch die Leistung steigern. Hugepages sind jedoch ein komplexes Thema und können, wenn falsch verwendet, die Gesamtleistung leicht verringern.

Dieser Beitrag versucht, Vorteile, Nachteile und Einschränkungen bei der Verwendung von Hugepages zu erklären. Da ein technisch schwerer oder pedantisch genauer Beitrag für die Benutzer, die oft von Hugepages getäuscht werden, wahrscheinlich unzugänglich ist, opfere ich Genauigkeit für Einfachheit. Bedenke einfach, dass die meisten Themen wirklich komplex und daher stark vereinfacht sind.

Beachte, dass wir hier über 64-Bit-x86-Systeme sprechen, die Linux ausführen, und ich einfach annehme, dass das System transparente Hugepages implementiert (d.h. es ist kein Nachteil, dass Hugepages nicht swappbar sind), da dies bei fast jeder aktuellen Linux-Umgebung der Fall ist.

Weitere technische Beschreibungen werden in den untenstehenden Links bereitgestellt.

Virtueller Speicher

Wenn du ein C++-Programmierer bist, weißt du, dass Objekte im Speicher bestimmte Adressen haben (d.h. der Wert eines Zeigers).

Diese Adressen repräsentieren jedoch nicht zwingend physische Adressen (d.h. eine Adresse im RAM). Sie repräsentieren Adressen im virtuellen Speicher. Deine CPU hat eine MMU-Hardware (Memory Management Unit), die den Kernel bei der Zuordnung von virtuellem Speicher zu einem physischen Ort unterstützt.

Dieser Ansatz hat zahlreiche Vorteile, aber hauptsächlich ist er nützlich für

Was sind Pages?

Der virtuelle Speicherplatz ist in Pages unterteilt.

Jede einzelne Page zeigt auf einen physischen Speicher — sie könnte auf einen Abschnitt des physischen RAMs zeigen, aber auch auf eine Adresse, die einem physischen Gerät wie einer Grafikkarte zugewiesen ist.

Die meisten Pages, mit denen du zu tun hast, zeigen entweder auf den RAM oder sind ausgelagert (swapped out), d.h. auf einer HDD oder einer SSD gespeichert.

Der Kernel verwaltet den physischen Ort für jede Page. Wenn auf eine Page zugegriffen wird, die ausgelagert wurde, stoppt der Kernel den Thread, der auf den Speicher zugreifen will, liest die Page von der HDD/SSD in den RAM und führt den Thread anschließend weiter aus.

Dieser Prozess ist für den Thread transparent, d.h. er muss nicht explizit von der HDD/SSD lesen.

Normale Pages sind 4096 Bytes lang. Hugepages haben eine Größe von 2 Megabytes.

Der Translation Lookaside Buffer (TLB)

Wenn ein Programm auf eine Speicher-Page zugreift, muss die CPU wissen, von welcher physischen Page die Daten gelesen werden sollen (d.h. eine virtuell-zu-physisch-Adressabbildung).

Der Kernel enthält eine Datenstruktur (die Page Table), die alle Informationen über alle verwendeten Pages enthält. Mit dieser Datenstruktur könnten wir die virtuelle Adresse auf eine physische Adresse abbilden.

Die Page Table ist jedoch ziemlich komplex und langsam und wir können einfach nicht die gesamte Datenstruktur parsen jedes Mal, wenn ein Prozess auf den Speicher zugreift.

Glücklicherweise enthält unsere CPU Hardware — den TLB — die die virtuell-zu-physische Adressabbildung cacht. Das bedeutet, dass obwohl du die Page Table beim ersten Zugriff auf die Page parsen musst, alle nachfolgenden Zugriffe auf die Page vom TLB verarbeitet werden können, was wirklich schnell ist!

Aber da er in Hardware implementiert ist (was ihn überhaupt erst schnell macht), hat er auch nur eine begrenzte Kapazität. Wenn du also auf eine größere Anzahl von Pages zugreifst, kann der TLB die Abbildung nicht für alle speichern. Dies wird dein Programm deutlich langsamer machen.

Hugepages zur Rettung

Was können wir also tun (unter der Annahme, dass das Programm noch denselben Speicherbedarf hat), um zu vermeiden, dass der TLB voll wird?

Hier kommen Hugepages ins Spiel. Anstatt dass 4096 Bytes einen TLB-Eintrag „verbrauchen“, kann ein TLB-Eintrag nun auf beachtliche 2 Megabytes zeigen.

Wenn wir annehmen, dass der TLB 512 Einträge hat, können wir ohne Hugepages abbilden

$$4096\ \text{b} \cdot 512 = 2\ \text{MB}$$

aber mit Hugepages können wir abbilden

$$2\ \text{MB} \cdot 512 = 1\ \text{GB}$$

Hugepages sind also toll — sie können zu stark erhöhter Leistung führen, fast ohne Aufwand. Aber sie kommen nicht ohne Einschränkungen.

Swapping von Hugepages

Dein Kernel verfolgt automatisch, wie oft jede Speicher-Page verwendet wird. Wenn nicht genügend physischer Speicher (d.h. RAM) verfügbar ist, verschiebt dein Kernel weniger wichtige (d.h. seltener verwendete) Pages auf deine Festplatte, um RAM für wichtigere Pages freizugeben.

Im Prinzip gilt das Gleiche für Hugepages. Aber der Kernel kann nur ganze Pages swappen — nicht einzelne Bytes.

Nehmen wir an, wir haben ein Programm wie dieses:

hugepage_swap_example.cpp
char* mymemory = malloc(2*1024*1024); //Wir nehmen an, dies ist eine Hugepage!
// Fülle mymemory mit einigen Daten
// Mache viele andere Dinge,
// was dazu führt, dass die mymemory-Page ausgelagert wird
// ...
// Greife nur auf das erste Byte zu
putchar(mymemory[0]);

In diesem Fall muss der Kernel die gesamten 2 Megabytes von der HDD/SSD einlesen (swap in), nur damit du ein einziges Byte lesen kannst. Bei normalen Pages müssen nur 4096 Bytes von der HDD/SSD gelesen werden.

Wenn also eine Hugepage ausgelagert wird, ist das Einlesen nur schneller, wenn du auf fast die gesamte Hugepage zugreifen musst. Das bedeutet, wenn du zufällig auf verschiedene Teile des Speichers zugreifst und nur ein paar Kilobytes liest, solltest du einfach normale Pages verwenden und dir keine Gedanken machen.

Wenn du andererseits einen großen Teil des Speichers sequenziell zugreifen musst, können Hugepages deine Leistung erhöhen. Dennoch musst du dies mit deinem Programm benchmarken (nicht mit einer abstrakten Benchmark-Software!) und prüfen, ob es mit oder ohne Hugepages schneller ist.

Speicher-Allokation

Als C-Programmierer weißt du, dass du beliebig kleine (oder fast beliebig große) Speichermengen vom Heap mit malloc() anfordern kannst.

Nehmen wir an, du forderst 30 Bytes Speicher an:

malloc_small_example.c
char* mymemory = malloc(30);

Für den Programmierer mag es so aussehen, als würde man 30 Bytes Speicher vom Betriebssystem „anfordern“ und einen Zeiger auf einen virtuellen Speicherbereich zurückbekommen.

In Wirklichkeit ist malloc() aber nur eine C-Funktion, die intern die Funktionen brk und sbrk aufruft, um Speicher vom Betriebssystem anzufordern oder freizugeben.

Es ist jedoch ineffizient, für jede kleine Allokation immer mehr Speicher vom OS anzufordern — sehr wahrscheinlich wurde ein Speichersegment bereits free()d und wir können diesen Speicher wiederverwenden. malloc() implementiert ziemlich komplexe Algorithmen zur Wiederverwendung von free()d-Speicher.

Aber das ist alles transparent für dich — also warum sollte dich das kümmern? Weil es auch bedeutet, dass der Aufruf von free() nicht bedeutet, dass der Speicher nicht zwingend sofort an das Betriebssystem zurückgegeben wird.

Es gibt eine Art Problem, die Speicherfragmentierung genannt wird. In extremen Fällen gibt es Segmente des Heaps, in denen nur wenige Bytes verwendet werden, während alles dazwischen free()d wurde.

In den meisten Fällen wird der Kernel den größten Teil des ungenutzten Speichers einfach auslagern, weil

Wenn du Hugepages für den gesamten Speicher deines Programms verwendest (d.h. nicht selektiv wie unten gezeigt), könnte dies

Beachte, dass Speicherfragmentierung ein unglaublich komplexes Problem ist und selbst kleine Änderungen eines Programms die Speicherfragmentierung signifikant beeinflussen können. In den meisten Fällen verursachen Programme keine signifikante Speicherfragmentierung, aber du solltest bedenken, dass Hugepages das Problem verschlimmern können, wenn es Speicherfragmentierungsprobleme in einem Bereich des Heaps gibt.

Selektives Hugepaging

Mit den Informationen in diesem Artikel hast du einige Teile deines Programms identifiziert, die von Hugepages profitieren könnten, während andere Teile nicht davon profitieren. Solltest du Hugepages aktivieren oder nicht?

Glücklicherweise kannst du madvise() verwenden, um Hugepaging nur für Speicherbereiche zu aktivieren, in denen du davon profitierst.

Prüfe zuerst, dass Hugepages im madvise-Modus aktiviert sind (siehe diesen Blogbeitrag).

Verwende dann madvise, um dem Kernel mitzuteilen, dass Hugepages verwendet werden sollen für den

madvise_example.cpp
#include <sys/mman.h>

// Eine große Speichermenge allokieren, für die du Verwendung hast
size_t size = 256*1024*1024;
char* mymemory = malloc(size);
// Entweder nur Hugepages aktivieren ...
madvise(mymemory, size, MADV_HUGEPAGE);
// ... oder auch dem Kernel mitteilen, dass
madvise(mymemory, size, MADV_HUGEPAGE | MADV_SEQUENTIAL)

Beachte, dass dies nur einige Speicherverwaltungsaspekte dem Kernel empfiehlt. Es bedeutet nicht automatisch, dass der Kernel tatsächlich Hugepages für den angegebenen Speicher verwendet.

Siehe die madvise-Manpage für weitere Details — es gibt viel über Speicherverwaltung und madvise zu lernen, und das Thema hat eine unglaublich steile Lernkurve. Wenn du diesen Weg gehen willst, bereite dich darauf vor, ein paar Wochen zu lesen, zu testen und zu benchmarken, bevor du ein positives Ergebnis erwartest.

Weiterlesen

IBM OpenStack-Artikel über Hugepages Transparente Hugepages vs. nicht-transparente Hugepages Wikipedia-Artikel über den TLBLinux-Kernel-Dokumentation über transparente HugepagesStackOverflow: Hugepages sind anfällig für SpeicherfragmentierungMicrosoft zur Verwaltung von virtuellem Speicher


Check out similar posts by category: C/C++, Performance