Génération de code

publicité
Génération de code
Frédéric Béchet
Carlos Ramisch
Sylvain Sené 1
Compilation – L3 Informatique
Département Informatique et Interactions
Aix Marseille Université
1. Adapté des diapos de Alexis Nasr
1 / 35
Données programme =⇒ machine
Machine
Registres
Instructions opèrent sur les registres
Accès rapide
Nombre limité (MIPS → 32)
Mémoire
Capacité “illimitée” (très grande)
Accès plus lent
Programme source
Variables (et temporaires)
Contiennent des valeurs d’un certain type
Temporaires utilisés pour évaluer des expressions
Pas d’adresse machine explicitée par programmeur
Décision registre × mémoire → compilateur
2 / 35
Allocation de registres
Dans quel registre/case mémoire seront représentées les variables ?
Le générateur de code doit affecter des registres aux variables
Généralement, nb. de registres < nb. de variables
Générer des instructions de transfert mémoire ↔ registres
Attention : temporaires t0 , t1 6= registres $t0 . . .
Probablement, certaines variables (ou temporaires) ne seront
jamais en mémoire
3 / 35
Algorithmes d’allocation de registres
Assigner des registres : quelles variables en mémoire/registres ?
Allouer des registres : dans quels registres ?
Critères d’optimisation :
maximiser l’utilisation des registres
minimiser le nombre d’instructions de transfert
Cas général : NP-complet → heuristiques
Registres spéciaux ($fp, $sp, $ra)
4 / 35
Stratégie 0 : basique
Toutes les variables sont en mémoire
Tous les temporaires sont stockés sur la pile
MIPS est utilisé comme une “machine à pile”
Seulement 3 registres temporaires utilisés
5 / 35
Opérations
Opération dans l’arbre abstrait → instructions pour :
1
Dépiler les opérandes dans les registres $t1 et $t2
2
Calculer le résultat dans un registre $t0$
3
Empiler le registre $t0 au sommet de la pile
Rappel
Pour empiler un entier de 4 octets dans $t0 :
subi
sw
$sp, $sp, 4
$t0, 0($sp)
Pour dépiler,
lw
addi
$t0, 0($sp)
$sp, $sp, 4
6 / 35
Exemple
entier
entier
entier
main()
$a =
$b =
$c =
}
$a,
$b,
$c;
{
5;
$a + 1;
$b * 5 + 1;
li $t0, 5
subi
$sp, $sp,
sw $t0, 0($sp)
lw $t1, 0($sp)
addi
$sp, $sp,
sw $t1, a
lw $t1, a
subi
$sp, $sp,
sw $t1, 0($sp)
li $t0, 1
subi
$sp, $sp,
sw $t0, 0($sp)
lw $t1, 0($sp)
addi
$sp, $sp,
lw $t0, 0($sp)
addi
$sp, $sp,
add $t2, $t0, $t1
subi
$sp, $sp,
sw $t2, 0($sp)
lw $t1, 0($sp)
addi
$sp, $sp,
sw $t1, b
lw $t1, b
subi
$sp, $sp,
4
4
4
4
4
4
4
4
4
sw $t1, 0($sp)
li $t0, 5
subi
$sp, $sp,
sw $t0, 0($sp)
lw $t1, 0($sp)
addi
$sp, $sp,
lw $t0, 0($sp)
addi
$sp, $sp,
mult
$t0, $t1
mflo
$t2
subi
$sp, $sp,
sw $t2, 0($sp)
li $t0, 1
subi
$sp, $sp,
sw $t0, 0($sp)
lw $t1, 0($sp)
addi
$sp, $sp,
lw $t0, 0($sp)
addi
$sp, $sp,
add $t2, $t0, $t1
subi
$sp, $sp,
sw $t2, 0($sp)
lw $t1, 0($sp)
addi
$sp, $sp,
sw $t1, c
4
4
4
4
4
4
4
4
4
7 / 35
Avantages et inconvénients
Avantages
Simple et élégant à programmer
Pas besoin de plus de 3 registres
Traduction directe arbre abstrait → MIPS
Inconvénients
Ne maximise pas l’usage des registres disponibles
Ne minimise pas les transferts mémoire–registres
Code généré peu efficace
8 / 35
Solutions possibles
1
Générer du code avec la stratégie basique, puis optimiser :
Éliminer des instructions inutiles dans une fenêtre “peephole”
Exemple :
sw $t1, b
lw $t1, b
2
Générer le code en 2 étapes :
1
2
3
Générer du code machine abstrait, avec des temporaires
Assigner des registres (interférence entre variables)
Générer du code machine cible en parcourant le code machine
abstrait
9 / 35
Code à 3 adresses
Version abstraite du code machine (assembleur)
Chaque ligne correspond à une instruction et au plus 3
opérandes
Exemple :
a
b
t0
c
=
=
=
=
5
a+1
b∗5
t0 + 1
On génère des temporaires pour évaluer les expressions
On fait abstraction de certains aspects de l’implémentation :
Gestion de la pile dans les appels à fonction
Allocation de registres
Transferts mémoire ↔ registres
10 / 35
Instructions
Quantité limitée d’instructions qui imitent l’assembleur
opérations arithmétiques, de comparaison et logiques ;
lire, ecrire, exit ;
sauts : goto, si x relop y goto L ;
appels :
param pour déclarer un paramètre,
call pour appeler une fonction,
entree, sortie pour délimiter une fonction,
retour pour le retour de fonction.
11 / 35
Opérandes
1
Identificateurs du programme source (table des symboles)
2
Temporaires t0 , t1 , t2 générés pendant la traduction
3
Constantes
4
Étiquettes ou numéros de ligne du code (sauts)
12 / 35
Subtilités
Pas d’instruction load/store et gestion de mémoire
Interaction code 3 adresses ↔ table des symboles
Traduction en même temps que parcours de l’arbre abstrait
13 / 35
Exemple
$a = $b + f( 2 * $c );
t0
param
t1
a
= 2∗c
t0
= call f
= b + t1
14 / 35
Représentation : triplets
Instructions pour charger une variable/constante dans temporaire
Les opérandes se réfèrent aux numéros de ligne
ligne
0
1
2
3
4
5
6
7
op
load
loadimm
mult
param
appel
load
add
store
arg1
c
2
1
2
f
b
5
6
arg2
0
4
a
15 / 35
Stratégie 1 : assignation simple
Les registres sont un tableau de lignes.
int reg[NB_REGISTRES];
Chaque fois qu’une ligne renvoie un résultat, on demande un
nouveau registre, et on y stocke le résultat.
int nouveau_registre(int *dernier, int ligne);
Pour avoir accès à une ligne donnée, on parcourt le tableau
jusqu’à
int trouve_registre(int ligne);
16 / 35
Assignation simple
Comment savoir si une ligne donnée est utilisable ou non ?
On fait une première passe sur le code, pour savoir quel est la
dernière fois où la ligne est appelée.
0:
1:
2:
3:
4:
5:
loadimm
store
load
loadimm
si
load
0
6:
0, i
7:
i
8:
10
9:
2 < 3 goto 10 10:
i
11:
ligne
dernier appel
0
1
1
1
2
4
3
4
4
4
param
call
store
jump
load
ecrire
5
6
6
6
5
f
7, i
2
i
10
7
8
8
8
9
9
10
11
11
11
17 / 35
Implémentation
int *dernier_appel(){
int l, *tab = malloc(ligne * sizeof(int));
for (l=0; l<ligne; l++){
tab[l] = l;
switch(code[l].op){
case c3a_plus:
tab[code[l].arg1] = l;
tab[code[l].arg2] = l;
break;
case store:
tab[code[l].arg1] = l;
break;
...
}
return tab;
}
18 / 35
Implémentation
int nouveau_registre(int *dernier, int lig){
int r;
for (r=0; r<NB_REGISTRES; r++)
if (reg[r] == -1 || dernier[reg[r]] < lig) {
reg[r] = lig;
return r;
}
return -1;
}
19 / 35
Utilisation lors de la génération de code
void genere_mips(){
int l;
int *dernier = dernier_appel();
for (l=0; l<ligne; l++){
switch(code[l].op){
case c3a_plus:
printf("add\t$t%i, $t%i, $t%i\n",
nouveau_registre(dernier, l),
trouve_registre(code[l].arg1),
trouve_registre(code[l].arg2));
break;
...
20 / 35
Stratégie 2 : nombre d’Ershov
Quel est le nombre de registres nécessaires au calcul d’une
expression ?
delta := b*b - 4*(a*c)
*
b
*
b
*
4
a
c
21 / 35
Nombre d’Ershov
Le nombre d’Ershov est un attribut synthétisé (rappel : calculé de bas
en haut) ayant la définition suivante :
le nombre d’une feuille est 1 ;
le nombre d’un nœud à un seul fils est celui de ce fils ;
le nombre d’un nœud à deux fils ayant pour nombres n1 et n2 est
max(n1 , n2 ) si n1 6= n2 ,
n1 + 1 sinon.
*
b
*
b
*
4
a
c
C’est le nombre de registres nécessaire au calcul.
22 / 35
Nombre d’Ershov
Le nombre d’Ershov est un attribut synthétisé (rappel : calculé de bas
en haut) ayant la définition suivante :
le nombre d’une feuille est 1 ;
le nombre d’un nœud à un seul fils est celui de ce fils ;
le nombre d’un nœud à deux fils ayant pour nombres n1 et n2 est
max(n1 , n2 ) si n1 6= n2 ,
n1 + 1 sinon.
3
2
*
1
b
2
*
1
b
1
4
2
*
1
a
1
c
C’est le nombre de registres nécessaire au calcul.
22 / 35
Algorithme
Il suffit de calculer d’abord le sous-arbre de poids le plus grand.
$delta = $b * $b - 4 * ( $a * $c )
Solution naïve :
lw
lw
mult
mflo
li
lw
lw
mult
mflo
mult
mflo
sub
sw
$t0,
$t1,
$t0,
$t0
$t1,
$t2,
$t3,
$t2,
$t2
$t1,
$t1
$t0,
$t0,
b
b
$t1
4
a
c
$t3
$t2
$t0, $t1
delta
Version améliorée :
lw
lw
mult
mflo
lw
lw
mult
mflo
li
mult
mflo
sub
sw
$t0,
$t1,
$t0,
$t0
$t1,
$t2,
$t1,
$t1
$t2,
$t2,
$t1
$t0,
$t0,
b
b
$t1
a
c
$t2
4
$t1
$t0, $t1
delta
23 / 35
Pas assez de place
Le nombre de registre étant limité, il est parfois trop petit : il faut faire
des sauvegardes en mémoire.
3
2
*
1
b
2
*
1
b
1
4
2
*
1
a
1
c
Pour une expression donnée, la mémoire nécessaire au calcul d’une
expression est son nombre d’Ershov moins le nombre de registres
disponibles.
24 / 35
Pas assez de place
Avec seulement 2 registres disponibles, le code précédent devient
lw
lw
mult
mflo
sw
lw
lw
mult
mflo
li
mult
mflo
sw
sub
sw
$t0,
$t1,
$t0,
$t0
$t0,
$t0,
$t1,
$t0,
$t0
$t1,
$t0,
$t0
$t1,
$t0,
$t0,
b
b
$t1
tmp
a
c
$t1
4
$t1
tmp
$t1, $t0
delta
25 / 35
Stratégie 3 : réduire les accès mémoire
On se rend bien compte que les multiples opérations liées à la
mémoire sont souvent inutiles.
$a = 2;
$b = $a;
li
sw
lw
sw
$t0,
$t0,
$t0,
$t0,
2
a
a
b
26 / 35
Table d’adressage dynamique
Idée : associer un registre aux variables “courantes” et maintenir une
table d’adressage
multiple : la valeur est rangée à plusieurs endroits, on préfère la
récupérer dans un registres qu’en mémoire ;
dynamique : la table change à chaque ligne.
1
2
3
4
li
sw
lw
sw
$t0,
$t0,
$t0,
$t0,
2
a
a
b
Après la ligne 4 :
a
adresse(a), $t0
b
adresse(b)
27 / 35
Table d’adressage dynamique
On peut même abandonner temporairement l’adresse en mémoire :
1
2
3
$a = 2;
$b = $a;
$a = 3;
li
sw
li
sw
$t0,
$t0,
$t0,
$t0,
2
b
3
a
A la ligne 2, la table serait alors
a
$t0
b
adresse(b)
Comment réaliser cette optimisation automatiquement ?
28 / 35
Vie et mort des variables
Une variable est vivante d’une instruction i à une autre i0 si elle sert à
quelque chose dans i0
Vivante = peut servir plus tard.
Pour toute instruction i de la forme a=..., la variable a est morte
juste avant i.
Pour toute instruction i utilisant a en lecture, la variable a est
vivante juste avant i
Si une variable est vivante après i et que i ne l’a pas écrasée, alors
elle est vivante aussi avant i.
La vivacité se propage donc en arrière.
Si deux variables sont vivantes en même temps, on dit qu’elles
interfèrent.
29 / 35
Vie et mort des variables
Exemple :
instruction
b
=
2
c
=
b
b
= b+c
a
=
vivante
morte
c
écrire( a)
a, b, c
Tant qu’une valeur est vivante, on doit y avoir accès, de
préférence dans un registre.
On peut “oublier” d’écrire dans les variables mortes !
30 / 35
Vie et mort des variables
Exemple :
instruction
b
=
2
c
=
b
b
= b+c
a
=
vivante
morte
a
b, c
c
écrire( a)
a, b, c
Tant qu’une valeur est vivante, on doit y avoir accès, de
préférence dans un registre.
On peut “oublier” d’écrire dans les variables mortes !
30 / 35
Vie et mort des variables
Exemple :
instruction
b
=
2
c
=
b
b
= b+c
a
=
vivante
morte
c
a, b
a
b, c
c
écrire( a)
a, b, c
Tant qu’une valeur est vivante, on doit y avoir accès, de
préférence dans un registre.
On peut “oublier” d’écrire dans les variables mortes !
30 / 35
Vie et mort des variables
Exemple :
instruction
b
=
2
c
=
b
b
= b+c
a
=
vivante
morte
b, c
a
c
a, b
a
b, c
c
écrire( a)
a, b, c
Tant qu’une valeur est vivante, on doit y avoir accès, de
préférence dans un registre.
On peut “oublier” d’écrire dans les variables mortes !
30 / 35
Vie et mort des variables
Exemple :
instruction
b
=
2
c
=
b
b
= b+c
a
=
vivante
morte
b
a, c
b, c
a
c
a, b
a
b, c
c
écrire( a)
a, b, c
Tant qu’une valeur est vivante, on doit y avoir accès, de
préférence dans un registre.
On peut “oublier” d’écrire dans les variables mortes !
30 / 35
Vie et mort des variables
Exemple :
instruction
b
=
2
c
=
b
b
= b+c
a
=
vivante
morte
a, b, c
b
a, c
b, c
a
c
a, b
a
b, c
c
écrire( a)
a, b, c
Tant qu’une valeur est vivante, on doit y avoir accès, de
préférence dans un registre.
On peut “oublier” d’écrire dans les variables mortes !
30 / 35
Vie et mort des variables
Exemple :
instruction
b
=
2
c
=
b
b
= b+c
a
=
vivante
morte
a, b, c
b
a, c
b, c
a
c
a, b
a
b, c
c
écrire( a)
a, b, c
Tant qu’une valeur est vivante, on doit y avoir accès, de
préférence dans un registre.
On peut “oublier” d’écrire dans les variables mortes !
30 / 35
Découpage en blocs
Un bloc de base dans un code à trois adresses est une suite
d’instructions
1
sans saut ni branchement :
on ne peut en sortir qu’à la fin,
2
sans point d’accroche :
on ne peut y rentrer qu’au début.
31 / 35
Graphe de flot
On relie les blocs de base entre eux en regardant de façon naturelle où
ils peuvent mener.
0:
1:
2:
3:
4:
5:
6:
7:
8:
c = c+1
a = b+c
si b > 10 goto 6
c = b−1
si a 6= b goto 0
goto 7
b=a
a=b
écrire( a)
0:
1:
2:
3:
4:
c = c+1
a = b+c
si b > 10 goto 6
c = b−1
si c 6= b goto 0
5 :goto 7
7:
8:
6 :b = a
a=b
écrire( a)
32 / 35
Graphe de flot
On relie les blocs de base entre eux en regardant de façon naturelle où
ils peuvent mener.
0:
1:
2:
3:
4:
5:
6:
7:
8:
c = c+1
a = b+c
si b > 10 goto 6
c = b−1
si a 6= b goto 0
goto 7
b=a
a=b
écrire( a)
0:
1:
2:
3:
4:
c = c+1
a = b+c
si b > 10 goto 6
c = b−1
si c 6= b goto 0
5 :goto 7
7:
8:
6 :b = a
a=b
écrire( a)
32 / 35
Du flot aux interférences
c = c+1
a = b+c
si b > 10 goto 6
c = b−1
si c 6= b goto 0
goto 7
b=a
a=b
écrire( a)
On note que a et c ne sont jamais vivantes en même temps : elles
n’interfèrent pas.
On crée un nouveau graphe, dont les sommets sont les variables, où
les arêtes signifient une interférence.
a
b
c
33 / 35
Allocation
Le problème d’attribuer des registres à chaque variable de façon
optimale devient donc un problème de graphe bien connu : il s’agit
de colorier le graphe d’interférence avec un certain nombre de
registres.
on cherche à minimiser le nombre de couleurs (registres),
deux éléments connectés ne peuvent pas avoir la même couleur.
$t0
$t1
a
b
$t0
c
34 / 35
Allocation
Le problème d’attribuer des registres à chaque variable de façon
optimale devient donc un problème de graphe bien connu : il s’agit
de colorier le graphe d’interférence avec un certain nombre de
registres.
on cherche à minimiser le nombre de couleurs (registres),
deux éléments connectés ne peuvent pas avoir la même couleur.
$t0
$t1
a
b
$t0
c
Ce problème est
complexe (NP-complet) dans le cas général,
mais il existe de bonnes approximations.
34 / 35
Allocation
c = c+1
a = b+c
si b > 10 goto 6
c = b−1
si c 6= b goto 0
goto 7
j0: lw
lw
addi
add
li
bgt
$t0,
$t1,
$t0,
$t0,
$t2,
$t1,
c
b
$t0, 1
$t1, $t0
10
$t2, j5
b=a
a=b
écrire( a)
addi
bne
j
j6: move
j7: move
...
$t0,
$t0,
j7
$t1,
$t0,
$t1, -1
$t1, j0
$t0
$t1
$t1 vaut toujours b,
$t0 vaut a ou c, selon le contexte.
35 / 35
Téléchargement