Fonctionnement de malloc et appels système

Le programme d’exemple qui invoque malloc(16) mille fois ne génère que deux appels brk. Le premier brk(NULL) interroge l’adresse du program break, le second brk(0x59faca252000) étend le heap de 132 KiB. Les 998 appels suivants utilisent la zone déjà réservée, ce qui montre que malloc regroupe les petites demandes pour éviter les syscalls coûteux (mesurés à 575 ns pour un appel vide).

brk(NULL)              = 0x59faca231000
brk(0x59faca252000)    = 0x59faca252000

Le contraste avec sbrk(16), qui effectue un syscall à chaque requête, est illustré par le benchmark : sbrk(16) coûte 610 ns, alors que malloc(16) ne nécessite que 26.9 ns, soit un facteur 22.7 plus rapide.

syscall(SYS_getppid)      575.2 ns per call
sbrk(16)                  610.1 ns per call
malloc(16)                 26.9 ns per call

Structure interne des blocs et alignement

Chaque allocation possède un en‑tête de 8 octets contenant la taille du bloc. La taille réelle du « chunk » correspond à la demande plus cet en‑tête, arrondie au multiple de 16 octets, avec un minimum de 32 octets. Ainsi, une requête de 1 octet devient un bloc de 32 octets, tandis qu’une demande de 1000 octets produit un chunk de 1008 octets.

 request    chunk
       1       32
      24       32
      25       48
    1000     1008

Le fait que les bits de poids faibles soient toujours zéro permet à glibc de stocker trois drapeaux dans ces bits. Lorsqu’un bloc est libéré, il est inséré dans une free list où le même espace mémoire contient à la fois le pointeur vers le bloc suivant et la taille, réutilisant ainsi la zone précédemment occupée par les données de l’utilisateur.

Analyse de performances, limites et améliorations possibles

Le « bump allocator » présenté utilise directement sbrk : chaque appel incrémente le program break sans réutiliser la mémoire libérée, ce qui entraîne une fragmentation linéaire. Le tableau d’exemple montre que l’allocation d se place après c même si b a été libéré, illustrant l’absence de récupération.

a = my_malloc(16)   start+0
b = my_malloc(1)    start+16
c = my_malloc(100)  start+32
my_free(b)
d = my_malloc(1)    start+144
break               start+160

En intégrant les mécanismes de splitting et coalescing, un allocateur peut réutiliser les espaces libres : il recherche le premier bloc assez grand, le découpe si nécessaire, et fusionne les blocs adjacents lors d’un free. Cette logique, déjà implémentée dans glibc, explique pourquoi malloc reste nettement plus rapide que sbrk tout en limitant la consommation de mémoire virtuelle. Cependant, la taille minimale de 32 octets impose un overhead de ≈31 octets pour les très petites allocations, ce qui peut devenir significatif dans des programmes à forte granularité.