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)

Documents pareils