Fundierte Artikel über Technologien, die das Kommende formen.

jemalloc: Speicherverwaltung im großen Maßstab

jemalloc verwaltet den Speicher einiger der größten Systeme der Welt. Wie der Allokator Fragmentierung reduziert und warum Meta stark darauf setzt.

Roboterarme sortieren leuchtende Datenblöcke in größenabgestufte Speicherfächer in einem Lager

Jedes Mal, wenn dein Programm malloc() aufruft, muss jemand entscheiden, welcher Abschnitt des virtuellen Speichers zurückgegeben wird. Diese Entscheidung ist für ein kleines Programm trivial, wird im großen Maßstab aber zu einem gewaltigen Ingenieursproblem. Ein schlechter Allokator fragmentiert den Speicher, verschwendet RAM, erzeugt Lock-Contention zwischen Threads und verursacht Latenzspitzen, wenn das Betriebssystem Seiten zurückfordern muss. Ein guter Allokator tut nichts davon. jemalloc ist ein guter Allokator, und Meta hat sich gerade erneut zu ihm bekannt, denn bei dieser Größenordnung ist der Unterschied zwischen einem guten und einem mittelmäßigen Allokator Milliarden Dollar an Hardwarekosten.

Die meisten Entwickler denken nie über Speicherallokation nach. Sie rufen new oder malloc auf und bekommen einen Zeiger zurück. Doch bei Metas Größenordnung – Milliarden täglicher Anfragen, Millionen Server, Petabytes an RAM – hat das Verhalten des Allokators direkte, messbare Auswirkungen auf die Hardware-Effizienz, die Tail-Latenz und die Betriebskosten.

Was ein Speicherallokator tatsächlich tut

Der Speicherallokator sitzt zwischen deinem Programm und dem Betriebssystem. Das OS stellt Speicher in großen Blöcken bereit (Seiten, typischerweise 4 KB oder 2 MB). Dein Programm braucht Speicher in kleinen, unterschiedlich großen Stücken (hier ein 24-Byte-String, dort ein 4096-Byte-Puffer). Die Aufgabe des Allokators ist es, die vom OS gelieferten Seiten in die Chunks zu zerlegen, die dein Programm braucht, und freigegebene Chunks für künftige Allokationen wiederzuverwenden.

Der naive Ansatz – für jede Allokation eine neue Seite vom OS anfordern und sie bei free() zurückgeben – ist katastrophal langsam. Systemaufrufe kosten Overhead, und Seiten sind viel größer als die meisten Allokationen. Für einen 24-Byte-String würdest du 4 KB Speicher verbrauchen.

Echte Allokatoren halten einen Speicherpool vor und teilen daraus Stücke zu. Die Herausforderungen: Fragmentierung minimieren (Lücken zwischen Allokationen, die zu klein sind, um genutzt zu werden), Lock-Contention minimieren (mehrere Threads allokieren gleichzeitig) und den Overhead minimieren (die Metadaten pro Allokation sollten im Verhältnis zur Allokation selbst klein sein).

Wie jemalloc funktioniert

jemalloc (entwickelt von Jason Evans, daher das „je“) wurde ursprünglich für FreeBSD entwickelt und später von Meta (damals Facebook) als Standardallokator für C- und C++-Dienste übernommen. Das Design begegnet den drei zentralen Herausforderungen mit konkreten Techniken.

Thread-Caches verhindern Contention. Jeder Thread erhält seinen eigenen Cache für kleine Allokationen. Ruft ein Thread malloc() für ein kleines Objekt auf, wird die Allokation vollständig aus dem lokalen Cache bedient – ohne Locks, ohne atomare Operationen, ohne Contention. Erst wenn der Thread-Cache erschöpft ist, holt er sich im gemeinsamen Arena Nachschub.

jemalloc allocation flow:
Thread calls malloc(64)
↓
Check thread cache for 64-byte size class
→ Hit: return cached chunk (no lock, no contention)
→ Miss: refill from arena
↓
Arena (shared, but per-thread affinity)
→ Find a partially-full slab for 64-byte objects
→ Carve out a chunk
→ Return to thread cache
↓
Return chunk to caller
Thread caches handle 95%+ of allocations without any locking.

Größenklassen reduzieren Fragmentierung. Statt exakt die angeforderte Anzahl Bytes zu reservieren, rundet jemalloc auf die nächstgrößere Größenklasse auf. Die Größenklassen sind sorgfältig gewählt: 8, 16, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256 und so weiter, wobei der Abstand mit wachsender Größe zunimmt. Eine 50-Byte-Allokation bekommt also einen 64-Byte-Chunk (23 % Verschnitt). Das klingt schlecht, ist aber tatsächlich gut, denn alle 64-Byte-Chunks sind austauschbar. Innerhalb einer Größenklasse gibt es daher keine externe Fragmentierung.

Slabs ordnen gleich große Allokationen. Jeder Slab ist ein zusammenhängender Speicherbereich, der in Chunks derselben Größenklasse unterteilt ist. Ein Slab für 64-Byte-Objekte enthält nichts als 64-Byte-Chunks. Damit verschwindet die schlimmste Form der Fragmentierung: wenn freier Speicher nicht wiederverwendet werden kann, weil er zwischen aktiven Allokationen unterschiedlicher Größe eingeklemmt ist.

Das Fragmentierungsproblem

Speicherfragmentierung ist der stille Killer langlaufender Dienste. Ein frisch gestarteter Server nutzt Speicher effizient, denn die Allokationen liegen dicht gepackt. Nach Tagen oder Wochen gemischter Allokationen und Freigaben gleicht der Heap einem Schweizer Käse: viele kleine freie Lücken zwischen aktiven Allokationen. Der gesamte freie Speicher mag 2 GB betragen, der größte zusammenhängende freie Bereich aber nur 64 KB.

Externe Fragmentierung (unbrauchbare Lücken zwischen Allokationen) und interne Fragmentierung (verschwendeter Platz innerhalb von Allokationen durch Aufrunden) sind beide relevant, doch die externe ist schlimmer. Die interne ist durch den Abstand der Größenklassen begrenzt: Im schlimmsten Fall verschwendest du etwa 25 % pro Allokation. Die externe Fragmentierung ist dagegen nach oben offen und wächst mit der Zeit.

Der slab-basierte Ansatz von jemalloc beseitigt die externe Fragmentierung für kleine Allokationen weitgehend, und das sind die allermeisten. Da alle Objekte in einem Slab gleich groß sind, hinterlässt eine Freigabe ein Loch, das genau die richtige Größe für die nächste Allokation dieser Größenklasse hat. Es gibt keine unbrauchbaren Lücken.

Für große Allokationen (typischerweise über 14 KB) verwendet jemalloc eine andere Strategie: Große Allokationen erhalten eigene Seiten, und freigegebene Seiten können an das Betriebssystem zurückgegeben oder für andere Größenklassen wiederverwendet werden. Hier kann weiterhin Fragmentierung entstehen, aber große Allokationen sind vergleichsweise selten.

jemalloc vs. glibc malloc vs. tcmalloc

Die drei wichtigsten Allokatoren im Linux-Ökosystem treffen jeweils unterschiedliche Abwägungen.

  • glibc malloc (ptmalloc2) ist auf den meisten Linux-Systemen der Standard. Es nutzt Arenen für die Skalierung über Threads, hat aber weniger Größenklassen als jemalloc, was bei langlaufenden Diensten zu mehr Fragmentierung führt. Sein Hauptvorteil: Es ist der Standard, es braucht keine Konfiguration.
  • tcmalloc (Google) ist ein Thread-Caching-malloc, ursprünglich für Googles C++-Dienste entwickelt. Es hat exzellente thread-lokale Caches und geringen Overhead. Es eignet sich gut für Workloads mit vielen kurzlebigen Allokationen und weniger gut für Workloads mit hohem Speicherdruck, bei denen Fragmentierung ins Gewicht fällt.
  • jemalloc ist auf geringe Fragmentierung und vorhersehbares Verhalten unter dauerhafter Last optimiert. Es verwendet ausgefeiltere Größenklassen und ein anspruchsvolleres Slab-Management als die anderen. Der Preis: etwas höherer Overhead pro Allokation (mehr Metadaten), dafür eine bessere Speichereffizienz über die Zeit.

Die richtige Wahl hängt von der Workload ab. Bei kurzlebigen Prozessen schneiden alle drei ähnlich ab. Bei langlaufenden Servern mit gemischten Allokationsmustern (Webserver, Datenbanken, Caches) gewinnt meist die Fragmentierungsresistenz von jemalloc. Du brauchst für dieselbe Workload 10–30 % weniger RAM als mit glibc malloc, und das schlägt sich im großen Maßstab direkt in Hardware-Einsparungen nieder.

Warum Meta sich darum kümmert

Bei Metas Größenordnung spart eine Reduktion des Speicherverbrauchs um 10 % in den C++-Diensten Millionen Dollar an Hardwarekosten. Nicht jährlich, sondern monatlich. Wenn du Millionen Server betreibst, auf denen Dienste Speicher milliardenfach pro Tag allokieren und freigeben, ist die Effizienz des Allokators ein Posten im Budget.

Metas erneute Investition in jemalloc konzentriert sich auf mehrere Bereiche: bessere Unterstützung für Huge Pages (weniger TLB-Misses auf Systemen mit viel Speicher), eine verbesserte Rückgabe von Speicher an das Betriebssystem (weniger residenter Speicher, wenn die Last sinkt) und bessere Profiling-Werkzeuge, um zu verstehen, wo Speicher genutzt und wo er verschwendet wird.

Besonders interessant ist das Profiling. jemalloc bringt ein eingebautes Heap-Profiling mit: Du kannst es Allokationen stichprobenartig erfassen lassen und ein Profil erzeugen, das zeigt, wo Speicher alloziert wurde, wie viel davon aktiv ist oder freigegeben wurde und wie stark der Heap fragmentiert ist. Dieses Profiling hat im Produktivbetrieb nahezu keinen Overhead, weshalb es sich dauerhaft auf Produktionsservern betreiben lässt.

jemalloc in deinen Projekten einsetzen

Der Wechsel zu jemalloc ist bei C/C++-Programmen meist unkompliziert. Unter Linux kannst du entweder direkt dagegen linken oder LD_PRELOAD verwenden, um es zur Laufzeit einzuschleusen, ganz ohne Codeänderungen.

# Install jemalloc
sudo apt install libjemalloc-dev  # Debian/Ubuntu
brew install jemalloc             # macOS
# Use LD_PRELOAD — works with any program, no recompilation
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so ./my_server
# Or link at compile time
gcc -o my_server my_server.c -ljemalloc
# Enable profiling
export MALLOC_CONF="prof:true,prof_prefix:jeprof"
./my_server
# Then analyze: jeprof --svg ./my_server jeprof.*.heap > heap.svg

Einige bekannte Projekte nutzen jemalloc standardmäßig: Redis, Rust (verwendet auf manchen Plattformen jemalloc als Standardallokator), Firefox (jemalloc wurde ursprünglich für FreeBSD entwickelt, auf das Firefox ausgerichtet war) und viele Game-Engines.

Für Sprachen mit verwaltetem Speicher (Python, Java, Go) übernimmt die Laufzeitumgebung die Allokation, und jemalloc ist dort nicht direkt anwendbar. Die Konzepte wie thread-lokale Caches, Größenklassen und Slab-Allokation tauchen aber in jedem modernen Laufzeit-Garbage-Collector und Speicherallokator auf. Der Allokator der Go-Laufzeit etwa nutzt ein von tcmalloc inspiriertes Design mit Caches pro P (Prozessor) und Größenklassen.

Die unsichtbare Infrastruktur

Speicherallokation ist Infrastruktur, die unsichtbar ist, solange sie funktioniert, und katastrophal, wenn nicht. Ein Server, der durch Fragmentierung nach und nach Speicher verliert, wird irgendwann vom OOM-Killer beendet, und die Ursache taucht in keinem Anwendungslog auf, denn sie liegt unterhalb der Anwendungsebene.

Für die meisten Anwendungen ist der Standardallokator in Ordnung. Wenn du aber langlebige Server betreibst, ungeklärtes Speicherwachstum erlebst oder in einer Größenordnung arbeitest, in der Hardware-Effizienz zählt, ist es einer der wirksamsten Infrastruktur-Hebel überhaupt, den Allokator zu verstehen und gegebenenfalls einen besseren einzusetzen. jemalloc ist keine Magie. Es ist Ingenieursarbeit: sorgfältiges Datenstrukturdesign, bewusste Abwägungen und unermüdliche Aufmerksamkeit für die Details, auf die es im großen Maßstab ankommt.