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