imprimable
Transcription
imprimable
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. En C, un malloc renvoie une adresse dans le tas. En assembleur, on y accède pile ↓ section dynamique tas ↑ $v0 = 9, $a0 = quantité de mémoire à allouer, l’adresse du bloc va dans $v0. data section fixe code 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 Pas d’instruction pour désallouer en MIPS ... Allocation de mémoire 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)); /* ... */ } free() 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. 5 Désallocation implicite La plupart des langages de programmation actuels utilisent donc l’allocation en interne. Il faut donc des algorithmes permettant 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. Ramassage de miettes On construit un graphe d’accessibilité du tas : les sommets sont les objets, d’allouer de la mémoire, les arêtes représentent les pointeurs, mais surtout de désallouer automatiquement les objets dont on n’a plus besoin. les objets auxquels le compilateur a directement accès sont appelés racines du graphe. On appelle cette dernière étape le ramassage de miettes (garbage collector). 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’algorithme récursif l 3 visiter (x) : si x n’est pas marqué, marquer x, pour tout champ de x, visiter (x). NULL Pour tout objet x du programme, Visiter(x). Deuxième phase : parcours du tas 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. Marquage dans les appels fonctionnels 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 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); } 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. 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). a ... b ... c d ... En fait non, si on retourne temporairement les pointeurs. C’est l’algorithme de Deutsch-Schorr-Waite. 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 2 1 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é. 1 2 Deuxième tactique : le compactage. 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. . . La compilation. . . . . . est un torrent qui semble difficile à traverser, mais on en vient à bout en sautant de rocher en rocher. analyse lexicale analyse syntaxique arbre abstrait code 3 adresses table des symboles langage machine 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)