jemalloc : la gestion mémoire à grande échelle
jemalloc gère la mémoire de certains des plus grands systèmes au monde. Comment il réduit la fragmentation et pourquoi Meta mise dessus.

Chaque fois que votre programme appelle malloc(), quelque chose doit décider quel morceau de mémoire virtuelle renvoyer. Cette décision, anodine pour un petit programme, devient un vrai défi d'ingénierie à grande échelle. Un mauvais allocateur fragmente la mémoire, gaspille de la RAM, crée de la contention de verrous entre threads et provoque des pics de latence quand le système d'exploitation doit récupérer des pages. Un bon allocateur ne fait rien de tout cela. jemalloc est un bon allocateur, et Meta vient de renouveler son engagement envers lui, car à leur échelle, la différence entre un allocateur correct et un allocateur médiocre se chiffre en milliards de dollars de matériel.
La plupart des développeurs ne pensent jamais à l'allocation mémoire. Ils appellent new ou malloc et récupèrent un pointeur. Mais à l'échelle de Meta, avec des milliards de requêtes quotidiennes, des millions de serveurs et des pétaoctets de RAM, le comportement de l'allocateur a un impact direct et mesurable sur l'efficacité matérielle, la latence de queue et les coûts d'exploitation.
Ce que fait réellement un allocateur mémoire
L'allocateur mémoire se place entre votre programme et le système d'exploitation. Le système fournit la mémoire par gros blocs (les pages, généralement de 4 Ko ou 2 Mo). Votre programme, lui, veut de la mémoire en petits morceaux de taille variable (une chaîne de 24 octets ici, un buffer de 4096 octets là). Le rôle de l'allocateur est de découper les pages fournies par le système en morceaux adaptés aux besoins du programme, et de recycler les morceaux libérés pour les allocations suivantes.
L'approche naïve, qui consiste à demander une nouvelle page au système pour chaque allocation et à la rendre à chaque libération, est catastrophiquement lente. Les appels système ont un coût. Les pages sont bien plus grandes que la plupart des allocations. Vous utiliseriez 4 Ko de mémoire pour une chaîne de 24 octets.
Les allocateurs réels maintiennent un pool de mémoire et sous-allouent à partir de celui-ci. Les défis sont de minimiser la fragmentation (les trous entre allocations trop petits pour être utilisés), de minimiser la contention de verrous (plusieurs threads qui allouent en même temps) et de minimiser la surcharge (les métadonnées par allocation doivent rester petites par rapport à l'allocation elle-même).
Comment fonctionne jemalloc
jemalloc (créé par Jason Evans, d'où le « je ») a d'abord été développé pour FreeBSD, avant d'être adopté par Meta (alors Facebook) comme allocateur par défaut de ses services C et C++. Sa conception répond aux trois grands défis grâce à des techniques précises.
Les caches par thread éliminent la contention. Chaque thread dispose de son propre cache de petites allocations. Quand un thread appelle malloc() pour un petit objet, l'allocation est servie entièrement depuis le cache local du thread : pas de verrou, pas d'opération atomique, pas de contention. Ce n'est que lorsque ce cache est épuisé que le thread va chercher de quoi se réapprovisionner dans l'arène partagée.
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.
Les classes de taille réduisent la fragmentation. Au lieu d'allouer exactement le nombre d'octets demandé, jemalloc arrondit à la classe de taille supérieure. Ces classes sont soigneusement choisies : 8, 16, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, et ainsi de suite, avec un espacement qui grandit avec la taille. Une allocation de 50 octets reçoit donc un bloc de 64 octets (23 % de perte). Ça a l'air mauvais, mais c'est en fait une bonne chose : comme tous les blocs de 64 octets sont interchangeables, il n'y a aucune fragmentation externe au sein d'une même classe de taille.
Les slabs regroupent les allocations de même taille. Chaque slab est une région contiguë de mémoire découpée en blocs d'une même classe de taille. Un slab pour des objets de 64 octets ne contient que des blocs de 64 octets. On élimine ainsi la pire forme de fragmentation, celle où la mémoire libérée ne peut pas être réutilisée parce qu'elle est coincée entre des allocations actives de tailles différentes.
Le problème de la fragmentation
La fragmentation mémoire est le tueur silencieux des services de longue durée. Un serveur fraîchement démarré utilise bien sa mémoire : les allocations sont serrées. Après des jours ou des semaines d'allocations et de libérations mélangées, le tas ressemble à du gruyère, avec une multitude de petits trous libres entre les allocations actives. La mémoire libre totale peut atteindre 2 Go, alors que la plus grande zone contiguë disponible ne fait que 64 Ko.
La fragmentation externe (les espaces inutilisables entre les allocations) et la fragmentation interne (la place perdue à l'intérieur des allocations à cause de l'arrondi) comptent toutes les deux, mais la fragmentation externe est pire. La fragmentation interne est bornée par l'espacement des classes de taille : au pire, on gaspille environ 25 % par allocation. La fragmentation externe, elle, n'a pas de borne et croît avec le temps.
L'approche par slabs de jemalloc élimine en grande partie la fragmentation externe pour les petites allocations, qui représentent la grande majorité des cas. Comme tous les objets d'un slab ont la même taille, en libérer un crée un trou exactement de la bonne taille pour la prochaine allocation de cette classe. Il n'y a aucun espace inutilisable.
Pour les grandes allocations (généralement au-delà de 14 Ko), jemalloc adopte une stratégie différente : elles obtiennent leurs propres pages, et les pages libérées peuvent être rendues au système d'exploitation ou réutilisées pour d'autres classes de taille. C'est là que la fragmentation peut encore apparaître, mais les grandes allocations restent relativement rares.
jemalloc vs glibc malloc vs tcmalloc
Les trois principaux allocateurs de l'écosystème Linux font chacun des compromis différents.
- glibc malloc (ptmalloc2) est l'allocateur par défaut sur la plupart des systèmes Linux. Il utilise des arènes pour passer à l'échelle avec les threads, mais dispose de moins de classes de taille que jemalloc, ce qui entraîne davantage de fragmentation dans les services de longue durée. Son principal avantage : être celui par défaut, sans aucune configuration.
- tcmalloc (Google) est un malloc à cache par thread, initialement développé pour les services C++ de Google. Il dispose d'un excellent cache local aux threads et d'une faible surcharge. Il convient bien aux charges de travail avec beaucoup d'allocations de courte durée, et moins aux charges sous forte pression mémoire, où la fragmentation compte.
- jemalloc optimise la faible fragmentation et un comportement prévisible sous charge soutenue. Il utilise des classes de taille et une gestion des slabs plus sophistiquées que les autres. Le compromis : une surcharge par allocation légèrement plus élevée (davantage de métadonnées), en échange d'une meilleure efficacité mémoire dans la durée.
Le bon choix dépend de votre charge de travail. Pour les processus de courte durée, les trois se comportent à peu près de la même façon. Pour les serveurs de longue durée aux schémas d'allocation variés (serveurs web, bases de données, caches), la résistance à la fragmentation de jemalloc l'emporte généralement : vous utilisez 10 à 30 % de RAM en moins pour la même charge par rapport à glibc malloc, ce qui, à grande échelle, se traduit directement en économies matérielles.
Pourquoi Meta s'en soucie
À l'échelle de Meta, une réduction de 10 % de la mémoire utilisée par leurs services C++ représente des millions de dollars d'économies matérielles. Pas par an, mais par mois. Quand on exploite des millions de serveurs, chacun faisant des milliards d'allocations et de libérations par jour, l'efficacité de l'allocateur devient une ligne du budget.
Les investissements renouvelés de Meta dans jemalloc portent sur plusieurs axes : un meilleur support des huge pages (moins de TLB misses sur les systèmes à grande mémoire), une restitution améliorée de la mémoire au système d'exploitation (réduction de la mémoire résidente quand la charge baisse) et de meilleurs outils de profilage (comprendre où la mémoire est utilisée et où elle est gaspillée).
L'aspect profilage est particulièrement intéressant. jemalloc intègre un profilage du tas : vous pouvez lui demander d'échantillonner les allocations et de produire un profil montrant où la mémoire a été allouée, quelle part est active ou libérée, et à quel point le tas est fragmenté. Ce profilage a un surcoût quasi nul en production, ce qui permet de le faire tourner en continu sur les serveurs de production.
Utiliser jemalloc dans vos projets
Passer à jemalloc est généralement trivial pour les programmes C/C++. Sous Linux, vous pouvez soit le lier directement, soit utiliser LD_PRELOAD pour l'injecter à l'exécution, sans aucune modification du code.
# 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
Plusieurs projets très connus utilisent jemalloc par défaut : Redis, Rust (qui l'utilise comme allocateur par défaut sur certaines plateformes), Firefox (jemalloc a été initialement développé pour FreeBSD, plateforme ciblée par Firefox) et de nombreux moteurs de jeux.
Pour les langages à mémoire gérée (Python, Java, Go), c'est le runtime du langage qui gère l'allocation, et jemalloc ne s'applique pas directement. Mais les concepts (cache local aux threads, classes de taille, allocation par slabs) se retrouvent dans tous les runtimes modernes et leur ramasse-miettes. L'allocateur du runtime de Go, par exemple, repose sur une conception inspirée de tcmalloc, avec des caches par P (processeur) et des classes de taille.
L'infrastructure invisible
L'allocation mémoire est une infrastructure invisible quand elle fonctionne et catastrophique quand elle ne fonctionne pas. Un serveur qui fuit lentement de la mémoire à cause de la fragmentation finira par être tué par l'OOM killer, et la cause n'apparaîtra dans aucun journal applicatif : elle se situe sous la couche applicative.
Pour la plupart des applications, l'allocateur par défaut suffit. Mais si vous exploitez des serveurs de longue durée, si vous constatez une croissance mémoire inexpliquée, ou si vous opérez à une échelle où l'efficacité matérielle compte, comprendre votre allocateur (et éventuellement en changer pour un meilleur) fait partie des changements d'infrastructure les plus rentables que vous puissiez faire. jemalloc n'a rien de magique. C'est de l'ingénierie : une conception de structures de données soignée, des compromis éclairés et une attention constante aux détails qui comptent à grande échelle.


