Version actuelle Gestion de la mémoire Dans notre version du compilateur, pas de pointeurs, pas de malloc/free, les variables globales sont allouées de façon statiques, les variables locales, même les tableaux, sont allouées dans la pile. Au total, 2 zones de mémoire (plus la zone du code lui-même) : adresses statiques et pile. Le tas Le tas Dernière zone de mémoire, dédiée à l allocation dynamique. pile tas data code section dynamique section fixe En C, un malloc renvoie une adresse dans le tas. En assembleur, on y accède soit en connaissant l adresse de la fin de la section de données, et en maintenant à la main soit par des appels systèmes : syscall avec $v0 = 9, $a0 = quantité de mémoire à allouer, l adresse du bloc va dans $v0. Pas d instruction pour désallouer en MIPS...
Allocation de mémoire free() typedef struct a { int val; struct a *filsg; struct a *filsd; } *arbre; arbre cree_arbre (int val, arbre filsg, arbre filsd){ arbre resultat = (arbre) malloc (sizeof(struct a)); /*... */ } 5 L instruction free() en C permet de libérer de la mémoire qui va pouvoir être réutilisée par le compilateur. Ce fonctionnement permet un usage efficace de la mémoire quand il est bien utilisé, mais donne beaucoup de responsabilité au programmeur : penser à tout libérer ; ne pas libérer un objet encore vivant ; ne pas libérer deux fois un même pointeur. Les problèmes qui viennent d une mauvaise utilisation provoquent des erreurs difficiles à détecter : fuites de mémoire, erreurs d écriture ne provoquant pas de crash, etc. Désallocation implicite Ramassage de miettes La plupart des langages de programmation actuels utilisent donc l allocation en interne. Il faut donc des algorithmes permettant d allouer de la mémoire, mais surtout de désallouer automatiquement les objets dont on n a plus besoin. On appelle cette dernière étape le ramassage de miettes (garbage collector). On construit un graphe d accessibilité du tas : les sommets sont les objets, les arêtes représentent les pointeurs, les objets auxquels le compilateur a directement accès sont appelés racines du graphe. Tout objet inaccessible depuis une racine ne sert à rien, et peut donc être désalloué.
Ramassage de miettes Première phase : marquage struct aliste { arbre tete; struct aliste* queue; } l; /*... */ l = l->queue; 5 l 3 NULL L algorithme récursif visiter (x) : si x n est pas marqué, marquer x, pour tout champ de x, visiter (x). Pour tout objet x du programme, Visiter(x). Deuxième phase : parcours du tas Marquage dans les appels fonctionnels Une fois le marquage réalisé, on balaye le tas horizontalement pour retirer les objets inutiles. Pour chaque élément x du tas si x est marqué démarquer x sinon ajouter x à la liste d objets disponibles. La pile contient la mémoire locale (trame) de potentiellement plusieurs fonctions superposées. Chaque segment contient des variables, pointeurs, etc, qui sont vivantes et que l on doit considérer. Chaque segment a une taille différente, des objets différents, selon la taille et le nombre de paramètres, les registres sauvegardés, etc... Comment déterminer explorer la pile et déterminer ce que contient chaque segment, sans ajouter d information supplémentaire?
Marquage dans les appels fonctionnels Solution : une table pour chaque call dans le programme contenant la taille de la trame associée, la liste des emplacements contenant des pointeurs dans cette trame. Marquage dans les appels fonctionnels inserer(aliste l, int v){ if (!l l->tete > v) nouvelle_cellule(v, l); else l->queue = inserer(l->queue, v); } l... v $ra Marquage et utilisation de la pile Algorithme de Deutsch-Schorr-Waite Version itérative de l algorithme de marquage. Autre problème : la version de Visiter qui explore les structures est récursive, donc a besoin d une pile, donc de mémoire (qu on cherche à économiser). c b d... En fait non, si on retourne temporairement les pointeurs. C est l algorithme de Deutsch-Schorr-Waite. a...... Visiter (x) : si x n est pas marqué : p := NULL marquer (x) pour tout champ non marqué i de x : tmp := x.i x.i := p p := x x := tmp continuer si aucun champ non marqué et p non NULL, soit j le dernier champ marqué de p tmp := p.j p.j := x x := p p := tmp sinon fini.
Fragmentation Compactage 1 2 Encore un problème : il arrive qu on libère beaucoup de petits objets disséminés, libérant de la mémoire fragmentée (qu on appelle trous). Il peut alors être impossible d allouer un gros bloc. Première tactique : toujours allouer le trou le plus juste pour le bloc demandé. Deuxième tactique : le compactage. 1 2 Trois balayages du tas : 1 calcul des nouvelles positions, 2 mise à jour des pointeurs existants, 3 déplacement des blocs, de gauche à droite. Limites des ramasse-miettes Les algorithmes de gestion de mémoire prennent de la place en mémoire en indexant les blocs... alors qu ils sont censés l économiser, sont lents de façon générale, bloquent le programme ponctuellement pendant un certain temps. Encore plus compliqué : utilisation de cache de vitesses différentes... Conclusion
La compilation...... est un torrent qui semble difficile à traverser, mais on en vient à bout en sautant de rocher en rocher. code 3 adresses analyse lexicale analyse syntaxique arbre abstrait table des symboles La compilation... Chaque étape amène une nouvelle représentation intermédiaire du programme, pose de nouveaux problèmes : certains impossible à résoudre de façon optimale, parfois plusieurs solutions possibles, aucune n étant la meilleure (LL ou LR? Quel algo de ramasse-miettes?) Chaque langage a ses spécificités, à traiter en particulier est plus ou moins proche du langage cible (C assembleur est plus facile que OCaml assembleur) langage machine