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

Vus : 322
Publié par Yellowiscool : 33