Encore une histoire de récupérateur de mémoire
« Il est dit que les programmeurs Lisp savent que la gestion de
la mémoire est si importante qu'elle ne peut être laissée aux
programmeurs, et que les programmeurs C savent que la gestion
de la mémoire est si importante qu'elle ne peut être laissée au
système »
Un jour, alors que je développai une application en Javascript
et Ruby (des langages de programmation où la gestion de la
mémoire est totalement cachée au développeur), mes deux
instances de Firefox en sont arrivées à consommer chacune
environ 512Mio de mémoire vive (plusieurs jours sans coupure
pour Firefox). Sur mes 2Gio, cela fait mal, surtout pour deux
onglets souvent actualisés par instance.
L'ordinateur d'à cotés qui a pour différence d'être en 32 bits
(ce qui demande moins de mémoire vive) a pour mémoire vive
totale 512Mio. Ce constat m'a porté à réfléchir sur le
fonctionnement d'un récupérateur de mémoire. Certains feront
peut-être le lien avec un déni trollesque que j'ai réalisé dans
les réactions d'un article sur le java.
Mais force est de constater qu'une grande quantité de mémoire
vive est gaspillée inutilement. Car afficher deux onglets dans
une interface demande d'un cotés 512Mio de mémoire, et de
l'autre, le dernier mac os bien lourd avec les mêmes deux
onglets dans Safari demande 512Mio (à peu de swap près).
Voyons de plus près ce qu'est un récupérateur de mémoire,
appelé aussi ramasse miettes, où «garbage collector».
Le but du récupérateur de mémoire est de récupérer les espaces
mémoires plus utilisés. Dans un langage de programmation tel
que le C, c'est au développeur de gérer les désallocations de
mémoires si l'on tient à passer par une gestion manuelle à base
de pointeurs. Le compilateur s'occupe de la gestion des
variables à l'intérieur des blocs, c'est déjà beaucoup, mais ce
restreindre à ce type de gestion de la mémoire est très vite
contraignant.
Pour presque toutes les applications en C, la gestion manuelle
de la mémoire est inévitable. Et même avec la plus grande
organisation, il est presque impossible de ne pas oublier une
libération quelque part, ou même de gérer tout les cas
possibles que l'on peut avoir.
D'autres langages tels que le ruby (pour ne pas parler du
java), ne fonctionnent que par une gestion automatique de la
mémoire. Cela a tout de suite pour effet un développement plus
rapide et plus simple pour le développeur. Niveau performances,
c'est aussi avantageux tant que l'on a de la mémoire
disponible, car il n'est pas nécessaire de désallouer les
nombreux emplacements alloués.
Le récupérateur de mémoire se déclenche généralement quand il
n'y a plus de mémoire disponible, où quand l'application n'a
rien de mieux à faire. Apparemment, sur eclipse et firefox, il
y a toujours de la mémoire disponible, et l'application a
toujours quelque chose de mieux à faire. Mais finalement, ce
n'est qu'une question de réglages.
Pour savoir quels zones mémoires sont encore utilisées ou pas,
il existe deux grands algorithmes. Je vais redire ce qu'il y a
sur wikipedia car j'aime bien mon clavier bépo. Le premier est
itératif, et le deuxième récursif.
Pour l'itératif donc, si le langage où le code le permet, les
pointeurs sont listés puis parcourus. Les zones mémoires qui
n'ont aucun pointeur vers elles sont à désallouer. Si les
pointeurs sont inconnus, car le langage ne le permet pas, ou
que le code ne le fait pas (en C, c'est souvent réalisé avec
des macros), c'est toute la mémoire qui est parcourue. Dès que
la valeur d'une zone dans la mémoire pointe vers une autre zone
de mémoire, cette zone est marquée comme utilisée. Alors cela
peut donner des faux positifs, un nombre peut pointer sans
faire exprès une zone de mémoire, mais ce n'est pas bien grave,
gérer les faux positifs serait beaucoup plus coûteux en termes
de ressources que de ne pas désallouer un objet parmi tant
d'autres.
Le récursif passe par un arbre à trois couleurs. Chaque zone de
mémoire est représentée par un nœud, et ces nœuds sont reliés
par les pointeurs. Au début de l'algorithme, tout les nœuds
sont marqués de la couleur blanche.
Les sommets de l'arbre sont des éléments connus, tels que les
registres processeurs, la pile d'exécution, mais aussi les
constantes, et encore pleins d'autres choses en fonction du
récupérateur de mémoire. Chaque nœud traité est passé en noir.
Ensuite, les fils du nœud sont récupérés à partir des pointeurs
(connus, ou alors comme dans la version itérative, toutes les
valeurs du nœud sont parcourues à la recherche d'une adresse
valide). Ils sont marqués de la couleur grise, pour ne pas
partir en boucle infinie (ce serait dommage). À la fin, les
nœuds qui sont restés blanc sont à désallouer.
Que ce soit la version itérative où toute la mémoire est passée
en revue, où la version récursive où une représentation de la
mémoire en arbre est carrément construite, force est de
constater que ce n'est pas une opération rapide pour la
machine. Cela m'a permis par exemple de comprendre pourquoi
firefox s'amuse à faire swapper la machine avant de se remettre
en cause.
Finalement, un récupérateur de mémoire n'est pas si compliqué
que ça. Mais face à quelques «delete» bien placés et pas
oubliés, ça fait légèrement lourd.
Pour un langage de script tel que le javascript, je pense qu'un
récupérateur de mémoire est un outil formidable. Surtout quand
on voit les scripts javascript que l'on a souvent sur le net.
Mais pour un langage tel que le C++, l'intérêt est je pense
plus faible. Les quantités de mémoires gérées sont très
importantes (pour que firefox en arrive à utiliser 512Mio de
mémoire, c'est qu'il en voit passé des zones de mémoire), et la
complexité de ces algorithmes font que c'est moins efficace.
Certains langages font le pari que le développeur sait ce qu'il
fait, et qu'il réfléchit plus et à plus long terme qu'une
simple et grosse boucle, alors que d'autres langages masquent
cet aspect au développeur en pensant que vue le nombre
d'erreurs élevé que ces derniers réalisent, il vaut mieux faire
une grosse boucle. Ce qui nous ramène à la citation du début.
PS: Je préfère une application en C qui perd un peu de mémoire
sur certaines opérations, à une application java qui consomme
200Mio quoi qu'il arrive. Mais ça porte à débat évidemment.
C'est aussi un journal sur
linuxfr.org, à l'adresse: http://linuxfr.org/~yellowiscool/28573.html