Vous êtes sur la page 1sur 131

Cours de Compilation, Master 1, 2004-2005

Tanguy Risset
2 mars 2005

Table des mati`eres


1 Generalites
1.1 Biblio . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Quest ce quun compilateur ? . . . . . . . . . . .
1.3 Un exemple simple . . . . . . . . . . . . . . . . .
1.3.1 front-end . . . . . . . . . . . . . . . . . . .
1.3.2 Creation de lenvironnement dexecution
1.3.3 Ameliorer le code . . . . . . . . . . . . . .
1.3.4 La generation de code . . . . . . . . . . .
1.4 assembleur Iloc . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

4
4
4
5
6
6
7
7
8

2 Grammaires et expressions reguli`eres


2.1 Quelques definitions . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 De lexpression reguli`ere a` lautomate minimal . . . . . . . . . . . .
2.2.1 Des expressions reguli`eres aux automates non deterministes
2.2.2 Determinisation . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2.3 Minimisation . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Implementation de lanalyseur lexical . . . . . . . . . . . . . . . . .
2.3.1 Analyseur lexical a` base de table . . . . . . . . . . . . . . . .
2.3.2 Analyseur lexical code directement . . . . . . . . . . . . . .
2.4 Pour aller plus loin . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

9
9
11
11
12
14
15
15
16
16

3 Analyse syntaxique
3.1 Grammaires hors contexte . . . . . . . . . .
3.2 Analyse syntaxique descendante . . . . . .
3.2.1 Exemple danalyse descendante . .

3.2.2 Elimination
de la recursion a` gauche

3.2.3 Elimination
du bactrack . . . . . . .
3.2.4 Parser descendant recursif . . . . . .
3.3 Parsing ascendant . . . . . . . . . . . . . . .
3.3.1 Detection des reductions candidates
3.3.2 Construction des tables LR(1) . . .
3.4 Quelques conseils pratiques . . . . . . . . .
3.5 Resume sur lanalyse lexicale/syntaxique .

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.

17
17
20
21
22
22
25
26
27
28
30
32

4 Analyse sensible au contexte


4.1 Introduction aux syst`emes de type . . . . .
4.1.1 Composant dun syst`eme de typage
4.2 Les grammaires a` attributs . . . . . . . . . .
4.3 Traduction ad-hoc dirigee par la syntaxe . .
4.4 Implementation . . . . . . . . . . . . . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

33
33
33
34
36
37

5 Representations Intermediaires
5.1 Graphes . . . . . . . . . . . . . . . . . . . .
5.2 IR lineaires . . . . . . . . . . . . . . . . . . .
5.2.1 code trois adresses . . . . . . . . . .
5.3 Static Single Assignment . . . . . . . . . . .
5.4 Nommage des valeurs et mod`ele memoire
5.5 La table des symboles . . . . . . . . . . . . .

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

38
38
39
40
40
41
42

6 Procedures
et espace des noms .
6.1 Abstraction du controle
6.2 Enregistrement dactivation (AR) . . . . . . .
6.3 Communication des valeurs entre procedures
6.4 Adressabilite . . . . . . . . . . . . . . . . . . .

6.5 Edition
de lien . . . . . . . . . . . . . . . . . .

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

.
.
.
.
.

44
44
45
47
48
50

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

6.6

Gestion memoire globale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

7 Implementations de mecanismes specifiques


7.1 Stockage des valeurs . . . . . . . . . . . .
7.2 Expressions booleennes et relationnelles .
de flot . . . . . . .
7.3 Operations de controle
7.4 Tableaux . . . . . . . . . . . . . . . . . . .
7.5 Chanes de caract`eres . . . . . . . . . . . .
7.6 Structures . . . . . . . . . . . . . . . . . .

51

.
.
.
.
.
.

57
57
57
59
61
62
63

8 Implementation des langages objet


8.1 Espace des noms dans les langages objets . . . . . . . . . . . . . . . . . . . . . . . .
8.2 Generation de code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

65
65
67

9 Generation de code : selection dinstructions


9.1 Selection dinstruction par parcours darbre (BURS) . . . . . . . . . . . . . . . . . . .
9.2 Selection dinstruction par fenetrage . . . . . . . . . . . . . . . . . . . . . . . . . . .

70
71
77

10 Introduction a` loptimisation : e limination de redondances

10.1 Elimination
dexpressions redondantes avec un AST . . . . . . . . . . . . . . . . . .
10.2 Valeurs numerotees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.3 Au del`a dun bloc de base . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

80
80
82
83

11 Analyse flot de donnees


11.1 Exemple des variables vivantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11.2 Formalisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11.3 Autres probl`emes data-flow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89
89
91
93

12 Static Single Assignment


12.1 Passage en SSA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

95
95

13 Quelques transformations de code

13.1 Elimination
de code mort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13.2 Strength reduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

97
97
99

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

.
.
.
.
.
.

14 Ordonnancement dinstructions
104
14.1 List scheduling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
14.2 Ordonnancement par region . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
14.3 Ordonnancement de boucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
15 Allocation de registres
110
15.1 Allocation de registres locale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
15.2 Allocation globale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
A Operation de lassembleur lineaire Iloc

117

B Assembleur, e diteur de liens, lexemple du Mips


B.1 Assembleur . . . . . . . . . . . . . . . . . . .

B.2 Edition
de lien . . . . . . . . . . . . . . . . . .
B.3 Chargement . . . . . . . . . . . . . . . . . . .
B.4 Organisation de la memoire . . . . . . . . . .
B.5 Conventions pour lappel de procedure . . .
B.6 Exceptions et Interruptions . . . . . . . . . .
B.7 Input/Output . . . . . . . . . . . . . . . . . .
B.8 Quelques commandes utiles . . . . . . . . . .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

119
120
121
122
122
123
124
126
128

1 Generalites
Page web du cours :
http://perso.ens-lyon.fr/tanguy.risset/cours/compil2004.html

1.1 Biblio
La construction dun compilateur rassemble une grande diversite de processus. Certains e tant
une application directe de resultats theoriques puissants (comme la generation automatique danalyseurs lexicaux qui utilise la theorie des langages reguliers). Dautres se rattachent a` des probl`emes
difficiles, NP-Complet (ordonnancement dinstruction, allocation de registres). Ce qui est probablement le plus delicat dans la construction dun compilateur cest quil faut trouver un e quilibre
entre un code produit efficace et un temps de production de code raisonnable.
Il est tr`es important de realiser que le fait de matriser les techniques de construction dun
compilateur sont une des raisons qui rendent les informaticiens indispensables. Un biologiste
peut savoir bien programmer, mais lorsquil aura a` faire des traductions entre differents formats,
il va utiliser des methodes ad-hoc. Linformaticien peut apporter e normement a` une e quipe par
la matrise de cet outil tr`es puissant quest le compilateur.
Le but de ce cours est de faire assimiler les techniques de bases de compilation pour
1. pouvoir rapidement e crire un compilateur : utiliser les techniques et outils ayant fait leurs
preuves.
2. avoir assimiles les notions qui permettent de comprendre les probl`emes et les solutions dans
les compilateurs modernes.
Ce cours a e te prepare a` partir du livre de Keith D. Cooper et Linda Torczon de Rice University : Engineering a Compiler [CT03]. Il est aussi issu du cours de compilation fait par Yves
Robert jusquen 2003. Parmi les ouvrages importants qui peuvent completer ce cours je recommande . Dautre part, une bonne connaissance de larchitecture des processeurs peut e tre utile
si lon decide dapprofondir un peu la compilation. La reference la plus accessible est le livre de
Hennessy et Patterson [HP98]

References
[ASU88] Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman. Compilers : Principles, Techniques and
Tools. Addison-Wesley, 1988. La bible, un peu depasse sur certains sujets, mais beaucoup
nont pas change depuis.
[CT03]

Keith D. Cooper and Linda Torczon. Engineering a Compiler. Morgan-Kaufmann, 2003.


Le livre utilise pour ce cours, en cours de commande a` lENS.

[Muc]

Steven S. Muchnick. Compiler Design Implementation. Morgan-Kaufmann. Tr`es complet


pour approfondir les optimisations.

[wA98] Andrew w. Appel. Modern Compiler implementation in Java. Cambridge University press,
1998. Certaines parties tr`es bien expliquees
[GBJL00] D. Grune, H. Bal, J. H. Jacobs, and K. Langendoen. Modern Compiler Design. John Wiley
& Sons, 2000. Globalement un peu moins bien (sauf pour certains points precis).
[HP98]

J. Hennessy, D. Patterson Computer Organization and Design : The hardware software interface Morgan Kaufman 1998. Reference pour la description des architectures Risc.

1.2 Quest ce quun compilateur ?


Definition 1 Un compilateur est un programme qui prend en entree un programme executable et produit
en sortie un autre programme executable.

Le langage du programme source est appele langage source, celui du programme cible est
appele langage cible. Exemple de langage sources : Fortran, C, Java etc... Le langage cible peut
e tre un de ces langages ou un assembleur ou un code machine pour une machine precise.
Beaucoup de compilateurs ont le langage C comme langage cible car il existe des compilateurs
C sur pratiquement toutes les plate-formes. Un programme qui gen`ere du postcript a` partir dun
format donnee (ascii, latex, jpg, etc.) est aussi un compilateur. En revanche, un programme qui
transforme du postcript en des pixel sur un e cran est un interpreteur.
programme
source

Compilateur

programme

programme

source

cible

rsultats
Interpreteur

Exemple de langage interprete : Perl, Scheme, Mathematica, mapple.


Les principes fondamentaux de la compilation sont :
1. Le compilateur doit conserver le sens du programme compile
2. Le compilateur doit ameliorer le code
les proprietes importantes dun compilateur sont :
code produit efficace (rapidite, memoire).
informations retournees en cas derreurs, debboging
rapidite de la compilation
Dans les annees 80, les compilateurs e taient tous de gros syst`emes monolithiques generant du
code assembleur le plus rapide possible directement a` partir dun programme entier. Aujourdhui
entrent en jeux pour juger de la qualite dun compilateur : la taille de
dautres fonctions de cout
code et la consommation e lectrique ont une importance primordiale pour les syst`emes embarques
(pour les applications multimedia recentes, la taille de code est quasiment le crit`ere numero 1).
Le processus de compilation peut aussi e tre decompose : comme le bytecode java qui est
un programme semi-compile. Lediteur de liens, reliant le programme compile avec les librairies precompilees quil utilise peut e ventuellement faire des optimisations. Le programme peut
meme e tre ameliore a` lexecution avant detre execute grace a` des informations supplementaires
presentes a` lexecution.

1.3 Un exemple simple


Considerons lexpression suivante pour laquelle nous voulons generer du code executable :
w w2xyz
On decompose aujourdhui un compilateur en 3 passes (front-end, middle-end, back-end) bien
quil y ait beaucoup plus de trois passes en pratique. En plus des actions de chaque passe, il
est important de definir precisement les representations intermediaires (intermediate representation,
IR ) utilis
ees ainsi que linfrastructure du compilateur pour manipuler ces representions. Une
representation intuitive est la suivante :

Infrastructure
Table des symbols, Arbres de syntaxe abstraite, Graphes, ensembles, IR, ...

Compilateur

Ordonnancement

Alloc. Reg.

Selection

Opt3

BackEnd

Opt2

Opt1

Parsing

C.S.A

MiddleEnd

Scanning

FrontEnd

On appelle cela un compilateur optimisant (optimizing compiler) par opposition aux premier
compilateurs qui ne comprenaient que deux passes (front-end, back end).
1.3.1 front-end
La premi`ere tache du compilateur est de comprendre lexpression du langage source. Cela se
fait en plusieurs e tapes :
1. Analyse syntaxique : permet de verifier que lexpression est bien une phrase du langage
source syntaxiquement correcte, on dit aussi quelle est bien formee. Cela necessite donc une
definition formelle du langage source. Exemple en francais : Le lion mange de la viande est
syntaxiquement correcte et le lion viande nest pas syntaxiquement correcte. En pratique,
lanalyse cette phase est divisee en deux traitements : lanalyse lexicale ou scanning (reperer
les cesures de mots, la ponctuation) et lanalyse syntaxique ou parsing (verifier les r`egles de
grammaire pour lexemple du francais).
2. Analyse semantique : permet de verifier que lexpression a un sens dans le langage source
(on peut dire aussi analyse sensible au contexte, context sensitive analysis, CSC en anglais).
Cela necessite une semantique precise pour le langage source. Exemple en Francais : le lion
dort de la viande est syntaxiquement correcte (sujet, verbe, complement dobjet) mais na pas
de sens defini. Ici, on peut e tre amene a` se demander si les variables w, x, y et z ont e te
declarees, et si on leur a affecte des valeurs precedemment.
Ces traitements sont regroupe dans ce qui est appele le front-end du compilateur. Ces deux traitements sont largement automatises aujourdhui grace a` lapplication des resultats de la theorie
des langages. On dispose doutils permettant de generer les analyzeurs lexicauxs et syntaxiques
a` partir de la description du langage source sous forme de grammaire.
1.3.2 Creation de lenvironnement dexecution
Le langage source est generalement une abstraction permettant dexprimer un calcul dans un
formalisme relativement intuitif pour letre humain. Par exemple, il contient des noms symboliques : x, y, z, w qui represente plus que des valeurs : des cases memoire qui peuvent prendre
plusieurs valeurs successivement.
Le compilateur doit detruire cette abstraction, faire un choix pour implementer la structure
et les actions designees et maintenir la coherence avec les actions qui suivent. Ici, par exemple le
compilateur peut choisir dallouer quatre cases memoire consecutives pour les quatre variables
w, x, y, z :
0
w x y z
,
ou il peut decider de conserver ces variables dans des registres par une serie daffection :
r1 w; r2 x; r3 y; r4 z
Dans tous les cas, le compilateur doit assurer la coherence de ces choix tout au long du processus
de traduction.
En plus des noms le compilateur doit creer et maintenir des mecanismes pour les procedures,
du flot. On appelle cela choisir la
les param`etres, les portees lexicales les operations de controle
forme du code.
Ce point precis est assez important : cest parce que lon a e tudie le processus de compilation
vers une architecture cible dun type tr`es precis : type architecture de Von Neumann sequentielle
avec un processeur programmable (muni de plusieurs unites) et une memoire (eventuellement
hierarchisee) que lon a pu automatiser cette descente dans le niveau dabstraction. Aujourdhui,
il nexiste toujours par de paralleliseur satisfaisant pour une machine parall`ele donnee (cest
dailleurs une des raisons pour laquelle les constructeurs ne fabriquent quasiment plus de machines parall`eles) car le raffinement efficace de labstraction pour ces architectures est beaucoup
plus delicat (les communications posent un e norme probl`eme de performance). De meme on
ne sait pas compiler efficacement un circuit integre a` partir dune specification de haut niveau
car pour beaucoup dabstraction faites par les langages de haut niveau les choix a` faire pour
6

limplementation sont tr`es difficiles. Par exemple lintroduction du temps a` partir dune specification
fonctionnelle (ordonnancement), la parallelisation etc.
1.3.3 Ameliorer le code
Le compilateur peut tr`es souvent tirer partie du contexte dans lequel se trouve une expression
pour optimiser son implementation. Par exemple, si lexpression a` compiler se trouve a` linterieur
dune boucle dans laquelle la sous expression 2 x y est invariante, le compilateur aura interet
a` introduire une variable temporaire pour sortir le calcul de cette expression de la boucle :
w <- 1
w <- 1
t1 <-2 * x * y
for i=1 to n
for i=1 to n
read z
read z
w <- w * 2 * x * y * z
w <- w * t1 * z
end
end
Cette phase de la compilation est generalement separee en des passes danalyse et des passes
de transformation. Les analyses vont permettre de mettre en place les objets utilises lors des transformations. Cette phase est quelque fois appelee middle-end.
Lanalyse data flow permet de raisonner au moment de la compilation sur la mani`ere dont les
valeurs seront transmises a` lexecution. Lanalyse de dependance sert a` rendre non ambigues les
references a` des e lements de tableaux.
Nous verrons quelques transformations mais il en existe e normement, toutes ne peuvent e tre
mentionnees ici.
1.3.4 La generation de code
Cest la partie qui diff`ere davec les interpreteurs. Le compilateur traverse la structure de
donnees qui represente le code et e met un code e quivalent dans le langage cible. Il doit choisir les instructions a` utiliser pour implementer chaque operation (selection dinstruction), decider
quand et ou` copier les valeurs entre les registres et la memoire (allocation de registre), choisir
un ordre dexecution pour les instructions choisies (ordonnancement dinstructions). Souvent, les
optimisations de ces differentes phases entrent en conflit. Cette phase est aussi appele back end.
Deux mani`eres de generer le code pour lexpression
w w2xyz
loadAI
loadI
loadAI
loadAI
loadAI
mult
mult
mult
mult
storeAI

rarp , @w
2
rarp , @x
rarp , @y
rarp , @z
rw , r2
rw , rx
rw , ry
rw , rz
rw

rw
r2
rx
ry
rz
rw
rw
rw
rw
rarp , @w

// load w
// la constante 2 dans r2
// load x
// load y
// load z
// rw w2
// rw (w2) x
// rw (w2) x y
// rw (w2) x y z
// e criture de w

Le code ci dessous utilise deux registres au lieu de 5 :

loadAI
add
loadAI
mult
loadAI
mult
loadAI
mult
storeAI

rarp , @w
r1 , r1
rarp , @x
r1 , r2
rarp , @y
r1 , r2
rarp , @z
r1 , r2
r1

r1
r1
r2
r1
r2
r1
r2
r1
rarp , @w

// load w
// r1 w 2
// load x
// r1 (w2) x
// load y
// rw (w2) x y
// load z
// r1 (w2) x y z
// e criture de w

1.4 assembleur Iloc


Un compilateur est e troitement lie a` lassembleur quil va generer (au moins pour les phases
du back-end). Il existe autant dassembleur que de type de processeurs, cependant on peut degager
des principes communs. Pour illustrer le cours on utilisera le langage assembleur Iloc propose
dans [CT03]. ILOC est un code lineaire assembleur pour une machine RISC. Cest une version
simplifiee de la representation intermediare utilisee dans le MSCP (Massively Scalar Compiler
Project),developpe a` luniversite de Rice. Iloc est detaille en Annexe A (page 117). Des exemples
dassembleur reel (Pentium et MIPS) sont visibles en pages 130 et 131.
ILOC na quun type de nombre : les entiers (pas de double, flottant etc.). La machine abstraite
sur laquelle ILOC sexecute poss`ede un nombre illimite de registres. Cest un code trois adresses.
Il supporte simplement les modes dadressage e lementaires :
adressage direct.
load r1 r2 : charger dans r2 le contenu de la case dont ladresse est contenue dans r1 ,
chargement.
loadI 2 r2 : charger dans r2 la valeur 2.
adressage indirect Address-Immediate.
loadAI r1 , 2 r2 : charger dans r2 le contenu de la case dont ladresse est contenu de
r1 +2.
adressage indirect indexe Adress-Offset.
loadAO r1 , r2 r3 : charger dans r3 le contenu de la case dont ladresse est contenu de
r1 +contenu de r2 .1
Chaque instruction ILOC peut e tre precedee dun label qui peut e tre reference par les instructions de branchement (jump,cbr). Le registre r0 designe par defaut lenregistrement dactivation
(activation record : AR) de la procedure courante. Les decalage en memoire par rapport a` cet AR
du flot
pour illustrer deux mani`eres
sont notes : @x. Il y a deux types dinstruction de controle
dimplementer cela au niveau de la generation de code.

1 demander

le nom des differents adressage en francais

2 Grammaires et expressions reguli`eres


2.1 Quelques definitions
Nous allons survoler quelques definitions importantes (langage regulier, grammaire, automate etc.). Ces notions sont utilisees dans beaucoup de domaines de linformatique, elles sont
approfondies dans le cours de Marianne Delorme (automates).
automates finis On cherche a` reconnatre un langage cest a` dire a` indiquer si la phrase que lon
a lu appartient bien syntaxiquement au langage ou pas. Commencons par la tache simple qui
consiste a` reconnatre quelques mots particuliers. Par exemple, nous souhaitons reconnatre le
mot fee. La mani`ere la plus naturelle est de lire les lettres les unes apr`es les autres et daccepter
le mot une fois que lon a bien lu fee. On passe alors par quatre etats : lorsquon a rien lu,
lorsquon a lu f, lorsquon a lu fe puis lorsquon a lu fee. On peut representer cela par le diagramme :

s0

S1

S2

S3

Sur ce dessin, letat initial est s0 , s3 est letat final signifiant quon a reconnut le mot fee. Si on
est en train de lire un autre mot, une des transitions ne pourra pas seffectuer, on rejettera le mot.
Le diagramme ci-dessus est la representation dun automate fini qui reconnat le mot fee. Si lon
veut reconnatre deux mots, par exemple fee et feu, on peut utiliser lautomate suivant :
f
s0

e
S1

e
S2

S3
u

S4

Plus formellement, on peut definir la notion dautomate fini :


Definition 2 Un automate fini deterministe est donne par (S, , , s0 , SF ) ou :
S est un ensemble fini detats ;
est un alphabet fini ;
: S S est la fonction de transition ;
s0 est letat initial ;
SF est lensemble des etats finaux

Dans notre exemple de lautomate reconnaissant fee, on a S = {s0 , s1 , s2 , s3 }, = {f, e} (ou


tout lalphabet) = {(s0 , f ) s1 , (s1 , e) s2 , (s2 , e) s3 }. Il y a en fait un e tat implicite
derreur (se ) vers lequel vont toutes les transitions qui ne sont pas definies.
Un automate accepte une chane de caract`ere si et seulement si en demarrant de s0 , les caract`eres successifs de la chane entranent une succession de transitions qui am`ene lautomate
dans un e tat final. Si la chane x est composee des caract`eres x1 x2 . . . xn , on doit avoir :
((. . . ((s0 , x1 ), x2 ), x3 ) . . . , xn1 ), xn ) SF
Par definition, on peut avoir des circuits dans le diagramme de transition dun automate, cest
a` dire que lon peut reconnatre des mots de longueur arbitraire, par exemple, lautomate suivant
9

reconnat nimporte quel entier :


09

19
s0

S1
0

S2

Les automates que nous avons vus jusqu`a present sont tous deterministes, cest a` dire que lorsquils sont dans un e tat particulier si et quils lisent un caract`ere ci la donnee sj = (si , ci ) definit
exactement letat suivant. Nous allons avoir besoin dautomates indeterministes, cest a` dire dautomates qui, e tant dans un e tat si et lisant un caract`ere ci pourront atteindre plusieurs e tats : sj1 ,
ou sj2 , . . ., ou sj3 . Il y deux mani`eres de representer cela sur les diagrammes de transition : soit
on autorise les -transitions, cest a` dire des transitions du type : (si , ) = sj ou` est la chane
vide (i.e. lautomate peut changer detat sans lire de caract`ere), soit on utilise plusieurs transitions
e tiquetees par un meme caract`ere a` partir dun meme e tat (cest a` dire que devient une fonction
de S 2S , qui retourne un ensemble detats). En pratique, on fait les deux.
Nous avons alors besoin dune nouvelle definition du fonctionnement dun automate (ou de la
notion daccepter). Un automate non deterministe (S, , , s0 , SF ) accepte une chane de caract`ere
x0 x1 . . . xn si et seulement si il existe un chemin dans le diagramme de transition qui demarre a`
s0 , et finit a` sk SF et tel que les e tiquettes des arcs e pellent toute la chane (sachant que les arcs
e tiquetes par ne comptent pas).
Par exemple : lautomate suivant reconnat un mot dun nombre quelconque (eventuellement
nul) de a ou un mot dun nombre quelconque strictement positif de a suivi dun b.
a

s0

b
S2

S1

S3

On peut demontrer que les automates deterministes et non deterministes sont e quivalents en
terme de langage reconnus (on verra sur un exemple comment determiniser un automate).
expressions reguli`eres Une expression reguli`ere est une formule close permettant de designer
un ensemble de chanes de caract`eres construites a` partir dun alphabet (eventuellement augmente de la chane vide ). On appelle cet ensemble de chane de caract`ere un langage.
Une expression reguli`ere est construite a` partir des lettres de lalphabet (un lettre dans une
expression reguli`ere designe lensemble reduit a` la chane de caract`ere e gale au caract`ere en question) et des trois operations de base sur les ensembles de chanes de caract`eres :
lunion de deux ensembles, notee R | S est {s | s R ou s S}.
la concatenation de deux ensembles R et S notee RS est {rs | r R et s S}. On note Ri
pour RRR . . . R i fois.
la fermeture transitive (ou e toile, ou fermeture de kleene) dun ensemble de chanes de
i
caract`eres R notee R est
enation de R avec lui meme
i=0 R . Cest donc lunion des concat
zero fois (cest a` dire la chane vide ) ou plus.
Le langage (ou ensemble) represente par une expression reguli`ere r est note L(r). On utilise
aussi les parenth`eses dans les expressions reguli`eres pour exprimer la precedence (priorite de
precedence : fermeture > concatenation > union). Enfin, on utilise des raccourcis de notation : par
exemple [0..4] e quivaut a` 0|1|2|3|4, cest a` dire lensemble constitue des cinq chanes { 0 , 1 , 2 , 3 , 4 }.
On utilise ce formalisme pour specifier les langages de programmation que nous allons compiler. Par exemple, les identificateurs dans beaucoup de langages actuels sont definis comme
commencant par une lettre suivi de lettres ou de chiffres. On peut decrire cet ensemble de chanes
par lexpression reguli`ere : [a..z]([a..z]|[0..9]) .
Pour les entiers : 0 | [1..9][0..9]
Pour les reels (+ | | )(0 | [1..9][0..9] )(.[0..9] | )E(+ | | )(0 | [1..9][0..9] )
Lensemble des langages qui peuvent e tre exprimes par des expressions reguli`eres est appele

10

lensemble des langages reguliers. Ces langages ont de bonnes proprietes qui les rendent en particulier aptes a` e tre reconnus par des analyseurs generes automatiquement. Une propriete importante
est que ces langages sont clos par les operations dunion, concatenation et fermeture de kleene ce
qui permet de construire des analyseurs incrementalement. On peut montrer (on va le voir sur un
exemple) que les langages reguliers sont exactement les langages qui peuvent e tre reconnus par
un automate fini.

2.2 De lexpression reguli`ere a` lautomate minimal


La theorie des langages reguliers permet de generer automatique un analyseur syntaxique a`
partir dune expression reguli`ere. Le schema global est le suivant :
Construction de Kleene
code pour analyseur

Expressions
Rgulires

Automates
finis
dterministes

Minimization

Construction
de Thompson
Automates
finis
nondterministes

Determinisation

2.2.1 Des expressions reguli`eres aux automates non deterministes


Pour e tre capable de construire un automate qui reconnaisse un langage regulier quelconque,
il suffit davoir un mecanisme qui reconnat chaque lettre, puis un mecanisme qui reconnat la
concatenation, lunion et la fermeture de langages reconnus.
Nous allons illustrer ce processus sur un exemple simple, il est e lementaire a` generaliser.
Considerons le langage designe par lexpression reguli`ere : a(b | c) . Le parenthesage indique
lordre dans lequel on doit decomposer lexpression ; il faut reconnatre dans lordre : les langages
b et c puis (b | c) puis (b|c) puis a puis a(b|c) .

11

s0

S1

s2

s3

s4

S1

b
s2

s3

s7

s6

c
s4

s5

NFA pour b|c

b
s2

s3

S6

s6

s8

c
s4

s9

s5

NFA pour (b|c)*

b
s2

a
s0

s1

s3

s8

S6

s6

c
s4

s5

s9

NFA pour a(b|c)*

Notons que ce langage particulier aurait e te reconnu par un automate beaucoup plus simple :
b

a
s0

S1

La suite du processus permettra de simplifier lautomate obtenu jusqu`a obtenir lautomate cidessus.
2.2.2 Determinisation
On veut maintenant obtenir un automate deterministe (D, , D , d0 , DF ) a` partir dun automate non-deterministe (N, , N , n0 , NF ). La clef est de deriver D et D . reste le meme. Lalgorithme utilise est appele la construction de sous-ensembles (subset construction). Lidee cest que
les e tats du nouvel automate sont des ensembles detats de lancien automate. Un nouvel e tat
qi = {n1 , . . . , nk } contient precisement lensemble des anciens e tats pouvant e tre atteints en lisant
un caract`ere particulier depuis un e tat qj . Lalgorithme utilise pour construire le nouvel automate
est le suivant :
q0 -fermeture(n0 )
initialiser Q avec q0
WorkList q0
Tant que (WorkList = )
choisir qi dans WorkList
12

pour chaque caract`ere c


q -fermeture((qi , c))
D [qi , c] q
si q
/ Q alors ajouter q a` WorkList ;
ajouter q a` Q ;
L -fermeture dun ensemble detats ajoute a` cet ensemble detat tous les e tats pouvant e tre
atteint par des -transitions a` partir des e tats de lensemble. Loperateur (qi , c) calcule lensemble
des e tats atteignables en lisant c depuis les e tats nij de qi : si qi = {ni1 , . . . , nik } alors (qi , c) =
nij qi N (nij , c)
Appliquons cet algorithme sur lautomate non-deterministe obtenu (on a renomme les e tats).

a
n4

a
n0

n1

n5

n2

n8

n3

a
n6

n7

n9

1. l -fermeture de n0 donne letat q0 = {n0 }.


2. la premi`ere iteration de la boucle while calcule :
(q0 , a) = {n1 } ; -fermeture({n1 })={n1 , n2 , n3 , n4 , n6 , n9 } = q1
(q0 , b) =
(q0 , c) =
3. la deuxi`eme iteration de la boucle while calcule :
(q1 , a) =
(q1 , b) = {n5 } ; -fermeture({n5 })={n5 , n8 , n9 , n3 , n4 , n6 } = q2
(q1 , c) = {n7 } ; -fermeture({n7 })={n7 , n8 , n9 , n3 , n4 , n6 } = q3
4. les iterations suivantes retomberont sur les memes e tats q2 et q3 , lalgorithme sarrete en 4
iterations et produit lautomate suivant :
.
b

a
q2
b
a
q0

q1

q3

On peut montrer que cette methode pour obtenir des automates deterministes reconnaissant
une expression reguli`ere produit des automates avec beaucoup detats (Q peut avoir 2|N | e tats)
mais naugmente pas le nombre de transitions necessaires pour reconnatre un mot.
remarque : algorithme de point fixe Lalgorithme que nous venons de voir est un bon exemple
dalgorithmes de point fixe qui sont tr`es largement utilises en informatique. La caracteristique de
tels algorithmes est quils appliquent une fonction monotone (f (x) x) a` une collection densembles choisis dans un domaine ayant une certaine structure. Ces algorithmes terminent lorsque
les iterations suivantes de la fonction ne modifient plus le resultat, on parle de point fixe atteint.
13

La terminaison de tels algorithmes depend des proprietes du domaine choisi. Dans notre
exemple, on sait que chaque qi Q est aussi dans 2N , donc ils sont en nombre fini. Le corps de la
boucle while est monotone car lensemble Q ne peut que grossir. Donc forcement, au bout dun
moment, Q sarretera de grossir. Lorsque Q ne grossit plus, lagorithme sarrete en |Q| iterations
au maximum, donc lalgorithme presente sarrete bien.
On calcule l -fermeture par un autre algorithme point fixe.
2.2.3 Minimisation
Le grand nombre detats de lautomate resultant de la determinisation est un probl`eme pour
limplementation. La minimisation permet de regrouper les e tats e quivalents, cest a` dire qui ont
le meme comportement sur les memes entrees. Lalgorithme de minimisation construit une partition de lensemble D des e tats de lautomate deterministe, chaque e tat e tant dans le meme sousensemble a le meme comportement sur les memes entrees. Lalgorithme est le suivant :
P {DF , D DF }
tant que (P change)
T
pour chaque ensemble p P
T T Partition(p)
P T

Partition(p)
pour chaque c
si c separe p en {p1 , . . . , pk }
alors Return({p1 , . . . , pk })
Return(p)
La procedure Partition decoupe lensemble p en mettant ensemble les e tats pi de p qui arrivent
dans le meme ensemble. Par exemple, le schema ci-dessous presente deux cas de figure. Sur la
gauche, on a un automate dans lequel le caract`ere c ne separe pas la partition p1 car toutes les
transitions e tiquetees par c arrive sur des e tats qui sont dans la meme partition (dx , dy , dz sont
dans p2 ). Sur la droite, c separe p1 en p1 = {{di }, {dj , dk }} car les transitions e tiquetees par c
partant de di narrivent pas dans la meme partition que celles partant de dj et dk .
c

di

dx

di

dx
p3

c
dj

c
dj

dy

dy

c
dk

p1

c
dz

dk

p2

p1

dz

p2

Appliquons cet algorithme sur lexemple, lalgorithme demarre avec la partie e lementaire
separant les e tats finaux des non finaux :

14

b
p2

q2
p1

b
a
q0

q1

q3

Lalgorithme est incapable de separer p2, il termine donc immediatement et on obtient alors lautomate recherche :
b

a
s0

S1

On peut aussi construire lexpression reguli`ere qui correspond a` un automate donne (cest la
construction de kleene), cest ce qui prouve lequivalence entre langages reconnus par des automates et expressions reguli`eres. Cela est e tudie dans le cours de Marianne Delorme.

2.3 Implementation de lanalyseur lexical


Il sagit maintenant de produire une implementation de lautomate genere lors de la production precedente. Il y a essentiellement deux types dimplementation, les analyseurs bases sur des
tables et les analyseurs codes directement .
2.3.1 Analyseur lexical a` base de table
Lanalyseur utilise un programme squelette commun a` tous les analyseurs lexicaux et une
table dans laquelle est stockee la fonction de transition. Pour notre exemple cela donne :
char prochain caract`ere
state s0
tant que (char = EOF )
state (state, char)
char prochain caract`ere
si (state SF )
alors accepter
sinon rejeter

s0
s1

15

a
s1

s1

s1

autres

2.3.2 Analyseur lexical code directement


Le type danalyseur precedent passe beaucoup de temps a` tester et manipuler la variable
detat, on peut e viter cela en codant directement letat dans letat du programme :
s0 :

char prochain caract`ere


si (char = a )
alors goto s1
sinon goto se

s1 :

char prochain caract`ere


si (char = b |char = c )
alors goto s1
sinon si (char = EOF )
alors accepter
sinon goto se

se :

rejeter

Ce type dimplementation devient rapidement difficile a` gerer manuellement, mais il peut e tre
genere automatiquement et sera en general plus rapide que lanalyseur a` base de table.

2.4 Pour aller plus loin


Un analyseur lexical peut faire un peu plus que reconnatre une phrase du langage, il peut
preparer le terrain pour lanalyse syntaxique. En particulier, il peut reconnatre les mots clefs du
langage. Pour cela il y a deux possibilites, soit on introduit une suite explicite detats reconnaissant
chaque mot clef, soit on reconnat les identificateurs et on les comparera a` une table des symboles
contenant les mots cles du langage.
En general, lanalyseur lexical va generer une sequence de mots e lementaires ou token (separant
etc.) qui sera utilisee pour lanalyse synidentificateurs, valeurs, mots clefs, structure de controle,
taxique.
On peut choisir de realiser des actions dans chaque e tat de lanalyseur (pas seulement lorsque
lon a fini de lire la chane). Par exemple si on essaie de reconnatre un entier, on peut calculer au
vol sa valeur pour la produire d`es que lentier est reconnu.
Le developpement des techniques presentees ici a entrane des definitions de langage coherentes
avec les contraintes des expressions reguli`eres. Mais les langages anciens ont certaines caracteristiques
qui les rendent delicat a` analyser. Par exemple, en Fortran 77, les espaces ne sont pas significatifs,
donc unentier, un entier, u n e n t i e r representent la meme chane. Les identificateurs sont limites a` 6 carct`eres, et le compilateur doit utiliser cela pour identifier les structures
du programme ; les mots cles ne sont pas reserves. Par exemple
FORMAT(4H)=(3)
est linstruction FORMAT car 4H est un prefixe qui designe )=(3 comme une chane de caract`ere.
Alors que
FORMAT(4 )=(3)
est lassignation a` lelement numero 4 du tableau FORMAT.
Ces difficultes ont amene les developpeurs a` produire des analyseurs lexicaux a` deux passes.

16

3 Analyse syntaxique
Dans ce cours nous allons e tudier comment produire des analyseurs syntaxique pour les langages definis par des grammaires hors contexte. Il existe de nombreuses methodes pour parser (i.e.
analyser syntaxiquement) de tels langage, nous verrons une methode recursive descendante (top
down recursive descent parsing) qui est la methode la plus pratique lorsquon code le parser a` la
main. Le meme principe est utilise pour les parser LL(1). Nous verrons aussi une technique ascendante (bottom-up LR(1) parsing) basee sur des tables aussi appelee shift reduce parsing.

3.1 Grammaires hors contexte


Pour decrire la syntaxe dun langage de programmation, on utilise une grammaire. Une grammaire est un ensemble de r`egles decrivant comment former des phrases.
Definition 3 Une grammaire hors contexte (context free grammar, CFG) est un quadruplet G = (T, N T, S, P )
ou` :
T est lensemble des symboles terminaux du langage. Les symboles terminaux correspondent aux
mots decouvert par lanalyseur lexical. On representera les terminaux soulignes pour les identifier.
N T est lensemble des symboles non-terminaux du langage. Ces symboles napparaissent pas dans
le langage mais dans les r`egles de la grammaire definissant le langage, ils permettent dexprimer la
structure des r`egles grammaticales.
S N T est appele lelement de depart de G (ou axiome de G). Le langage que G decrit (note
L(G)) correspond a` lensemble des phrases qui peuvent etre derivees a` partir de S par les r`egles de la
grammaire.
P est un ensemble de production (ou r`egles de ree criture) de la forme N 1 2 . . . n avec
i T N T . Cest a` dire que chaque element de P associe un non terminal a` une suite de terminaux
et non terminaux. Le fait que les parties gauches des r`egles ne contiennent quun seul non terminal
donne la propriete hors contexte a` la grammaire.

Exemple : une liste delement


List elem List
List elem
On peut facilement voir quune suite finie dun nombre quelconque de elem correspond a` une
phrase du langage decrit par cette grammaire. Par exemple, elem elem correspond a` lapplication
de la premi`ere r`egle puis de la deuxi`eme en partant de S :
T = {elem}, N T = {List}, S = List, P =

List elem List elem elem


Exemple : des parenth`eses et crochets correctement e quilibres en alternance, on pourra utiliser
les r`egles suivants :
P =

P aren

( Croch )
| (
)

Croch

[ P aren ]
[
]

On peut e ventuellement rajouter un non terminal Start pour commencer indifferemment par
des crochets ou par des parenth`eses :
Start

P arent
Croch

Considerons la grammaire suivante :


1.
2.
3.
4.
5.
6.

Expr
Op

Expr Op nombre
nombre
+

17

En appliquant la sequence de r`egles : 1, 5, 1, 4, 2 on arrive a` deriver la phrase : nombre


nombre nombre :
R`
egle P hrase
Expr
1
Expr Op number
5
Expr number
1
Expr Op number number
4
Expr number number
2
number number number
On peut representer graphiquement cette derivation par un arbre que lon nomme arbre de derivation
(ou arbre syntaxique, arbre de syntaxe, parse tree).
Expr

Expr

Expr

Op

number

Op

number

number

Lors de cette derivation, nous avons toujours decide dutiliser une r`egle derivant du non terminal
le plus a` droite de la phrase (derivation la plus a` droite, rightmost derivation). On aurait pu arriver
a` la meme phrase en choisissant systematiquement le non-terminal le plus a` gauche par exemple
(derivation la plus a` gauche), cela donne lordre dapplication des r`egles : 1, 1, 2 ,4 , 5
R`
egle
1
1
2
4
5

P hrase
Expr
Expr Op number
Expr Op number Op number
number Op number Op number
number number Op number
number number number

Si lon dessine larbre de derivation correspondant on sapercoit que cest le meme arbre que celui
dessine precedemment. En general, ce nest pas toujours le cas. Cette propriete est appelee la
non-ambigute. Une grammaire est ambigue sil existe une phrase de L(G) qui poss`ede plusieurs
arbres de derivation.
Lexemple classique de grammaire ambigue est le if-then-else
1
2
3
4

Instruction

|
|
|

if Expr then Instruction else Instruction


if Expr then Instruction
Assignation
AutreInstructions....

Avec cette grammaire, le code


if Expr1 then if Expr2 then Ass1 else Ass2
peut e tre compris de deux mani`eres :
if Expr1
if Expr1
then if Expr2
then if Expr2
then Ass1
then Ass1
else Ass2
else Ass2
les sens des deux interpretations sont differents. Pour enlever cette ambigute, on
Bien sur,
doit faire un choix pour la resoudre (par exemple que chaque else se rapporte au if non ferme
18

le plus proche dans limbrication), et on doit modifier la grammaire pour faire apparatre cette
r`egle de priorite, par exemple de la facon suivante :
1
2
3
4
5
6
7
8

Instruction

|
AvecElse

|
|
DernierElse
|
|

AvecElse
DernierElse
if Expr then AvecElse else AvecElse
Assignation
AutreInstructions....
if Expr then Instruction
if Expr then AvecElse else DernierElse
AutreInstructions....

On voit donc une relation entre la structure grammaticale et le sens dune phrase. Il existe dautres
situations pour lesquelles la structure de la grammaire va influencer sur la mani`ere avec laquelle on comprend la phrase. Par exemple, pour la priorite des operateurs, la grammaire donnee
precedemment pour les expressions arithmetiques simples ne permet pas dencoder dans larbre
de derivation lordre correct devaluation de lexpression : si lon parcourt larbre dun mani`ere naturelle par exemple par un parcours en profondeur, on e valuera lexpression (nombrenombre)
nombre alors que lon voudrait e valuer nombre (nombre nombre).
Pour permettre que larbre de derivation refl`ete la precedence des operateurs, il faut restructurer la grammaire, par exemple de la facon suivante :
1.
2.
3.
4.
5.
6.

Expr

T erm

Expr +T erm
Expr T erm
T erm
T erm number
T erm number
number

On fera de meme pour introduire les precendences superieures pour les parenth`eses, les indices
de tableaux, les conversions de type (type-cast) etc. Dans la suite on utilisera la grammaire cidessous (grammaire dexpressions classiques)
La grammaire dexpressions arithmetiques classiques :
1.
2.
3.
4.
5.
6.
7.
8.
9.

Expr

T erm

F actor

Expr + T erm
Expr T erm
T erm
T erm F actor
T erm F actor
F actor
( Expr )
number
identif ier

Jusqu`a present nous avons devine les r`egles a` utiliser pour deriver une phrase. Le travail
du parser est de construire larbre de derivation a` partir dune phrase du langage composee donc
uniquement de terminaux. En fait, le parser prend en entree une phrase allegee par lanalyse
syntaxique cest a` dire une sequence de mots (token) avec leur categorie syntaxique attachee. Par
exemple si le code source est x 2 y, lentree du parser sera quelque chose comme :
identif icateur, x

nombre, 2

identif icateur, y

On voudrait produire larbre suivant grace a` la grammaire des expressions arithmetiques classiques.

19

Expr

Expr

Term

Term

Term

Factor

Factor

<x,id>

<2,num>

Factor

<y,id>

Les parseurs descendants (top down parsers) vont partir de laxiome et a` chaque e tape choisir
dappliquer une r`egle pour lun des non terminals presents sur la fronti`ere de larbre. Les parsers
ascendants (bottom up parsers) vont partir des feuilles et tenter de reconstruire larbre a` partir du
bas.
Les grammaires hors contexte decrivent plus de langages que les expressions reguli`eres (celui des parenth`eses e quilibrees ne peut e tre exprime en expression reguli`ere, les prioritees entre
operateurs peuvent e tre exprimees dans la structure des grammaires hors contexte). La classe des
grammaires reguli`eres decrit exactement les meme langages. Une grammaire est dite reguli`ere (ou
grammaire lineaire a` gauche) si toutes les productions sont de la forme : soit A a soit A aB
avec A, B N T et a T . Les langages reguliers sont un sous ensemble strict de lensemble des
langages reconnus par les grammaires hors contexte.
On peut legitimement se demander pourquoi on nutilise pas uniquement le parsing pour
analyser un programme source. As-ton besoin de lanalyse lexicale ? La raison est que les analyseurs bases sur des automates finis sont tr`es efficaces, le temps danalyse est proportionnel a` la
longueur de la chane dentree et les constantes multiplicatives sont tr`es petites (un analyseur syntaxique serait beaucoup plus long). Mais surtout, en reconnaissant les structures lexicales de base,
les analyseurs lexicaux reduisent fortement la complexite des grammaires necessaires a` specifier
la syntaxe du langage (suppression des commentaires, detection des identificateurs, des valeurs
etc.).

3.2 Analyse syntaxique descendante


Le principe de lanalyse syntaxique descendante est le suivant : on commence avec laxiome de
la grammaire et on developpe systematiquement larbre de derivation jusquaux feuilles (terminaux) en choisissant systematiquement de deriver le non-terminal le plus a` gauche sur la fronti`ere
de larbre. Quand on arrive aux terminaux, on verifie quils correspondent aux mots qui sont dans
la phrase ; si cest OK, on continue, sinon, il faut revenir en arri`ere (bactracker) et choisir une autre
r`egle dans le processus de derivation. Larbre de derivation au cours de lalgorithme ressemble
donc a` cela :

20

Axiome

NTn
.......
NT1

NT2

t1

t2

NT3

NT4

NT5
Frontire

t3
Prochain non terminal dvelopp

On va voir que ce qui rend ce processus viable est quil existe un large sous ensemble de
grammaires hors contexte pour lesquelles, on naura jamais besoin de bactracker. On utilise un
pointeur sur la chane a` lire qui ne peut quavancer (on na pas acc`es a` nimporte quel endroit de
la chane nimporte quand).
3.2.1 Exemple danalyse descendante
Considerons que lon ait a parser lexpression x 2 y que lon veut parser avec la grammaire
des expressions arithmetiques classiques. On va utiliser les notation precedentes pour la succession de r`egles permettant la derivation en ajoutant un signe : dans la colonne des numeros de
r`egle pour indiquer que lon avance le pointeur de lecture. La fl`eche verticale indiquera la position
du pointeur de lecture. Si lon applique betement la methode expliquee ci-dessus, on va pouvoir
tomber sur ce type de derivation :
R`
egle
1
1
1
1

P hrase
Expr
Expr + T erm
Expr + T erm + T erm
Expr + T erm ...
...

Entree
x2y
x2y
x2y
x2y
x2y

En fait la grammaire telle quelle est definie pose un probl`eme intrins`eque car elle est recursive
a` gauche. Ignorons pour linstant ce probl`eme et supposons que le parser ait effectue la derivation
suivante :
Entree
R`
egle P hrase
Expr
x2y
T erm
x2y
3
6
F actor
x2y
9
identif ier x 2 y
identif ier x 2 y

Ici, on ne boucle pas mais on na pas reconnu la phrase enti`ere, on en a reconnu une partie, mais on
na plus de non terminal a` remplacer, on ne sait pas si lon a fait un mauvais choix a` un moment ou
si la phrase nest pas valide. Dans le doute, on backtrack et, en supposant que lon puisse essayer

21

toutes les possibilites, on arrivera finalement a` la derivation suivante :


R`
egle
2
3
6
9

4
6
8

3.2.2

P hrase
Expr
Expr T erm
T erm T erm
F actor T erm
identif ier T erm
identif ier T erm
identif ier T erm
identif ier T erm F actor
identif ier F actor F actor
identif ier number F actor
identif ier number F actor
identif ier number F actor
identif ier number identif ier
identif ier number identif ier

Entree
x2y
x2y
x2y
x2y
x2y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x2y

Elimination
de la recursion a` gauche

La recursivite a` gauche pose un probl`eme pour ce type de parser car le bactrack ne peut
arriver que lorsquon arrive sur un non terminal en premi`ere position dans la phrase partielle
en cours de reconnaissance. On peut transformer mecaniquement des grammaires pour enlever
toute recursivite a` gauche en autorisant les r`egles du type A .
A

A
B

B
B

Lorsque la recursivite est indirecte, on deroule les r`egles jusqu`a ce que lon rencontre une
recursivite directe et on utilise le meme processus :

A
B

B
A

A
B

A

A

A
C
B

C
C

Du fait de la nature finie de la grammaire, on sait que le processus de deroulage va sarreter.


La grammaire des expressions arithmetiques classique peut e tre ree crite en variante recursive a`
droite :
1. Expr
T erm Expr
2. Expr
+ T erm Expr
3.
T erm Expr
4.

5. T erm
F actor T erm
F actor T erm
6. T erm
7.
F actor T erm

8.
9. F actor ( Expr )
10.
number
11.
identif ier
3.2.3

Elimination
du bactrack

On va montrer comment on peut, pour des grammaires recursives a` droite, systematiquement


e liminer le bactrack. En fait, le bactrack arrive lorsque lalgorithme choisi une mauvaise r`egle a`
22

appliquer. Si lalgorithme choisissait systematiquement la bonne r`egle, il ny aurait jamais besoin


de bactracker.
Lidee est que lorsque le parser choisit une r`egle pour remplacer le non terminal le plus a`
gauche sur la fronti`ere, il va e tre capable de connatre lensemble des terminaux qui arriveront
finalement a` cette place. En regardant le premier mot non accepte sur la phrase dentree, le parser va alors pouvoir choisir la r`egle qui convient. On peut montrer que pour les grammaires
recursives a` droite, la lecture dun seul mot en avant permet de decider quelle r`egle appliquer. De
l`a vient le terme LL(1) : Left-to-right scanning, Leftmost derivation, use 1 symbol lookahead .
Voyons comment cela marche en reconnaissant lexpression x 2 y avec la version recursive
a` droite de la grammaire dexpressions arithmetiques classiques :
R`
egle
1
5
11

8
3

5
10

11

8
4

P hrase
Expr
T erm Expr
F actor T erm Expr
id T erm Expr
id T erm Expr
id Expr
id T erm Expr
id T erm Expr
id F actor T erm Expr
id num T erm Expr
id num T erm Expr
id num F actor T erm Expr
id num F actor T erm Expr
id num id T erm Expr
id num id T erm Expr
id num id Expr
id num id

Entree
x2y
x2y
x2y
x2y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x 2 y
x2y
x2y
x2y

Les deux premi`eres derivations sont imposees. Nous voyons que la troisi`eme derivation (application de la r`egle 11) est imposee par le fait que le mot arrivant nest ni une parenth`ese ni un
nombre. De meme on est oblige dappliquer la r`egle 8 (T erm ) car sinon on devrait trouver
soit un soit un . On continue jusqu`a reconnaissance de la phrase enti`ere.
On va formaliser cela en introduisant les ensembles F irst et F ollow.
Definition 4 Soit F irst() lensemble des symboles terminaux qui peuvent apparatre comme premier
symbole dans une phrase derivee du symbole
F irst est defini pour les symboles terminaux et non terminaux. Considerons les r`egles pour
Expr :
2. Expr + T erm Expr
3.
T erm Expr
4.

Pour les deux premi`eres,il ny a pas dambiguites, par contre, pour lepsilon transition, le seul
symbole derive est , on met alors dans lensemble F irst on a donc : F irst(Expr ) = {+, , }.
Si un ensemble F irst(A) contient , on ne sait pas quel non terminal sera recontre lorsque ce
A aura e te developpe. Dans ce cas, le parser doit connatre ce qui peut apparatre immediatement
a` la droite de la chaine vide produite, ici, immediatement a` la droite de Expr .

donne un symbole non terminal A, on definit F ollow(A) comme lensemble des symDefinition 5 Etant
boles terminaux qui peuvent apparatre immediatement apr`es un A dans une phrase valide du langage.
Dans notre grammaire, on voit que rien ne peut apparatre apr`es Expr a` part une parenth`ese
fermante : F ollow(Expr ) = {eof, )}. Voici les algorithmes pour calculer les ensembles F irst et
F ollow :
23

pour chaque T
F irst()
pour chaque A N T
F irst(A)
tant que (les ensembles F irst changent)
Pour chaque p P ou p a` la forme A 1 2 . . . k
F irst(A) F irst(A) (F irst(1 ) { })
i1
tant que ( F irst(i ) et i k 1)
F irst(A) F irst(A) (F irst(i+1 ) { })
ii+1
si i = k et F irst(k )
alors F irst(A) F irst(A) { }
pour chaque A N T
F ollow(A)
tant que (les ensembles F ollow changent)
Pour chaque p P ou p a` la forme A 1 2 . . . k
si k N T alors F ollow(k ) F ollow(k ) F ollow(A)
T raqueur F ollow(A)
Pour i k jusqu`a 2
si F irst(i ) alors
si i1 N T alors F ollow(i1 ) F ollow(i1 ) (F irst(i ) { }) T raqueur
sinon
si i1 N T alors F ollow(i1 ) F ollow(i1 ) F irst(i )
T raqueur F irst(i )
Ces algorithmes sont formules comme des calculs de point fixe, on peut montrer quils convergent.
Les ensemble F irst et F ollow pour les non terminaux de la grammaire dexpressions arithmetiques
classiques (variante recursive a` droite) sont :
symbole
Expr
Expr
T erm
T erm
F actor

F irst
{(, number, identif ier}
{+, , }
{(, number, identif ier}
{, , }
{(, number, identif ier}

F ollow
{)}
{)}
{+, }
{+, }
{, , +, }

On peut maintenant formuler la condition a` laquelle une grammaire permet deviter les bactrack dans un parsing descendant. Notons F irst+ () lensemble F irst() si
/ F irst() et
F irst() F ollow() si F irst(). La condition que doit verifier la grammaire est que pour
tout non-terminal A qui est en partie de gauche plusieurs r`egles : A 1 | 2 | . . . n on doit avoir
la propriete suivante. Par extension F irst+ (ABCD) designe F irst+ (A) et dans le cas particulier
ou` un des i est e gal a` , on rajoute lintersection avec des F irst+ (i ) avec F ollow(A).
F irst+ (i ) F irst+ (j ) = , i, j 1 i < j n
Pour que lon ait cette propriete, e tant donnee une grammaire recursive a` droite, on va la factoriser a` gauche, cest a` dire que lorsquon aura plusieurs productions pour le meme non-terminal
qui ont le meme prefixe, on va introduire un nouveau non-terminal pour tous les suffixes suivant ce prefixe, ce qui permettra de choisir une et une seule r`egle lorsquon veut demarrer par ce
prefixe. Par exemple, si on a les r`egles :
A 1 | 2 | . . . | n | 1 | . . . | k

24

on les remplacera par :


A B | 1 | . . . | k
B 1 | 2 | . . . | n
La factorisation a` gauche permet de convertir certaines grammaires necessitant du bactrack en
des grammaires ne necessitant pas de bactrack (ou grammaires predictives, ou grammaire LL(1)).
Cependant toutes les grammaires hors contexte ne peuvent pas e tre transformees en grammaires
ne necessitant pas de bactrack. Savoir si une grammaire LL(1) e quivalente existe pour une grammaire hors contexte arbitraire est indecidable.
3.2.4 Parser descendant recursif
On a maintenant tous les outils pour construire un parser recursif descendant a` la main. On va
e crire un ensemble de procedures mutuellement recursives, une pour chaque non-terminal dans
la grammaire. La procedure pour le non terminal A doit reconnatre une instance de A pour cela
elle doit reconnatre les differents non-terminaux apparaissant en partie droite dune production
de A. Par exemple, si A poss`ede les productions suivantes :
A a1 1 | 1 2 |
avec a1 T , 1 , 1 , 2 N T alors le code de la procedure pour A aura la forme suivante :
trouverA()
si (motCourant = a1 ) alors
motCourant = prochainM ot() ;
return (trouver1 ()) /* A a1 1 choisi */
sinon si (motCourant F irst+ (1 )) alors
return (trouver1 () trouver2 ()) /* A 1 2 choisi */
sinon si (motCourant F ollow(A))
alors return True /* A choisi */
sinon return False /* erreur */
Par exemple, voici le parser recursif descendant pour la grammaire des expressions arithmetiques
(variante recursive a` droite) :

25

M ain()
/* But Expr */
motCourant = prochainM ot() ;
si (Expr() motCourant == eof )
alors return T rue
sinon return F alse

EP rime()
/* T erm F actor T erm */
/* T erm F actor T erm */
si (motCourant == )
(motCourant == ) alors
motCourant = prochainM ot()
si (F actor() == F alse)
alors retrun F alse
sinon return T P rime()
/* T erm */
return T rue

Expr()
/* Expr T erm Expr */
si (T erm() == F alse)
alors return F alse
sinon return EP rime()

EP rime()
/* Expr + T erm Expr */
/* Expr T erm Expr */
si (motCourant == +)
(motCourant == ) alors
motCourant = prochainM ot()
si (T erm() == F alse)
alors retrun F alse
sinon return EP rime()
/* Expr */
return T rue
T erm()
/* T erm F actor T erm */
si (F actor() == F alse)
alors return F alse
sinon return T P rime()

F actor()
/* F actor ( Expr ) */
si (motCourant == () alors
motCourant = prochainM ot()
si (Expr() == F alse)
alors retrun F alse
sinon si (motCourant =))
alors return F alse
sinon
/* F actor number */
/* F actor identif ier */
si (motCourant() = number)
(motCourant() = identif ier)
alors return F alse
motCourant = prochainM ot()
return T rue

On peut aussi imaginer une implementation a` base de table indiquant, pour chaque non terminal, la r`egle a` appliquer en fonction du mot rencontre. Ce type de parseur presentent les avantages
detre facilement codes manuellement, ils sont compacts et efficaces. Ils gen`erent en particulier un
debogging tr`es efficace pour les entrees non conformes.

3.3 Parsing ascendant


On va voir maintenant lapproche inverse : on construit des feuilles au fur et a` mesure quon
rencontre des mots, et lorsque lon peut reduire les feuilles en utilisant une r`egle (i.e. ajouter un
parent commun a` plusieurs noeuds contigus correspondant a` une partie gauche de r`egle), on le
fait. Si la construction est bloquee avant darriver a` une racine unique e tiquetee par laxiome, le
parsing e choue. Larbre de derivation est donc construit par le bas et la fronti`ere est maintenant
dirigee vers le haut :

26

NT4
NT7

Frontire
NT1

t1

NT2

t2

NT3

NT5

t3

t4

NT6

t5

t6

Du fait que les reductions de r`egles ont lieu a` partir de la gauche (dans lordre de lecture de la
phrase), et que larbre est construit a` lenvers, par les feuilles, larbre de derivation decouvert par
ce parseur sera un arbre de derivation la plus a` droite. On va considerer les parser LR(1). Ces
parseurs lisent de gauche a` droite et construisent (`a lenvers) une derivation la plus a` droite en
regardant au plus un symbole sur lentree, dou leur nom : Left-to -right scan, Reverse-rightmost
derivation with 1 symbol lookahead. On appelle aussi cela du shift-reduce parsing.
On introduit la notation suivante : A , k indique que sur la frontiere, a` la position k,
il y une possibilite de reduction avec la r`egle A . On appelle cela une reduction candidate
(handle en anglais). Limplementation du parser va utiliser une pile. Le parser fonctionne de la
mani`ere suivante : il empile la fronti`ere, reduit les reductions candidates a` partir du sommet de
pile (depile les parties droites et empile la partie gauche de la r`egle selectionnee). Quand cela
nest plus possible, il demande le prochain mot et lempile puis recommence. Le sommet de la
pile correspond a` la partie la plus a` droite de la fronti`ere. Lalgorithme generique pour le shiftreduce parsing est le suivant :
push Invalid
motCourant prochainM ot()
tant que (motCourant = eof ou la pile ne contient pas exactement Expr au dessus de Invalid)
si une reduction candidate A , k est au sommet de pile
alors /* reduire par A */
pop || symbols
push A
sinon si (motCourant = eof )
alors/* shift : decalage dun sur lentree */
push motCourant
motCourant prochainM ot()
sinon /* pas de reduction candidate pas dinput */
error
Ici les bonnes reductions candidates ont e te trouvees par un oracle. Une detection efficace des
bonnes reductions candidates a` realiser est la clef dun parsing LR(1) efficace. Une fois que lon a
cela, le reste est trivial.
3.3.1 Detection des reductions candidates
On va introduire une nouvelle notation pour les reductions candidates : A . Le point
noir (, dot en anglais) represente le sommet de la pile. La notation A signifie, la pile est
telle que lon peut reduire avec la r`egle A a` partir du sommet de pile (on na plus besoin de
ladresse dans la fronti`ere puisquon choisi de reduire toujours a` partir du sommet de pile). Dans
le meme ordre didee, la notation A va representer letat dans lequel jai empile , si
jamais jempile encore alors la r`egle A sera une reduction candidate et je pourrai reduire.

27

On dit alors que lon a une reduction potentielle (elle nest pas encore candidate puisquon a pas lu
tout ce dont on aurait besoin pour faire la reduction).
On peut construire un nombre fini de tel dotted item (reduction candidate ou potentielle) pour
une grammaire donnee. Un ensemble de reductions candidates ou potentielles represente donc
un e tat du parser (i.e. un ensemble de possibilites daction a` realiser en fonction de ce qui est
lu). Les parseurs LR(1) sont en fait des automates dont les e tats sont constitues densemble de
reductions potentielles ou candidates. On a vu quon pouvait implementer ces automates a` partir
dune table repesentant la fonction de transition, les outils permettant la construction de cette
table sont tr`es largement utilises aujourdhui pour la construction de parser.
La grosse difficulte va e tre de construire deux tables (Action et Goto) qui vont indiquer laction
a` realiser a` chaque e tape. Ces tables contiennent la connaissance precompilee des reductions
potentielles et candidates pouvant se presenter en fonction de ce quon lit et de letat dans lequel
on se trouve. Lalgorithme pour le parser LR(1) est le suivant :
push Invalid
push s0
motCourant prochainM ot()
tant que (True)
s sommet de pile
si Action[s, motCourant]= shift
alors push motCourant
push Goto[s, motCourant]
motCourant prochainM ot()
sinon si Action[s, motCourant]= reduce A
alors pop 2 || symboles
s sommet de pile
push A
push Goto[s, A]
sinon si Action[s, motCourant]= accept alors Accept
sinon Erreur
On voit que, au lieu de la phrase vague : si une reduction candidate A est au sommet de
pile on a une action indiquee par la table action, et au lieu dempiler uniquement la fronti`ere, on
empile aussi letat courant.
3.3.2 Construction des tables LR(1)
Pour construire les tables Action et Goto, le parseur construit un automate permettant de reconnatre les reductions potentielles et candidates et deduit de cet automate les tables. Cet automate utilise des ensembles ditem LR(1) (voir definition ci-apr`es) comme e tats, lensemble des
e tats est appele la collection canonique densemble ditem LR(1) : CC = {cc0 , cc1 , . . . , ccn }. Chacun de
ces ensembles va correspondre a` un e tat si de lautomate dans lalgorithme ci-dessus.
Definition 6 Un item LR(1) est une paire : [A , a] ou` A est une reduction potentielle
ou candidate, indique ou est le sommet de pile et a T est un terminal (on rajoute toujours le terminal
EOF pour signifier la fin du fichier).
Ces item LR(1) decrivent les configurations dans lesquelles peut se trouver le parser ; ils decrivent
les reductions potentielles compatibles avec ce qui a e te lu, a est le prochain caract`ere a` lire, une
fois que lon aura reduit par cette reduction potentielle. Reprenons lexemple de la grammaire de
liste :
1. But List
2. List elem List
3.
| elem
28

elle produit les items LR(1) suivants :


[But List, eof ]
[But List , eof ]
[List elem, eof ]
[List elem , eof ]
[List elem, elem]
[List elem , elem]

[List elem List, eof ]


[List elem List, eof ]
[List elem List , eof ]
[List elem List, elem]
[List elem List, elem]
[List elem List , elem]

On suppose, comme cest le cas ici que lon a une unique r`egle indiquant le but a` realiser pour
reconnatre une phrase du langage. Litem [But List, eof ] sert a` initaliser letat initial cc0
du parseur. On va rajouter a cela les item qui peuvent e tre rencontres pour realiser cet item : on
utilise pour cela la procedure closure :
closure(s)
tant que (s change)
pour chaque item [A C, a] s
pour chaque production C P
pour chaque b F irst(a)
s s {[C , b]}
Voici lexplication en francais : si [A C, a] s alors une completion possible du contexte
dej`a lu est de trouver une chane qui se reduit a` C suivi de a. Si cette reduction (par exemple
C ) a lieu, on va donc recontrer puis un e lement de F irst(a) juste apr`es donc on passera
forcement par un des item ajoutes dans la procedure closure.
Dans notre exemple, on a initalement [But List, eof ] dans cc0 , le passage par la procedure
closure() ajoute deux items : [List elem List, eof ] et [But elem, eof ], comme les
prec`edent uniquement des non terminaux, cela ne gen`ere pas plus ditem :
cc0 = closure(s0 ) = {[But List, eof ], [List elem List, eof ], [But elem, eof ]}
Table Goto On construit une table qui prevoit letat que le parseur devrait prendre sil e tait dans
un certain e tat s = cci et quil lisait un symbole x.
goto(s, x)
moved
pour chaque item i s
si i est [ x, a] alors
moved moved {[ x , a]}
return closure(moved)
Sur notre exemple, goto(cc0 , elem) donne dabord : les deux items : {[List
List, eof ], [List elem , eof ]} puis la fermeture rajoute :
cc2 = goto(cc0 , elem) =

[List elem List, eof ] , [List elem , eof ] ,


[List elem List, eof ] , [List elem, eof ]

On a aussi
cc1 = goto(cc0 , List) = {[But List , eof ]}
et
cc3 = goto(cc2 , List) = {[List elem List, eof ]
et enfin : goto(cc2 , elem) = cc2 .

29

elem

Collection canonique densemble ditem LR(1)


cc0 closure(s0 )
CC cc0
tant que (CC change)
pour chaque ccj CC non marque
marquer ccj
pour chaque x suivant un dans un item de ccj
temp goto(ccj , x)
si temp
/ CC
alors CC CC {temp}
enregistrer la transition de ccj a` temp sur x
Lensemble CC des e tats generes ainsi que les transitions enregistrees donnent directement la
table Goto (on utilise que les transitions sur les non-terminaux pour la table Goto). Pour la table
Action, on regarde les e tats generes. Il y a plusieurs cas :
1. Un item de la forme [A c, a] indique le fait de rencontrer le terminal c serait une e tape
valide pour la reconnaissance du non terminal A. Laction a` generer est un shift sur c dans
letat courant. Le prochain e tat est letat genere avec la table Goto ( et peuvent e tre ).
2. Un item de la forme [A , a] indique que le parser a reconnu ; si le symbole a` lire est a
alors litem est une reduction candidate, on gen`ere donc un reduce A sur a dans letat
courant.
3. Litem [But S , eof ] est unique ; il gen`ere une acceptation.
Notez quon ignore les item ou` le prec`ede un non-terminal. En fait, le fait que lon ait execute la
procedure closure assure que les items qui permettent de reduire ce non-terminal sont presents
dans le meme e tat. On recup`ere donc les deux tables :
etat
cc0
cc1
cc2
cc3

T able Action
eof
accept
reduce List elem
reduce List elem List

elem
shif t cc2
shif t cc2

T able Goto
etat List eof elem
cc0
cc1
cc2
cc1
cc2
cc3
cc2
cc3

Si une grammaire nest pas LR(1) (si elle est ambigue, par exemple) alors cette propriete va
apparatre lors de la construction des tables. On va se retrouver avec un conflit shif t reduce :
un item contient a` la fois [A c, a] et [B , c]. Il peut aussi contenir un conflit reduce
reduce : [A , a] et [B , a]. La meilleure methode pour determiner si une grammaire est
LR(1) est de lancer le parseur LR(1) dessus.

3.4 Quelques conseils pratiques


Bien que les parseurs soient maintenant essentiellement generes automatiquement, un certain
nombre de choix sont laisses au programmeur.
Gestion derreur Le parseur est largement utilise pour debugger les erreurs de syntaxe, il doit
donc en detecter le maximum dun seul coup. Pour linstant on a toujours suppose que le parseur
sarretait lorsquil rencontrait une erreur. Si lon desire continuer le parsing, il faut un mecanisme
qui permette de remettre le parseur dans un e tat a` partir duquel il peut continuer le parsing. Une
mani`ere de realiser cela est de reperer des mots de synchronisation qui permettent de synchroniser lentree lue avec letat interne du parseur. Lorsque le parseur rencontre des erreurs, il oublie
les mots jusqu`a ce quil rencontre un mot de synchronisation. Par exemple, dans beaucoup de
langage `a la Algol le point-virgule est un separateur qui peut e tre utilise comme mot de synchronisation.
30

Pour les parseurs descendants, ca ne pose pas de probl`eme. Pour les parseur LR(1), on cherche
dans la pile le dernier Goto[s, Statement] (i.e. celui qui a provoque lerreur), on depile, avale les
caract`eres jusquau debut du prochain Statement et empile un nouveau Goto[s, Statement]. On
peut indiquer aux generateurs de parseurs comment se synchroniser avec les r`egles de recouvrement derreurs (on indique dans les parties droites de r`egles ou` et comment ils peuvent se
resynchroniser).
Ambiguite sensible au contexte On utilise souvent les parenth`eses pour representer a` la fois
les acc`es aux tableaux et les appels de fonctions. Une grammaire permettant cela sera forcement
ambigue. Dans un parser descendant on peut tester le type de lidentificateur precedant les parenth`ese. Pour les parseurs LR(1) on peut introduire un non terminal commun pour les deux
usages (et tester le type de lidentificateur) ou reserver des identificateurs differents pour les tableaux et le fonctions.
recursion a` droite ou a` gauche Les parsers LR(1) autorisent les grammaires recursives a` gauche.
On a donc le choix entre grammaires recursives a` droite et a` gauche. Pour decrire le langage, plusieurs facteurs entrent en ligne de compte :
la taille de la pile. En general la recursion a` gauche entrane des piles de taille inferieure.
La recursion a` gauche va reduire plus rapidement et limiter la taille de la pile (`a lextreme :
une liste de taille l peut entraner une pile de taille 2 en recursif a` gauche et l en recursif a`
droite).
Associativite. La recursion a` gauche ou a` droite de la grammaire va imposer lordre dans
lequel lassociativite va e tre faite (les grammaires recursives a` gauche produisent de lassociativite a` gauche si on a List List elem on produit un AST qui descend a` gauche et
donc : ((elem1 elem2 ) elem3 ) . . .. Meme pour des operateurs theoriquement associatifs cet
ordre peut changer les valeurs a` lexecution. On peut e ventuellement modifier lassociativite
par defaut dans le parseur en manipulant explicitement lAST
optimisations La forme de la grammaire a un impact direct sur les performances de son analyse
syntaxique (bien que la complexite asymptotique ne change pas). On peut ree crire la grammaire
pour reduire la hauteur de larbre de syntaxe. Si lon reprend la grammaire des expressions classiques et que lon deroule le non-terminal F actor dans lexpression de T erm on obtient 9 parties droites possibles pour T erm mais on reduit la hauteur generale de tout arbre dexpression
arithmetique de 1. En parsing LR(1) sur lexpression x 2 y, cela e limine trois reductions sur
6 (le nombre de shift reste inchange). En general on peut e liminer toutes les productions inutiles, cest a` dire ne comportant quun seul symbole en partie droite. En revanche la grammaire
resultante est moins lisible et comme on va le voir plus loin, les actions que pourra realiser le

parseur seront moins faciles a` controler.


Cette transformation peut aussi augmenter la taille des
tables generees (ici CC passe de 32 ensembles a` 46 ensembles), le parseur resultant execute moins
de reductions mais manipule des tables plus grandes.
On peut aussi regrouper des non terminaux dans des categories (par exemple, + et ) et ne
mettre quune seule r`egle pour tous, la differenciation e tant fate en testant explicitement la valeur
du symbole a` posteriori.
De gros efforts sont faits pour reduire les tables des parseurs LR(1). Lorsque les lignes ou les
colonnes des tables sont identiques, le concepteur du parseur peut les combiner et renommer les
lignes des tables en consequence (une ligne peut alors representer plusieurs e tats). pour la grammaire des expressions simple, cela produit une reduction de taille de 28% . On peut aussi choisir
de combiner deux lignes dont le comportement ne diff`ere que pour les erreurs, on acceptera peute tre alors des codes incorrects.

On peut aussi decider dimplementer le contenu de Action et Goto directement en code plutot
que dans une table. chaque e tat devient alors un gros case. Cela devient difficile a` lire mais peut
e tre tr`es efficace
Enfin, il existe dautres algorithmes de construction de parseur qui produisent des tables plus
petites que la construction LR(1) classique. Lalgorithme SLR(1) (Simple LR(1)) impose a` la
grammaire que le symbole a` lire en avant ne soit pas necessaire pour effectuer une reduction par

31

le bas. Les item sont alors de la forme A sans le symbole cense suivre apr`es . Lalgorithme est similaire a` LR(1) mais il utilise lensemble F ollow entier (independant du contexte)
que la restriction de cet ensemble au contexte dans
pour choisir entre shift et reduction plutot
lequel on se trouve.
Lalgorithme LALR(1) (Lookahead LR(1)) rassemble certains item qui ne diff`erent que par
le symbole lu en avant (item ayant le meme core, le meme item SLR). Cela produit une collection canonique semblable a` celle de la construction LR(1) mais plus compacte. Une grammaire
LR(1) peut ne pas e tre LALR(1) (on peut montrer que tout langage ayant une grammaire LR(1)
a aussi une grammaire SLR(1) et une grammaire LALR(1)). Malgre la moindre expressivite,
les parseurs LALR(1) sont extremement populaires du fait de leur efficacite (Bison/Y acc en est
un). En fait pour une grammaire de langage quelconque il tr`es probable que la transformation
LR(1) LALR(1) de lautomate donne un automate sans conflit.

3.5 Resume sur lanalyse lexicale/syntaxique


Lanalyse lexicale transforme un flot de caract`eres en un flot de token. Lanalyse syntaxique
transforme un flot de token en arbre syntaxique.
Un token est generalement une classe et une chane de caract`ere (son nom) (on peut quelquefois rajouter des informations sur sa position dans la chane).
Les analyseurs lexicaux peuvent e tre generes a` partir des automates detats finis, eux-memes
generes a` partir des expressions reguli`eres decrivant des langages.
Lanalyseur lexical fait un traitement lorsquil rencontre un identificateur (en general il le
reference dans une table des symboles).
Lanalyse syntaxique peut se faire de mani`ere descendante ou ascendante (se referant a` la
mani`ere avec laquelle on construit larbre syntaxique.
Les parseurs descendants e crits a` la main consistent en un ensemble de procedures mutuellement recursives structurees comme la grammaire.
Pour e crire ces parseurs, il faut avoir calcule les ensembles F irst et F ollow. Les grammaires
acceptees par ces parseurs sont dits LL(1).
Les grammaires recursives a` gauche ne sont pas LL(1), on peut derecursiver une grammaire
recursive a` gauche.
Les parseurs ascendants manipulent des item : a` la base cest une r`egle avec un point (dot)
symbolisant lendroit jusquauquel on a reconnu (eventuellement associe a` un ou plusieurs
symboles representant ce que lon rencontrera apr`es avoir fini de reconnatre la r`egle en
question).
Les parseurs ascendants essayent didentifier successivement les reductions candidates (handle).
Il y de nombreuses techniques pour trouver les reductions candidates. Les parseurs LR(1)
utilisent des ensembles ditem LR(1).
Un ensemble ditem represente un e tat dans lequel peut se trouver le parseur LR(1) a` un
moment donne. Formellement, les e tats de lautomate a` partir duquel est construit le parseur LR(1) sont des ensembles ditem.
En parsing LR(0) tout item de la forme [N ] gen`ere une reduction. En parsing SLR(1)
un item [N ] gen`ere une reduction si et seulement si le prochain symbole a` lire
appartient a` F ollow(N ). En parsing LR(1) un item [N , A] gen`ere une reduction si le
prochain symbole a` lire est dans lensemble A, un petit ensemble de terminaux. En LR(1) A
est limite a` un symbole. En LALR(1) cest un ensemble de symbole (dans F ollow(N )). En
pratique une grammaire LR(1) a de tr`es fortes chances detre LALR(1).
Concernant les classes de grammaires, on a les relations : RG LL(1) LR(1) CF G.
Lambigute entrane le fait que la grammaire nest pas LR(1), mais ce nest pas le seul cas.

Etant
donnee une grammaire, on ne sait pas dire si on peut trouver une autre grammaire
e quivalente LR(1). La methode la plus simple pour savoir si une grammaire est LR(1) est
de construire le parseur et de voir sil y a des reductions.

32

4 Analyse sensible au contexte


On a vu que lon pouvait introduire certaines informations semantiques dans la phase de parsing. Mais cela ne suffit pas, on a besoin de plus dinformation que lon ne peut pas coder avec
une grammaire hors contexte, on va avoir besoin dinformation contextuelle. Par exemple, lorsquon rencontre un identificateur, est-ce une fonction ou une variable ; si cest une variable, quel
type de valeur est stockee dans cette variable ; si cest une fonction, combien et quel type dargument a-t-elle ? Toutes ces informations sont disponibles dans le programme mais pas forcement
a` lendroit ou` lon rencontre lidentificateur. En plus de reconnatre si le programme est bien une
phrase du langage, le parseur va construire un contexte qui va lui permettre davoir acc`es a` ces
informations en temps voulu.

4.1 Introduction aux syst`emes de type


La plupart des langages propose un syst`eme de type cest a` dire un ensemble de proprietes
associees a` chaque valeur. La theorie des types a genere e normement de recherches et de nouveaux concepts de programmation que nous ne mentionneront pas ici. Le but dun syst`eme de
typage est de specifier le comportement du programme plus precisement quavec une simple
grammaire hors contexte. Le syst`eme de type cree un deuxi`eme vocabulaire pour decrire la forme
et le comportement des programmes valides. Il est essentiellement utile pour :
e a` lexecution : il permet de detecter a` la compilation beaucoup plus de programmes
la suret
mal formes.
augmenter lexpressivite : il permet dexprimer au moins plus naturellement certains concepts
(par exemple par la surcharge doperateurs e lementaires comme laddition).
generer un meilleur code : il permet denlever des tests a` lexecution puisque le resultat est
connu a` la compilation (exemple : comment stocker une variable quand on ne connait pas
son type).
4.1.1 Composant dun syst`eme de typage
En general tous les langages ont un syst`eme de type base sur :
Des types de base En general les nombres : entiers de differentes tailles, reels (flottants), caract`eres et booleens (quelquefois les chane de caract`eres).
` partir de ces types de base, on peut construire des structures de
Des constructeurs de types A
donnees plus complexes pour representer les graphes, arbres, tableaux, listes, piles, etc. Parmi les
plus frequents on trouve : les tableaux avec des implementations diverses (ordre de linearisation,
contraintes sur les bornes) et e ventuellement des operations associees. Les chanes de caract`eres
sont quelquefois implementees comme des tableaux de caract`eres. Le produit cartesien de type :
type1 type2 (souvent appele structure) permett dassocier plusieurs objets de type arbitraire,
les unions permettent de rassembler plusieurs types en un seul. Les ensembles e numeres sont
souvent autorises et enfin les pointeurs permettent davoir une certaine liberte quant a` la manipulation de la memoire.

Equivalence
de types On peut decider que deux types sont dits e quivalents lorsquils ont le
meme nom (difficile a` gerer pour les gros programmes) ou lorsquils ont la meme structure (peut
reduire les erreurs detectees par le syst`eme de typage).
Des r`egles dinference Les r`egles dinference specifient pour chaque operateur les relations
entre le type des operandes et le type du resultat. Elles permettent daffecter un type a` toutes
les expressions du langage et de detecter des erreurs de type.

33

Inference de type pour toutes les expressions Souvent les langages imposent une r`egle de
declaration de type avant utilisation. Dans ce cas, linference et la detection derreur de type peut
se faire a` la compilation. Sinon (ou pour les constantes par exemple) le type est choisi lorsquon
rencontre lexpression, le mecanisme mis en place est plus complexe. Le syst`eme doit forcement
contenir des r`egles pour lanalyse interprocedurale et definir des signatures (types des arguments
et resultats) pour chaque fonction.
Remarque : Dans le cas des langages fortement types, linference de type se fait relativement
bien. Il existe des situations plus difficiles. Par exemple, dans certains langages les declarations
sont optionnelles, mais le typage doit e tre consistant quand meme (le type ne change pas dune
utilisation a` lautre). On introduit des algorithmes dunification pour trouver le type le plus large
compatible avec lutilisation qui est faite des variables. Dautres langages permettent le changement dynamique de types.

4.2 Les grammaires a` attributs


Les grammaires a` attributs ont e te proposees pour resoudre certains probl`emes de transmission dinformation dans larbre de syntaxe. La grammaire hors contexte du langage est fournie
par le concepteur du langage ainsi que la semantique associee. Le concepteur du compilateur
introduit alors un ensemble dattributs pour les terminaux et non terminaux du langage et un
ensemble de r`egles associees aux r`egles de la grammaire qui permet de calculer ces attributs.
Considerons la grammaire SBN = (T, N T, S, P ) suivante representant les nombres binaires
signes :

N umber Sign List

Sign
+

List
List Bit
T = {+, , 0, 1} N T = {N umber, Sign, List, Bit} S = {N umber} P =

| Bit

Bit

| 1
On va annoter cette grammaire avec des attributs permettant de calculer la valeur du nombre
binaire signe qui est represente :
Symbole
N umber
Sign
List
Bit

Attribut
valeur
negatif
position, valeur
position, valeur

On va ensuite definir les r`egles pour definir les attributs de chaque symbole. Ces r`egles sont
associees aux r`egles de la grammaire :
1.

N umber

Sign List

2.
3.
4.

Sign
Sign
List

+

Bit

5.

List0

List1 Bit

6.
7.

Bit
Bit

0
1

List.position
si Sign.negatif
alors N umber.valeur
sinon N umber.valeur
Sign.negatif
Sign.negatif
Bit.position
List.valeur
List1 .position
Bit.position
List0 .valeur
Bit.valeur
Bit.valeur
34

List.valeur
List.valeur
f alse
true
List.position
Bit.valeur
List0 .position + 1
List0 .position
List1 .valeur + Bit.valeur
0
2Bit.position

Par exemple, pour la chane 101, on va construire larbre suivant :

Number
Sign

val:5

val:5

List

pos:0

List

neg: true

List

pos:2
val:4

Bit

pos:2
val:4

pos:1
val:4

Bit

pos:0
val:1

Bit

pos:1
val:0

On peut representer les dependences entre les attributs :

Number
Sign

val:5

val:5

List

pos:0

List

neg: true

List

pos:2
val:4

Bit

pos:2
val:4

pos:1
val:4

Bit

pos:0
val:1

Bit

pos:1
val:0

Pour construire cet arbre, on a execute un parseur qui a cree une instance des attributs pour
chaque noeud et on a ensuite execute un evaluateur qui a calcule les valeurs de chaque attribut.
Les r`egles sur les attributs induisent des dependances implicites. Selon la direction des dependances
entre les attributs, on parle dattributs synthetises (lorsque le flot des valeurs va de bas en haut)
ou herites (lorsque le flot des valeurs va de haut en bas, ou lorsque les valeurs viennent de lui
meme ou de ses fr`eres). Il faut quil ny ait pas de circuits dans un tel graphe de dependance.
Pour les grammaires non circulaires, il existe des generateurs devaluateurs, qui e tant donnee
une grammaire a` attributs, vont generer un e valuateur pour les attributs. On a une certaine liberte pour lordre devaluation du moment quil respecte les dependances. On peut utiliser des

35

methodes dynamiques (qui maintiennent une liste dattributs prets a` e tre e value), des methodes
systematiques (parcours systematique de larbre jusqu`a convergence), des methodes basees sur
les r`egles (on analyse statiquement lordre dans lequel peuvent e tre executees les r`egles, voire on
cree une methode dediee a` certains attributs).
Limitations des grammaires a` attributs Pour certaines informations qui se transmettent de
proche en proche dans un seul sens, les grammaires a` attributs sont bien adaptees. En revanche
beaucoup de probl`emes pratiques apparaissent lorsquon veut transmettre dautre type dinformations.
Information non locale. Si lon veut transmettre une information non locale par des attributs
(e.g. le type dune variable stockee dans une table) , on doit ajouter des r`egles pour tous les
non terminaux afin de transporter cette information jusqu`a lendroit ou` elle est utile.
Place memoire. Sur des exemples realistes, levaluation produit un tr`es grand nombre dattributs. Le caract`ere fonctionnel de la transmission des attributs peut entraner une place
utilisee trop grande, or on ne sait pas a` priori quand est ce que lon va pouvoir desallouer
cette memoire.
Utilisation de larbre de parsing. Les attributs sont instancies sur les noeuds de larbre de
parsing (i.e. larbre representant exactement la derivation dans la grammaire). Comme on va
le voir, peu de compilateurs utilisent veritablement larbre syntaxique ; on utilise en general
un arbre de syntaxe abstrait (abstract syntax tree, AST) qui est une simplification de larbre
syntaxique. Il peut arriver quon utilise pas du tout de representation sous forme darbre, le
de la gestion additionnelle dun arbre doit e tre e value.
cout
Acc`es aux informations. Les informations recuperees par les attributs sont disseminees
dans larbre. Lacc`es aux informations ne se fait pas en temps constant. Ce probl`eme peut
e tre resolu par lutilisation de variables globales stockant les attributs. Dans ce cas, les
dependances entre attributs ne sont plus explicites mais par les acc`es aux cases du tableau
(le paradigme nest plus fonctionnel)
Pour toutes ces raisons, les grammaires a` attributs nont pas eu le succ`es quont eu les grammaires
hors contexte pour la definition de compilateurs. Elles restent neanmoins un moyen relativement
propre et efficace de calculer certaines valeurs lorsque la structure du calcul sy prete bien.

4.3 Traduction ad-hoc dirigee par la syntaxe


Lidee sous-jacente aux grammaires a` attributs est de specifier une sequence dactions associee
aux r`egles. Ce principe est maintenant implemente dans tous les compilateurs mais sous une
forme moins systematique que dans la theorie des grammaires a` attributs. Il ny aura quun seul
attribut par noeud (qui peut e tre considere comme la valeur associee au noeud) et cet attribut sera
systematiquement synthetise (calcule de bas en haut). Les actions sont specifiees par le concepteur
du compilateur et peuvent e tre des actions complexes referencant des variables globales (table des
symbole par exemple).
Le concepteur du compilateur ajoute des morceaux de code qui seront executes lors de lanalyse syntaxique. Chaque bout de code ou action est directement attachee a` une production dans
la grammaire. Chaque fois que le parseur reconnat cette production, il execute laction. Dans un
parser descendant, il suffit dajouter le code dans les procedures associees a` chaque r`egle. Dans
un parseur LR(1) les actions sont a` realiser lors des reductions.
Par exemple, voici une mani`ere simple de realiser le calcul de la valeur representee par un mot
de la grammaire SBN :
1.
2.
3.
4.
5.
6.
7.

N umber
Sign
Sign
List
List0
Bit
Bit

Sign List
+

Bit
List1 Bit
0
1

N umber.val
Sign.val
Sign.val
List.val
List0 .val
Bit.val
Bit.val

36

Sign.val List.val
1
1
Bit.val
2 List1 .val + Bit.val
0
1

4.4 Implementation
En pratique, il faut proposer un mecanisme pour passer les valeurs entre leur definition et
leurs utilisations, en particulier un syst`eme de nommage coherent. On peut utiliser la pile du parser LR(1) qui stockait deja un couple symbol, etat pour stocker maintenant un triplet : symbol, etat, valeur ,
dans le cas dun attribut par symbole. Pour nommer ces valeurs, on va utiliser la notation de Y acc
qui est tr`es repandu.
Le symbol $$ ref`ere lemplacement du resultat de la r`egle courante. Donc $$ 0 empilera 0
lorsque la reduction de la r`egle concernee aura lieu (en plus dempiler letat et le symbole). Les
emplacement des symboles de la partie droite de la r`egle sont representes par $1, $2 . . .$n. On
peut ree crire lexemple precedent avec ces conventions :
1.
2.
3.
4.
5.
6.
7.

N umber
Sign
Sign
List
List0
Bit
Bit

Sign List
+

Bit
List1 Bit
0
1

$$
$$
$$
$$
$$
$$
$$

$1 $2
1
1
$1
2 $1 + $2
0
1

Lorsque le programmeur veut effectuer une action entre levaluation de deux symbole en partie droite dune r`egle, il peut ajouter une r`egle, il peut aussi modifier le code du parser lorsquil
realise un shift.
On a plus de souplesse au niveau du traitement, en particulier on peut decider de construire
des structures de donnees globales accessibles depuis nimporte quelle r`egle. Cest par exemple
le cas pour la table des symboles (souvent implementee comme une table hachage) qui permet
dacc`eder rapidement aux informations contextuelles concernant les identificateurs.
Cette approche permet aussi de construire un arbre lors du parsing et de verifier les typages simultanement. Considerons par exemple la grammaire des expressions simples. On peut
construire un arbre de syntaxe complet tout en e valuant le type de chaque noeud
On dispose generalement dune serie de procedures M akeN odei (. . .) qui construisent des
noeuds ayant i fils. Lorsquon arrive aux feuilles (identificateurs ou constantes), le parseur fait
reference a` la table des symboles construite lors du parsing des declarations. La table des symboles peut contenir divers types dinformations comme le type, la taille de la representation a`
lexecution, les informations necessaires pour generer ladresse a` lexecution, la signature pour
les fonctions, les dimensions et bornes pour les tableaux etc.
En general on pref`ere une version compressee de larbre de syntaxe appelee arbre de syntaxe
abstraite. Pour cela, on garde les e lements essentiels de larbre de syntaxe mais on enl`eve les
noeuds internes qui ne servent a` rien. En pratique :
Pour les r`egles inutiles du type T erm F actor, laction a` realiser est simplement de passer
que de construire un nouveau
le pointeur sur le noeud de F actor directement a` T erm plutot
noeud.
Les operateurs sont representes differemment, le type doperateur (par exemple + ou )
nest plus une feuille mais un arbre binaire dont les feuilles sont les operandes.
Autre exemple : au lieu de generer un AST a` partir dune expression arithmetique, on gen`ere
directement du code assembleur I LOC. On suppose que lon dispose des procedures N extRegister
qui retourne un nom de registre non utilise, Adress qui retourne ladresse dune variable en
memoire, V alue pour les constantes et Emit qui gen`ere le code correspondant a` loperation.

37

5 Representations Intermediaires
La structure de donnees utilisee pour effectuer les differentes passes de compilation apr`es le
parsing est appelee la representation intermediaire. En fait il y aura plusieurs representations
intermediaires pour les differentes passes du compilateur. On distingue deux grandes classes de
representations intermediaires :
Les representations intermediaires a` base de graphes. Les algorithmes travaillant dessus
manipulent des noeuds, des arcs, des listes, des arbres, etc. Les arbres de syntaxe abstraite
construits au chapitre precedent sont de ce type.
Les representations intermediaires lineaires qui ressemblent a` du pseudo-code pour une
machine abstraite. Les algorithmes travaillent sur de simple sequences lineaires doperations.
en est un exemple.
La representation I LOC introduite plus tot
En pratique de nombreuses representations intermediaires melangent les deux notions (representation
flot). Ces representations influencent fortement la mani`ere
hybrides, comme le graphe de controle
dont le concepteur du compilateur va programmer ses algorithmes.

5.1 Graphes
On a vu la simplification de larbre de syntaxe en arbre de syntaxe abstrait. Cest loutil ideal
pour les traductions source a` source. Un des probl`emes des AST est quils peuvent prendre une
place relativement importante. On peut contracter un AST en un graphe acyclique (DAG) en regroupant les noeuds e quivalents. Par exemple, on peut representer lexpression x 2 + x 2 y
par :
+

*
x

*
2
x

*
y

*
2

DAG
AST
que la valeur de x ne change
Ce type doptimisation doit e tre fait avec precaution ; on doit e tre sur
pas entre les deux utilisations, on peut utiliser un syst`eme de table de hachage base sur la syntaxe externe des morceaux de code. On utilise cette optimisation dune part parce quelle reduit
la place utilisee mais aussi parce quelle expose des redondances potentielles. Notons que lon
utilise aussi de telles representations intermediaires pour des representations bas niveau
Le graphe de flot de controle (control flow graph, CFG) modelise la mani`ere dont le programme
entre les blocs de code dune procedure. Exemple de graphes de flot de
transfert le controle
:
controle
While (i < 100)
begin
stmt1
end
stmt2

If (x = y)
then stmt1
else stmt2
stmt3

38

If (x=y)

While i < 100


stmt1

stmt2

stmt1
stmt2

stmt3

Ce graphe permet de suivre lexecution qui aura lieu beaucoup mieux quavec lAST. On a le
choix de la granularite, cest a` dire de ce que contient un bloc (un noeud du graphe). En general
on utilise les noeuds pour representer les blocs de base (basic blocs). Les blocs de base representent
des portions de code dans lesquelles aucun branchement na lieu. En outre on impose souvent
quil ny ait quun seul point dentree pour un bloc de base. Les blocs de base sont representes en
utilisant une autre representation intermediaire : AST ou code lineaire trois adresses.
Le graphe de dependance (dependence graph, DG) sert a` representer le flot des donnees (valeurs)
entre les endroits ou` elles sont definies et les endroits ou` elles sont utilisees. Un noeud du graphe
de dependance represente une operation. Un arc connecte deux noeuds si lun doit e tre execute
avant lautre. Il peut y avoir plusieurs cas de figure : lecture dune variable apr`es son e criture,
e criture apr`es une lecture ou deux e critures dans la meme variable. graphe de dependance permet
de reordonner les calculs sans changer la semantique du programme. Il est generalement utilise
temporairement pour une passe particuli`ere puis detruit. Lexemple suivant montre un graphe
de dependance, les arcs e tiquetes par -1 indique que la dependance a` lieu sur un instruction de
literation precedente de la boucle :
1
2
3
4
5
6
7

x0
i1
while (i < 100)
if (a[i] > 0)
then x x + a[i]
ii+1
print x

4
1
1

On peut vouloir exprimer linformation de dependance de mani`ere plus ou moins precise.


Lanalyse de dependance devient complexe a` partir du moment ou` lon souhaite identifier les
e lements de tableau individuellement. Sans cette analyse precise, on est oblige de grouper toutes
les instructions modifiant le tableau a dans un nom appele a. Une technique pour raffiner cette
analyse a e te proposee par Paul Feautrier : array data-flow analysis.

5.2 IR lineaires
Beaucoup de compilateurs utilisent des representations intermediaires lineaires. Avant larrivee des compilateurs, cetait la mani`ere standard pour programmer. Historiquement, on a utilise
des codes une adresse (machine a` pile), codes deux adresses ou codes trois adresses.
Code une adresse Les codes une adresse (ou code de machine a` pile) suppose la presence dune
pile sur laquelle se font tous les calculs. Les operandes sont lus dans la pile (et depiles, on dit
detruits) et le resultat est pousse sur la pile. Par exemple pour la soustraction de deux entiers on
va executer push(pop()pop()). Ce type de code est apparu pour programmer des machines a` pile
qui ont eu du succ`es pendant un moment. Un des avantages des codes une adresse est quils sont
relativement compacts (la pile cree un espace de nommage implicite et supprime de nombreux
noms). Ils sont utilises pour les interpreteurs bytecode (Java, smalltalk)
code deux adresses Les codes deux adresses autorisent les operations du type x x op y (on
e crit x op y). Ils detruisent donc un de leurs operandes. Encore une fois, ils sont surtout utiles
pour les machines qui ont un jeu dinstructions deux adresses (PDP -11)
Voici trois representations lineaires de x 2 y, en code une adresse, deux adresses ou trois

39

adresses.

push
2
push
y
multiply
push
x
substract

t1
t2
t1
t3
t3

2
y
t2
x
t1

t1
t2
t3
t4
t5

2
y
t1 t2
x
t4 t3

5.2.1 code trois adresses


Les codes trois adresses autorisent les instructions du type : x y op z avec au plus trois
noms (en pratique il autorise des instructions avec moins de trois operandes aussi). Ci dessus, on
montre un exemple de notation infixe. Labsence de destruction des operateurs donne au compilateur plus de souplesse pour gerer les noms. Il y a plusieurs mani`eres dimplementer le code trois
adresses. La mani`ere la plus naturelle et la plus souple est avec des quadruplets (cible, operation,
deux operandes, c.f. repesentation ci-dessous a` gauche), mais on peut aussi utiliser des triplets
(lindex de loperation nomme son registre cible), le probl`eme est que la reorganisation du code
est difficile car lespace des noms est directement dependant du placement de linstruction. On
peut alors faire deux tables (triplets indirects). Cette derni`ere solution a` deux avantages sur le
quadruplet : on peut reordonner tr`es facilement les instructions et dautre part, si une instruction
est utilisee plusieurs fois elle peut netre stockee quune fois. Le probl`eme de ce stockage est quil
fait des acc`es memoire en deux endroits differents. Voici les trois propositions de stockage du
code trois adresses pour la meme expression x 2 y.
T arget
t1
t2
t3
t4
t5

Op

Arg1
2
y
(1)
x
(4)

Arg2

Op

(2)

(3)

Arg1
2
y
(1)
x
(4)

Arg2
(2)
(3)

Instr
(1)
(2)
(3)
(4)
(5)

Op

Arg1
2
y
(1)
x
(4)

Arg2
(2)
(3)

5.3 Static Single Assignment


Le code assembleur en assignation unique (static single assignment, SSA) est de plus en plus
et sur le flot des donnees. Un
utilise car il ajoute de linformation a` la fois sur le flot de controle
programme est en forme SSA quand
1. chaque nom de variable est defini une seule fois.
2. chaque utilisation ref`ere une definition.
Pour transformer un code lineaire en SSA, le compilateur introduit ce quon appelle des
f onctions et renomme les variables pour creer lassignation unique. Considerons lexemple suivant :
x ...
y ...
while (x < 100)
xx+1
y y+x
end
Le passage en SSA va donner :
x0 ...
y0 ...
if (x0 100) goto next
loop :
x1 (x0 , x2 )
y1 (y0 , y2 )
40

x2 x1 + 1
y2 y1 + x2
if (x2 < 100) goto loop
next : x3 (x0 , x2 )
y3 (y0 , y2 )
Notons que le while a disparu,la forme SSA est une representation utilisant le graphe de flot de
; la conservation du while impliquerait de rajouter des blocs ce que lon e vite de faire. On
controle
voit mieux ce qui se passe si on dessine le CDFG
x0<...
y0<...

x<...
y<...

If (x0>100)

while (x<100)

x1= (x0,x2)

x<x+1
y<y+1

y1= (y0,y2)
x2<x1+1
y2<y1+1
x3= (x0,x2)
y3= (y0,y2)

On utilise frequemment la relation de dominance entre instructions : a domine b signifie : tout


chemin arrivant a b passe par a. La definition formelle utilisee pour la SSA est la suivante :
1. chaque nom de variable est defini une seule fois.
2. Si x est utilise dans une instruction s et que s nest pas un fonction , alors la definition de
x domine s.
3. Si x est le ieme argument dune fonction dans le bloc n, alors la definition de x domine la
derni`ere instruction du ieme predecesseur de n.
Les fonctions ne sont jamais implementees dans le code genere, elle peuvent e tre supprimees
en introduisant des copies.
En revanche, on doit les manipuler dans la representation intermediaire. Elle presentent deux
singularites par rapport a` des instructions habituelles : lorsquelles sont executees, certains de
leurs arguments peuvent ne pas avoir e te definis et dautre part elles peuvent avoir un nombre
quelconque darguments (par exemple si on a une instruction case).

5.4 Nommage des valeurs et mod`ele memoire


Une des sources doptimisation du compilateur vient du fait que dans sa representation intermediaire, il nomme beaucoup plus de valeurs que ne le fait un langage de haut niveau. Par
exemple, sur un simple appel de tableau A[i, j], le codage dans un IR lineaire trois adresses du
type ILOC sera :
load
sub
loadI
mult
sub
add
loadI
add
load

1
rj , r1
10
r2 , r 3
ri , r1
r4 , r5
@A
r7 , r6
r8

r1
r2
r3
r4
r5
r6
r7
r8
rAij

//r2 j 1
// le tableau A est range par ligne, il des ligne de taille 10

//r6 10 (j 1) + i 1
//rAij @A + 10 (j 1) + i 1

On voit que beaucoup de valeurs sont renommees et peuvent e tre utilisees par la suite car les
registres ne sont pas e crases.
41

Cette implementation dun acc`es a` un tableau est faite en choisissant un mod`ele de memorisation
du type registre a` registre. Dans ce mod`ele de memorisation (ou mod`ele memoire), le compilateur
conserve les valeurs dans des registres sans se soucier du nombre de registres disponibles effectivement sur la machine cible. Les valeurs peuvent netre stockees en memoire que lorsque
la semantique du programme limpose (par exemple lors dun appel de procedure ou` ladresse
dune variable est passee en param`etre). On gen`ere alors explicitement les instructions load et
store necessaires.
Lautre alternative est dutiliser un mod`ele memoire a` memoire. Le compilateur suppose alors
que toutes les valeurs sont stockees en memoire, les valeurs sont chargees dans des registres juste
avant leur utilisation et remise en memoire juste apr`es. Lespace des noms est plus important.
On peut aussi choisir dintroduire des operations entre cases memoire. La phase dallocation de
registre sera differente suivant le mod`ele memoire choisi.
Pour les machines RISC on utilise plus frequemment le mod`ele registre a` registre qui est plus
proche du mode de programmation de larchitecture cible. Dautre part, cela permet de conserver
des informations au cours du processus de compilation du type cette valeur na pas e te changee
depuis son calcul et cela tr`es facilement si il est en SSA

5.5 La table des symboles


Le compilateur va rencontrer un nombre important de symboles, correspondant a` beaucoup
dobjets differents (nom de variables, constantes, procedures, labels, types, temporaires generes
par le compilateur,. . .). Pour chaque type dobjet le compilateur stocke des informations differentes.
On peut choisir de conserver ces informations soit directement sur lIR (stocker le type dune variable sur le noeud de lAST correspondant a` sa declaration par exemple) ou creer un repertoire
central pour toutes ces informations et proposer un mecanisme efficace pour y acceder. On appelle
ce repertoire la table des symboles (rien nempeche quil y ait plusieurs tables). Limplementation
de la table des symboles necessite une attention particuli`ere car elle est tr`es sollicitee lors de la
compilation.
Le mecanisme le plus efficace pour ranger des objets qui ne sont pas pre-indexes est la table
de hachage (hash table). Ces structures ont une complexite en moyenne de O(1) pour inserer et
rechercher un objet et peuvent e tre e tendues relativement efficacement. On utilise pour cela une
fonction de hachage h qui envoie les noms sur des petits entiers qui servent dindex dans la table.
Lorsque la fonction de hachage nest pas parfaite, il peut y avoir des collisions ; deux noms donnant
le meme index. On utilisera les primitives :
recherche(nom) qui retourne les informations stockees dans la table pour le nom nom (adresse
h(nom))
inserer(nom, inf o) pour inserer linformation inf o a` ladresse h(nom). Cela peut e ventuellement
provoquer lexpansion de la table.
Dans le cas dun langage nayant quune seule portee syntaxique, cette table peut servir directement a` verifier des informations de typage telles que la declaration dune variable que lon
utilise, la coherence du type etc. Malheureusement, la plupart des langages proposent differentes
portees, generalement imbriquees, pour les variables. Par exemple en C, une variable peut e tre
globale (tous les noms correspondant a` cette variable ref`erent la meme case memoire), locale a` un
fichier (avec le mot cle static), locale a` une procedure ou locale a` un bloc.
Par exemple :
static int w ;
int x ;
void example(a, b)
int a, b
{
int c ;
{
int b, z ;

/* niveau 0 */

/* niveau 1 */

/* niveau 2a */

42

}
{
int a, x ;
/* niveau 2b */
...
{
int c, x ;
/* niveau 3 */
b = a + b + c + x;
}
}
}
Pour compiler correctement cela, le compilateur a besoin dune table dont la structure permet de
resoudre une reference a` la definition la plus recente lexicalement. Au moment de la compilation
il doit e mettre le code effectuant la bonne reference.
On va donc convertir une reference a` un nom de variable (par exemple x) en un couple
niveau, decalage ou niveau est le niveau lexical dans lequel la definition de la variable referencee
a lieu et decalage est un entier identifiant la variable dans la portee consideree. On appelle ce
couple la coordonnee de distance statique (static distance coordinate).
La construction et la manipulation de la table des symboles va donc e tre un peu differente.
Chaque fois que le parseur entre dans une nouvelle portee lexicale, il cree une nouvelle table des
symboles pour cette portee. Cette table pointe sur sa m`ere cest a` dire la table des symboles de la
portee immediatement englobante. La procedure Inserer est la meme, elle naffecte que la table
des symboles courante. La procedure recherche commence la recherche dans la table courante
puis remonte dun niveau si elle ne trouve pas (et ainsi de suite). La table des symboles pour
lexemple considere ressemblera donc a` cela :
Niveau 0
Niveau 2b

Niveau 1

Niveau 3

b,...

x,...

x,...
x,...
c,...
c,...

w,...

Niveau 2a
b,...

example

a,...

a,...

z,...

Les noms generes par le compilateur peuvent e tre stockes dans la meme table des symboles.

43

6 Procedures
Les procedures sont les unites de base pour tous les compilateurs, grace a` la compilation
separee. Une procedure propose trois abstractions importantes :
Le mecanisme standard de passage des param`etres et du resultat
Abstraction du controle.
entre la procedure appelee et le programme appelant permet dappeler une procedure sans
connatre comment elle est implementee.
Lespace des noms. Chaque procedure cree un nouvel espace des noms protege. La reference
aux param`etres se fait independamment de leur nom a` lexterieur. Cet espace est alloue a`
lappel de la procedure et desalloue a` la fin de lexecution de la procedure, tout cela de
mani`ere automatique et efficace.
Interface externe. Les procedures definissent les interfaces entre les differentes parties dun
gros syst`eme logiciel. On peut par exemple, appeler des librairies graphiques ou de calculs
scientifiques. En general le syst`eme dexploitation utilise la meme interface pour appeler
une certaine application : appel dune procedure speciale, main.
La compilation dune procedure doit faire le lien entre la vision haut niveau (espace des noms
hierarchique, code modulaire) et lexecution materielle qui voit la memoire comme un simple
tableau lineaire et lexecution comme une simple incrementation du program counter (PC). Ce cours
sapplique essentiellement a` la classe des langages a` la Algol qui a introduit la programmation
structuree. Nous verrons un peu les nouveautes avec la programmation objet.

6.1 Abstraction du controle


et espace des noms
entre les procedures est le suivant : lorsquon appelle
Le mecanisme de transfert de controle
est donne a` la procedure appelee ; lorsque cette procedure appelee se
une procedure, le controle
est redonne a` la procedure appelante, juste apr`es lappel. Deux appels a` une
termine, le controle
meme procedure creent donc deux instances (ou invocations) independantes. On peut visualiser
cela en representant larbre dappel et lhistorique de lexecution qui est un parcours de cet arbre.
Considerons le programme Pascal suivant :
program M ain(input, output) ;
var x,y,...
procedure calc ;
begin { calc}
...
end ;
procedure call1 ;
var y...
procedure call2
var z : ...
procedure call3 ;
var y....
begin { call3 }
x :=...
calc ;
end ;
begin { call2 }
z :=1 ;
calc ;
call3 ;
end ;
begin { call1 }
call2 ;
...
end ;

44

begin { main }
...
call1 ;
end ;
la difference entre le graphe dappel (call graph) et larbre dappel (qui est la version deroulee du
graphe dappel correspondant a` une execution particuli`ere). Lhistorique dexecution est un parcours de larbre dexecution.
main

main

call1

call1

call2

call2

call3

call3

calc

calc

main appelle call1


call1 appelle call2
call2 appelle calc
calc revient a` call2
call2 appelle call3
call3 appelle calc
calc revient a` call3
call3 revient a` call2
call2 revient a` call1
call1 revient a` main

calc

Ce mode dappel et de retour sexecute bien avec une pile. Lorsque call1 appelle call2 , il pousse
ladresse de retour sur la pile. Lorsque call2 finit, il depile ladresse de retour et branche a` cette
adresse. Ce mecanisme fonctionne aussi avec les appels recursifs. Certains ordinateurs (Vax-11)
ont cable ces acc`es a` la pile dans les instructions dappel et de retour de procedure.
Certain langages autorisent une procedure a` retourner une autre procedure et son environ
nement a` lexecution (cloture).
La procedure retournee sexecute alors dans lenvironnement retourne. Une simple pile nest pas suffisante pour cela.
Dun point de vue vision logique de la memoire par le programme, la memoire lineaire est
generalement organisee de la facon suivante : la pile sert a` lallocation les appels de procedures
(elle grandit vers les petites adresses), le tas sert a` allouer la memoire dynamiquement (il grandit
vers les grandes adresses).
Code

static

Tas

Memoire libre

0
(petites adresses)

Pile

100000
(grandes adresses)

6.2 Enregistrement dactivation (AR)


Lors de lappel dune procedure, le compilateur met en place une region de la memoire appelee Enregistrement dactivation (activation record : AR) qui va implementer le mecanisme des appels et retours de la procedure. Cette zone memoire est en general allouee sur la pile. Sauf cas
exceptionnel, elle est desallouee lorsquon sort de la procedure.
Lenregistrement dactivation dune procedure q est accede par le pointeur denregistrement
dactivation (activation record pointer, ARP souvent appele frame pointer : FP). Lenregistrement dactivation comporte la place pour les param`etres effectifs, les variables locales a` la procedure, la sauvegarde de lenvironnement dexecution de la procedure appelante (essentiellement les registres)
45


ainsi que dautres variables pour controler
lexecution. En particulier, il contient ladresse de retour de la procedure, un lien dacc`es permettant dacceder a` dautres donnees : (access link) et un
pointeur sur lARP de la procedure appelante.

Parametres

sauvegarde
registres
resultat
adresse de retour
ARP

access link
ARP appelant
Variables
locales

Pile libre

La plupart des compilateurs dedient un registre au stockage de lARP. Ici, on supposera que ou
rarp contient en permanence lARP courant. Ce qui est proche de ladresse pointee par lARP est de
taille fixe, le reste (generalement de part et dautre de lARP est de taille dependant de lappel).
Variables locales. Lors de lappel de la procedure, le compilateur calcule la taille des variables
locales, reference pour chaque variable le niveau dimbrication et le decalage par rapport a` lARP.
Il enregistre ces informations dans la table des symboles. La variable peut alors e tre accedee par
linstructions :
loadAI rarp , decalage
Lorsque la taille de la variable ne peut pas e tre connue statiquement (provenant dune entree
du programme), le compilateur alloue un pointeur sur une zone memoire qui sera allouee sur
le tas (heap) lorsque sa taille sera connue. Le compilateur doit alors generer un code dacc`es
leg`erement different, ladresse de la variable est recuperee avec loadA0, puis un deuxi`eme load
permet dacceder a` la variable. Si celle-ci est accedee souvent, le compilateur peut choisir de
conserver son adresse dans un registre.
Le compilateur doit aussi se charger de linitialisation e ventuelle des variables. Pour une variable statique (dont la duree de vie est independante de la procedure), le compilateur peut inserer
la valeur directement au moment de ledition de lien. Dans le cas general il doit generer le code
permettant dinitialiser la variable lors de chaque appel de procedure (c.a.d. rajouter des instructions avant lexecution de la premi`ere instruction de la procedure).
sauvegarde des registres Lorsque p appelle q, soit p soit q doit se charger de sauvegarder les registres (ou au moins une partie dentre eux qui representent le contexte dexecution de la procedure
p) et de les restaurer en fin de procedure.
Allocation de lenregistrement dactivation Le compilateur a plusieurs choix, en general la
procedure appelante ne peut pas allouer compl`etement lenregistrement dactivation, il lui manque
des donnees comme la taille des variables locales.
Lorsque la duree de vie de lAR est limitee a` celle de la procedure, le compilateur peut allouer
de lallocation et de la desallocation est faible. On peut aussi implementer
sur la pile. Le surcout
des donnees de tailles variables sur la pile, en modifiant dynamiquement la fin de lAR.

46

ARP
ARP appelant
Variables
locales
pointeur sur A

Ancien TOS
esopace pour A
TOS

Dans certains cas, la procedure doit survivre plus longtemps que son appel (par exemple si la

procedure retourne une cloture


qui contient des references aux variables locales a` la procedure).
Dans ce cas, on doit allouer lAR sur le tas. On desalloue une zone lorsque plus aucun pointeur ne
pointe dessus (ramasse miette).
Dans le cas ou une procedure q nappelle pas dautre procedure, q est appelee une feuille. Dans
ce cas, lAR peut e tre alloue statiquement et non a` lexecution (on sait quun seul q sera actif a` un
instant donne). Cela gagne le temps de lallocation a` lexecution.
Dans certain cas, le compilateur peut reperer que certaines procedures sont toujours appelees
dans le meme ordre (par exemple, call1 , call2 et call3 ). Il peut alors decider de les allouer en meme
temps (avantageux surtout dans le cas dune allocation sur le tas).

6.3 Communication des valeurs entre procedures


Il y deux modes principaux de passage des param`etres entre les procedures : passage par
valeur et passage par reference. Considererons lexemple C suivant :
int proc(x, y)
int x, y;
{
x = 2 x;
y = x + y;
return y ;
}

c = proc(2, 3);
a = 2;
b = 3;
c = proc(a, b);
a=2
b = 3;
c = proc(a, a);

Dans ce cas, il y a passage de param`etres par valeurs : lappelant copie les valeurs du param`etre
effectif dans lespace reserve pour cela dans lAR. Le param`etre formel a donc son propre espace
de stockage, seul son nom ref`ere a` cette valeur qui est determinee en e valuant le param`etre effectif
au moment de lappel. Voici le resultat de lexecution qui est generalement assez intuitif :
appel par valeur
proc(2, 3)
proc(a, b)
proc(a, a)

a in

2
2

a out

2
2

b in

3
3

b out

3
3

resultat
7
7
6

Lors dun appel par reference, lappelant stocke dans lAR de lappele un pointeur vers lendroit ou` se trouve le param`etre (si cest une variable, cest ladresse de la variable, si cest une
expression, il stocke le resultat de lexpression dans son propre AR et indique cette adresse). Un
appel par reference implique donc un niveau de plus dindirection, cela implique deux comportements fondamentalement differents : (1) la redefinition du param`etre formel est transmise au
47

param`etre effectif et (2) un param`etre formel peut e tre lie a` une autre variable de la procedure
appelee (et donc modifiee par effet de bord de la modification de la variable locale), ce qui est un
comportement tr`es contre-intuitif. Voici un exemple en PL/I qui utilise le passage par references :
procedure proc(x, y)
returns fixed binary
declare x,y fixed binary ;
begin
x = 2 x;
y = x + y;
return y ;
end

c = proc(2, 3)
a = 2;
b = 3;
c = proc(a, b)
a=2
b = 3;
c = proc(a, a);

et le resultat de lexecution :
appel par valeur
proc(2, 3)
proc(a, b)
proc(a, a)

a in

2
2

a out

4
8

b in

3
3

b out

7
8

resultat
7
7
8

Il existe dautres methodes exotiques pour passer les param`etres ; par exemple, le passage par
nom (Algol) : Une reference au param`etre formel se comporte exactement comme si le param`etre
effectif avait e te mis a` sa place. Le passage par valeur/resultat (certains compilateurs fortran)
execute le code de la procedure comme dans un passage par valeur mais recopie les valeurs des
param`etres formels dans les param`etres effectifs a` la fin (sauf si ces param`etres sont des expressions).
La valeur retournee par la procedure doit e tre en general stockee en dehors de lAR car elle est
utilisee alors que la procedure a termine. LAR inclut une place pour le resultat de la procedure. La
procedure appelante doit alors elle aussi allouer de la place pour le resultat dans son propre AR et
stocker un pointeur dans la case prevue pour le resultat dans lAR de lappelant. Mais si la taille
du resultat nest pas connue statiquement, il sera probablement alloue sur le tas et la procedure
appelee stockera un pointeur sur ce resultat dans lAR de lappelant. Quelque soit le choix fait,
il doit e tre compatible avec des programmes compiles par ailleurs, sous peine de provoquer des
erreurs a` lexecution incomprehensibles.

6.4 Adressabilite
Pour la plupart des variables, le compilateur peut e mettre un code qui gen`erera ladresse
de base (ladresse a` partir de laquelle on compte le decalage pour retrouver la variable) puis
le decalage. Le cas le plus simple est le cas des variables locales ou` ladresse de base est lARP. De
meme, lacc`es aux variables globales ou statiques, est fait de la meme mani`ere, ladresse de base
e tant stockee quelque part. Lendroit ou` est cette adresse de base est en general fixe a` ledition de
lien, le compilateur introduit un nouveau label (par exemple &a pour la variable globale a) qui
representera ladresse a` laquelle a est stockee
Pour les variables des autres procedures, cest un peu plus complexe ; il faut un mecanisme
dacc`es de ces variables a` lexecution a` partir des coordonnees de distance statique. Plusieurs
solutions sont possibles.Considerons par exemple que la prodedure calc reference une variable a
declaree au niveau l dans la procedure call1 . Le compilateur a donc acc`es a` une information du
type l, decalage et il connat le niveau de calc. Le compilateur doit dabord generer le code pour
retrouver lARP de la procedure call1 , puis utiliser decalage pour retrouver la variable.
lien dacc`es Dans ce schema, le compilateur ins`ere dans chaque AR un pointeur sur lAR de
lancetre immediat (cest a` dire dont la portee est immediatement englobante). On appelle ce
pointeur un lien dacc`es (Access link) car il sert a` acceder aux variables non locales. Le compilateur
e met le code necessaire a` descendre dans les AR successifs jusqu`a lAR recherche.

48

niveau 0
niveau 1

niveau 2
Parametres

Parametres

sauvegarde
registres

Parametres

sauvegarde
registres
resultat
adresse de retour

sauvegarde
registres
resultat
adresse de retour
ARP

access link

resultat

access link

adresse de retour

ARP appelant

access link

Variables
locales

ARP appelant
Variables
locales

ARP appelant
Variables
locales

Pile libre

Par exemple, en supposant que le lien dacc`es soit stocke avec un decalage 4 par rapport a` lARP.
Voici le type de code pour differents acc`es a` differents niveaux (notons que quelquefois on pref`ere
acceder aux variables en utilisant le sommet de pile (P) mais le mecanisme est similaire, avec des
decalages positifs) :
2, 24 loadAI rarp , 24 r2
1, 12 loadAI rarp , 4
r1
loadAI r1 , 12
r2
0, 16 loadAI rarp , 4
r1
loadAI r1 , 4
r1
loadAI r1 , 16
r2
Souvent la procedure appelante est celle de niveau lexical juste englobant, le lien dacc`es est alors
simplement lARP de lappelant mais pas toujours (voir par exemple lappel a` calc dans lexemple
plus haut). Dans ce cas, il faut maintenir le lien dacc`es dans lARP.
Lors de la construction de lAR, le lien dacc`es est calcule en remontant par le lien dacc`es de
lappelant jusquau niveau p`ere commun a` lappelant et a` lappele (qui est le niveau sur lequel il
faut pointer avec le lien dacc`es de lappele).
acc`es global On peut aussi choisir dallouer un tableau global (global display) pour acceder
les ARP des procedures. Chaque acc`es a` une variable non locale devient un reference indirecte.
Comme des procedures de meme niveau peuvent sappeler, il faut un mecanisme de sauvegarde
` lappel dune procedure de niveau l, on stocke le pointeur sur le
du pointeur sur chaque niveau. A
niveau l dans lAR de la procedure appelee et on le remplace (dans le tableau global) par lARP de
la procedure appelee. Lorsque lon sort de la procedure, on restaure le pointeur de niveau l dans
le tableau. Lacc`es aux differents niveaux presente des differences de complexite moins marquees,
mais il faut deux acc`es a` la memoire (un a` lappel, un au retour) alors que le mecanisme du lien
rien au retour. En pratique, le choix entre les deux depend du rapport entre le
dacc`es ne coute
nombre de references non locales et le nombre dappels de procedure. Dans le cas ou` les variables
peuvent survivre aux procedures les ayant cree es, lacc`es global ne fonctionne pas.

49

Tableau global
niveau 0
niveau 1
niveau 2
niveau 3
niveau 0
niveau 1

Parametres

Parametres

niveau 2

sauvegarde
registres

Parametres

sauvegarde
registres
resultat
adresse de retour

resultat

sauvegarde
registres
resultat
adresse de retour
ARP

Sauvegarde Ptr

Sauvegarde Ptr

adresse de retour

ARP appelant

Sauvegarde Ptr

Variables
locales

ARP appelant
Variables
locales

ARP appelant
Variables
locales

Pile libre

Voici le type de code pour differents acc`es a` differents niveaux :


2, 24
1, 12

0, 16

6.5

loadAI
loadI
loadAI
loadAI
loadI
loadAI

rarp , 24 r2
disp
r1
r1 , 4
r1
r1 , 12
r2
disp
r1
r1 , 16
r2

Edition
de lien

Ledition de lien est un contrat entre le compilateur, le syst`eme dexploitation et la machine


cible qui repartit les responsabilites pour nommer, pour allouer des ressources, pour ladressabilite et pour la protection. Ledition de lien autorise a` generer du code pour une procedure
independamment de la mani`ere avec laquelle on lappelle. Si une procedure p appelle une procedure
q, leurs codes auront la forme suivante : chaque procedure contient un prologue et un e pilogue,
chaque appel contient un pre-appel et un post-appel.
Pre-appel Le pre-appel met en place lAR de la procedure appelee (allocation de lespace
necessaire et remplissage des emplacements memoire). Cela inclut levaluation des param`etres
effectifs, leur stockage sur la pile, lempilement de ladresse de retrour, lARP de la procedure
appelante, et e ventuellement la reservation de lespace pour une valeur de retour. La procedure
appelante ne peut pas connatre la place necessaire pour les variables locales de la procedure
appelee.
Post-appel Il y a liberation des espaces alloues lors du pre-appel.
Prologue Le prologue cree lespace pour les variables locales (avec initialisation e ventuelle).
Si la procedure contient des references a` des variables statiques, le prologue doit preparer

ladressabilite en chargeant le label approprie dans un registre. Eventuellement,


le prologue
met a` jour le tableau dacc`es global (display)

Epilogue
Lepilogue lib`ere lespace alloue pour les variables locales, il restore lARP de lappelant et saute a` ladresse de retour. Si la procedure retourne un resultat, la valeur est
transferee l`a ou` elle doit e tre utilisee.
De plus, les registres doivent e tre sauvegardes, soit lors du pre-appel de lappelant (callersave) soit lors du prologue de lappele (callee save). Voici un exemple de division des taches (sans
mentionner les liens dacc`es ou le tableau dacc`es global) :

50

appel

retour

appele
pre-appel
alloue les bases de lAR
e value et stocke les param`etres
stocke ladresse de retour et lARP
sauve les registres (caller-save)
saute a` ladresse appelee
post-appel
lib`ere les bases de lAR
restaure les registres (caller save)
transmet le resultat et restaure les param`etres
par reference

appelant
prologue
preserve les registres (callee save)
e tend lAR pour les donnees locales
trouve lespace des donnees statiques
initialise les variables locales
execute le code
epilogue
restaure les registres (callee save)
lib`ere les donnees locales
restaure lARP de lappelant

saute a` ladresse de retour


En general, mettre des fonctionnalites dans le prologue ou lepilogue produit du code plus
compact (le pre-appel et post-appel sont e crits pour chaque appel de procedure).

6.6 Gestion memoire globale


Chaque programme comporte son propre adressage logique. Lexecution de plusieurs processus en parall`ele (decoupage du temps) implique des r`egles strictes pour la gestion memoire.
Organisation physique de la memoire On a vu que le compilateur voyait la memoire de la
mani`ere suivante :
Code

static

Tas

Memoire libre

Pile

0
(petites adresses)

100000
(grandes adresses)

En realite, le syst`eme dexploitation assemble plusieurs espaces dadressage virtuels ensemble


dans la memoire physique. Les differentes memoires virtuelles sont decoupees en pages qui sont

arrangees dans la memoire physique de mani`ere compl`etement incontrolable.


Cette correspondance est maintenue conjointement par le materiel et le syst`eme dexploitation et est en principe
transparente pour le concepteur de compilateur.
Vue du compilateur

adresses virtuelles

Code

static

Tas

Memoire libre

Pile

Code

static

Tas

Memoire libre

Pile

Code

static

Tas

Memoire libre

Pile

..........

10000000

0
adresses physiques

Vue matrielle

51

Vue de lOS

Cette correspondance est necessaire car lespace adressable en adresse virtuelle est beaucoup
plus grand que lespace memoire disponible en memoire vive. La memoire de la machine fonctionne alors comme un gros cache : lorsquil ny a plus de place, on swappe un page, cest a` dire
quon la copie sur le disque (en fait cest un cache a` deux niveaux car il y a aussi le cache de donnee
Ladressage dun case memoire depuis un programme comporte donc
et dinstruction bien sur).
les operations suivantes :
traduction de ladresse virtuelle en adresse physique
Verification que la page correspondant a` ladresse physique est en memoire et (eventuellement)
generation dun defaut de page (page fault)
lire (ou e crire) la donnee effectivement. Comme tout syst`eme de cache, lecriture necessite
quelques precautions.
En general la page des tables (page indiquant la correspondance entre pages virtuelles et pages
physiques) se trouve elle-meme en memoire. Pour accelerer le decodage, il existe souvent un
composant materiel specifique, dedie a` la traduction entre les adresses virtuelles et les adresses
physiques (memory management unit, MMU). Aujourdhui, sur la majorite des syst`emes, la MMU
consiste en un translation lookahead buffer (TLB), cest un petit cache contenant la correspondance
entre page virtuelle et page physique pour les pages les plus recemment utilisees.
Voici quelques valeurs typiques pour un syst`eme memoire sur un ordinateur :
TLB
Taille du TLB : entre 32 et 4096 entrees
Penalite dun defaut de TLB 10-100 cycles
Taux de defauts de TLB : 0.01%-1%
cache
Taille du cache 8 a` 16 000 Koctets en blocs de 4 a` 256 octets
Penalite dun defaut de cache : 10 a` 100 cycles
Taux de defauts de cache : 0.1%-10%
memoire paginee
Taille de la memoire : 8 a` 256 000 Koctets organisee en pages de 4 a` 256 Koctets
Penalite dun defaut de page : entre 1 et 10 millions de cycles
Taux de defauts de page : 105 %-104 %.
La figure 1, tiree du Hennessy-Patterson [HP98] presente lorganisation du syst`eme memoire
sur les DECStation 3100.
Organisation de la memoire virtuelle
Alignement et padding. Les machines cibles ont des restrictions quant a` lalignement des
mots en memoire ; par exemple les entiers doivent commencer a` une cesure de mot (32 bits),
les longs mots doivent commencer a` une cesure long mot (64 bits), ce sont les r`egles dalignement (alignment rules). Pour respecter ces contraintes, le compilateur classe ses variables
en groupes du plus restrictif (du point de vue des restrictions liees a` lalignement) au moins
restrictif. Lorsque quil nest pas possible de respecter les r`egles dalignement sans faire de
trou, le compilateur ins`ere des trous (padding).
` ce niveau, il est important de prendre en compte que toutes les ma Gestion des caches. A
chines poss`edent un cache. Les performances liees au cache peuvent e tre ameliorees en augmentant la localite. Par exemple, deux variables succeptibles detre utilisees en meme temps
vont partager le meme bloc de cache. La lecture de lune des deux rapatriera lautre dans le
cache a` la meme occasion. Si cela nest pas possible, le compilateur peut essayer de sassurer que ces deux donnees sont dans des blocs qui ne vont pas e tre mis sur la meme ligne de
cache.
En general, le probl`eme est tr`es complexe. Le compilateur va se focaliser sur le corps des
boucles et il peut choisir dajouter des trous entre certaines variables pour diminuer leur
chance detre en conflit sur le cache.
Cela marche bien lorsque le cache fonctionne avec des adresses virtuelles (virtual adress
cache), cest a` dire que le cache recopie des morceaux de memoire virtuelle (et donc lecart
des donnees en memoire virtuelle influe vraiment sur leur presence simultanee dans le
cache). Si le cache fonctionne avec la memoire physique (physical adress cache), la distance
52

F IG . 1 Implementation du TLB et du cache sur les DECStation 3100 (figure tiree de [HP98]).

53


entre deux donnees nest contolable
que si les deux donnees sont dans la meme page, le
compilateur sinteresse alors aux donnees referencees en meme temps sur la meme page.
gestion du tas

La gestion du tas doit assurer (dans lordre dimportance)

1. lappel (ou pas) explicite a` la routine de liberation de memoire,


2. des routines efficaces dallocation et de liberation de memoire,
3. la limitation de la fragmentation du tas en petit blocs.
La methode traditionnelle est appelee allocation du premier (first-fit allocation). Chaque bloc
alloue a un champ cache qui contient sa taille (en general juste avant le champ retourne par la
procedure dallocation). Les blocs disponibles pour allocation sont accessibles par une liste dite
liste libre (free list), chaque bloc sur cette liste contient un pointeur vers le prochain bloc libre dans
la liste.
Bloc alloue

bloc libre

taille

taille
next

Lallocation dun bloc se fait de la mani`ere suivante (il demarre avec un unique bloc libre contenant toute la memoire allouee au tas) :
parcours de la liste libre jusqu`a la decouverte dun bloc libre de taille suffissante.
si le bloc est trop grand, creation dun nouveau bloc libre et ajout a` la liste libre.
la procedure retourne un pointeur sur le bloc choisi.
si la procedure ne trouve pas de bloc suffisamment grand, elle essaie daugmenter le tas. Si
elle y arrive, elle retourne un bloc de la taille voulue.
Pour liberer un bloc, la procedure la plus simple est dajouter le bloc a` la liste libre. Cela produit une procedure de liberation rapide et simple. Mais cela conduit en general a` fragmenter trop
vite la memoire. Pour essayer dagglomerer les blocs, lallocateur peut tester le mot precedant le
champ de taille du bloc quil lib`ere. Si cest un pointeur valide (i.e. si le bloc precedent est dans
la liste libre), on ajoute le nouveau bloc libre a` celui-ci en augmentant sa taille et mettant a` jour le
pointeur de fin. Pour mettre a` jour efficacement la liste libre, il est utile quelle soit doublement
chanee.
free list

Avant allocation

taille
next

free list
Aprs allocation

taille

taille

newbloc

next

Les allocateurs modernes prennent en compte le fait que la taille de memoire est de moins en
moins un param`etre critique alors que la vitesse dacc`es a` la memoire lest de plus en plus. De
plus, on remarque que lon alloue frequemment des petites tailles standard et peu frequemment
des grandes tailles non standard. On utilise alors des zones memoires differentes pour chaque
54

taille, commencant a` 16 octets allant par puissance de deux jusqu`a la taille dune page (2048 ou
4096 octets). On alloue des blocs dune taille redefinie. La liberation nessaie pas dagreger des
blocs
liberations memoire implicites Beaucoup de langages specifient que limplementation doit
liberer la memoire lorsque les objets ne sont plus utilises. Il faut donc inclure un mecanisme
pour determiner quand un objet est mort. Les deux techniques classiques sont le comptage des
references (reference couting) et le ramasse-miettes (garbage collecting).
Le comptage des references augmente chaque objet dun compteur qui compte le nombre de
pointeurs qui referencent cet objet. Chaque assignation a` un pointeur modifie deux compteurs de
reference (celui de lancienne valeur pointee et celui de la nouvelle valeur pointee). Lorsque le
compteur dun objet tombe a` zero, il peut e tre ajoute a` la liste libre (il faut alors enregistrer le fait
que tous les pointeurs de cet objet sont desactives).
Ce mecanisme pose plusieurs probl`emes :
Le code sexecutant a besoin dun mecanisme permettant de distinguer les pointeurs des
autres donnees. On peut e tendre encore les objets pour indiquer quel champ de chaque objet
est un pointeur ou diminuer la zone utilisable dun pointeur et tagger chaque pointeur.
Le travail a` faire pour une simple liberation de pointeur peut e tre arbitrairement gros. Dans
les syst`emes soumis a` des contraintes temps-reel, cela peut e tre un probl`eme important. On
emploie alors un protocole plus complexe qui limite la quantite dobjets liberes en une fois.
Un programme peut former des graphes cycliques de pointeurs. On ne peut pas mettre
a` zero les compteurs des objets sur un circuit, meme si rien ne pointe sur les circuits. Le
programmeur doit casser les circuits avant de liberer le dernier pointeur qui pointe sur ces
circuits.
La technique du ramasse-miettes ne lib`ere rien avant de ne plus avoir de place en memoire.
Quand cela arrive, on arrete lexecution et on scanne la memoire pour traquer les pointeurs pour
decouvrir les zones memoire inoccupees. On lib`ere alors la memoire en compactant (ou pas) la
memoire a` la meme occasion.
La premi`ere phase determine quel ensemble dobjets peut e tre atteint a` partir des pointeurs
des variables du programme et des temporaires generes par le compilateur. Les objets non accessibles par cet ensemble sont consideres comme morts. La deuxi`eme phase lib`ere explicitement
ces objets et les recycle. Les deux techniques de ramasse-miettes (mark-sweep et copying collectors)
diff`erent par cette deuxi`eme phase de recyclage.
La premi`ere phase proc`ede par marquage des objets, chaque objet poss`ede un bit de marquage
(mark bit). On commence par mettre tous ses drapeaux a` 0 et on initialise une liste de travail qui
contient tous les pointeurs contenus dans les registres et dans les AR des procedures en cours.
Ensuite on descend le long des pointeurs et lon marque chaque objet rencontre, en recommencant
avec les nouveaux pointeurs. Exemple dalgorithme :
Initialiser les marques a` 0
W orkList { valeurs des pointeurs des AR et registres}
While (W orkList = 0)
p tete de W orkList
if (p objet non marque)
marquer p objet
ajouter les pointeurs de p objet a` W orkList
La qualite de lalgorithme (precis ou approximatif) vient de la technique utilisee pour identifier
les pointeurs dans les objets. Le type de chaque objet peut e ventuellement e tre enregistre dans
lobjet lui-meme (header), dans ce cas on peut determiner exactement les pointeurs. Sinon tous les
champs dun objet sont consideres comme des pointeurs. Ce type de ramasse-miettes peut e tre
utilise pour des langages qui nont habituellement pas de ramasse-miettes (ex : C).
Les ramasse-miettes du type mark-sweep font une simple passe lineaire sur le tas et ajoutent les
objets morts a` la liste libre comme lallocateur standard. Les ramasse-miettes du type copy-collector
55

travaillent avec un tas divise en deux parties de taille e gale (old et new) ; lorsquil ny a plus de
place dans old, ils copient toutes les donnees vivantes (et donc les compactent) de old dans new
et e changent simplement les noms old et new (la copie peut e tre faite apr`es la premi`ere phase ou
incrementalement au fur et a` mesure de la premi`ere phase).

56

7 Implementations de mecanismes specifiques


7.1 Stockage des valeurs
En plus des donnees du programme il y a de nombreuses autres valeurs qui sont stockees
comme valeurs intermediaires par le compilateur (c.f. lacc`es aux tableaux au chapitre precedent).
On a vu quune donnee pouvait, suivant sa portee et son type, e tre stockee en differents endroits
de la memoire (on parle de classe de stockage, storage class). On distingue le stockage local a` une
procedure, le stockage statique dune procedure (objet dune procedure destine a` e tre appele en
dehors de lexecution de la procedure), le stockage global et le stockage dynamique (tas). Ces
differentes classes de stockage peuvent e tre implementees de differentes mani`eres.
Le concepteur doit aussi decider si une valeur peut resider dans un registre. Il peut dans ce cas
lallouer dans un registre virtuel, qui sera, ou pas, recopie en memoire par la phase dallocation
quune valeur peut e tre stockee dans un registre, le compilateur
de registres (spill). Pour e tre sur
doit connatre le nombre de noms distincts dans le code qui nomment cette valeur. Par exemple
dans le code suivant, le compilateur doit assigner une case memoire pour la variable a
void calc()
{
int a, b ;
...
b = &a ;
...
}
Il ne peut meme pas laisser la variable a dans un registre entre deux utilisations a` moins detre
capable de prouver que le code ne peut avoir acc`es a` b entre les deux. Une telle valeur est une
valeur ambigue. On peut creer des valeurs ambigue avec les pointeurs mais aussi avec le passage
de param`etres par reference. Les mecanismes pour limiter lambigute sont importants pour les
performance de lanalyse statique (ex : le mots cles restric en C qui limite le type de donnees
sur lequel peut pointer un pointeur).
Il faut aussi tenir compte dun certain nombre de r`egles specifiques aux machines cibles. Il
y a plusieurs classes de registres (ex : floatting point, general purpose). Les registres peuvent
e tre distribues sur differents register file (file/fichier de registres) qui sont associes en priorites a`
certains operateurs.

7.2 Expressions booleennes et relationnelles


Comme pour les operations arithmetiques, les concepteurs darchitectures proposent tous
des operations booleennes natives dans les processeurs. En revanche, les operateurs relationnels
(comparaison, test degalite) ne sont pas normalises. Il y a deux mani`eres de representer les valeurs booleennes et relationnelles : la representation par valeur (value representation) et le codage
par position (positional encoding).
Dans la representation par valeur, le compilateur assigne des valeurs aux booleens Vrai et
Faux (en general des valeurs enti`eres) et les operations booleennes sont programmees pour cela.
Le code a` generer pour une expression booleenne est alors simple : pour b c d le code est :
not rd
and rc , r1
or rb , r2

r1
r2
r3

Pour la comparaison, si la machine supporte des operateurs de comparaison, le code est trivial :
pour x < y :
cmp LT rx , ry r1
de flot peuvent alors se faire a` partir dune operation de branchement
Les operations de controle
conditionnel simple :
cbr r1 L1 , L2
57

Dans le codage par position, la comparaison va mettre a` jour un registre de code condition
(condition code register), qui sera utilise dans une instruction de branchement conditionnel (Iloc
poss`ede les deux possibilites). Dans ce cas, le code genere pour x < y (sl est utilise comme un
booleen dans une expression booleenne) est :

L1 :
L2 :
L3 :

comp
cbr LT
loadI
jumpI
loadI
jumpI
nop

rx , r y
cc1
true
f alse

cc1
L1 , L2
r2
L3
r2
L3

Ce type de codage permet doptimiser les instructions du type


If

(x < y)
then statement1
else statement2

a` condition que le compilateur sache les reconnatre.


Lencodage par position peut paratre moins intuitif a` priori, mais en fait, il permet deviter
lassignation de valeur booleenne tant quune assignation nest pas explicitement necessaire. Par
exemple, considerons lexpression : (a < b) (c > d) (e < f ). Un generateur de code avec
encodage par position produirait :

L1 :
L2 :
L3 :
L4 :
L5 :

comp
cbr LT
comp
cbr LT
comp
cbr LT
loadI
jumpI
loadI
jumpI
nop

ra , r b
cc1
rd , r c
cc2
re , r f
cc3
true
f alse

cc1
L3 , L1
cc2
L2 , L4
cc3
L3 , L4
r1
L5
r1
L5

Lencodage par position permet de representer la valeur dune expression boleenne implicitement
du programme. La version en representation par valeur serait :
dans le flot de controle
cmp LT
cmp GT
cmp LT
and
or

ra , rb
rc , r d
re , rf
r2 , r 3
r1 , r 4

r1
r2
r3
r4
r5

Elle est plus compacte (5 instructions au lieu de 10) mais pourra e tre plus longue a` lexecution. Ici
elle ne lest pas, mais on voit comment elle peut letre : deux operations par profondeur de larbre
au lieu dune operation par noeud de larbre.
Dans le mode de representation par position, on peut optimiser le code produit par une technique appelee e valuation court-circuit (short circuit evaluation) on peut calculer la mani`ere qui
demandera le moins devaluation pour decider du resultat (par exemple en C : (x != 0 && y/x
>0.01) nevaluera pas la deuxi`eme condition si x vaut 0.
En plus de lencodage choisi, limplementation des operations de relation va dependre fortement de limplementation des operateurs de relation sur la machine cible. Voici quatre solutions
envisageables avec leur expression en Iloc pour laffectation : x (a < b) (c < d). Pour faire
de flot
une comparaison compl`ete, il faudrait comparer les codes sur une operation de controle
(type if-then-else).
58

Code condition direct. Loperateur de condition va mettre a` jour un registre de code condition (code condition register), la seule instruction qui peut lire ce registre est le branchement
conditionnel (avec six variantes :<, , =, etc.). Si le resultat de la comparaison est utilise,
on doit le convertir explicitement en booleen. Dans tous les cas, le code contient au moins
un branchement conditionnel par operateur de comparaison. Lavantage vient du fait que
le registre de code condition peut e tre mis a` jour par loperation arithmetique utilisee pour
faire la comparaison (difference entre les deux variables en general) ce qui permet de ne pas
effectuer la comparaison dans certains cas.

L1 :
L2
L3
L4

comp
cbr LT
cmp
cbr LT
load
jumpI
load
jumpI
nop

ra , r b
cc1
rc , r d
cc2
f alse
true

cc1
L1 , L2
cc2
L3 , L2
rx
L4
rx
L4

Move conditionnel. On poss`ede une instruction du type


i2i LT cci , ra , rb ri
Le move conditionnel sexecute en un seul cycle et ne casse pas les blocs de base, il est donc
plus efficace quun saut.
comp
i2i LT
comp
i2i LT
and

ra , rb
cc1 , rtrue , rf alse
rc , rd
cc2 , rtrue , rf alse
r1 , r 2

cc1
r1
cc2
r2
rx

Comparaison a` valeurs booleenes. On supprime le registre de code condition compl`etement,


loperateur de comparaison retourne un booleen dans un registre general (ou dedie aux
booleens). Il y a uniformite entre les booleens et les valeurs relationnelles.
cmp LT
cmp LT
and

ra , r b
rc , r d
r1 , r2

r1
r2
rx

Execution prediquee. Certaines architectures autorisent le code predique (ou predicate) cest
a` dire que chaque instruction du langage machine peut e tre precedee dun predicat (i.e. un
booleen stocke dans un certain registre), linstruction ne sexecute que si le predicat est vrai.
En Iloc, cela donne :
(r1 )? add r1 , r2 rc
de flot. Le code est le meme que pour
Ici, pour lexemple, il ny a pas doperation de controle
les comparaisons de valeurs booleennes.
cmpL T
cmpL T
and

ra , r b
rc , r d
r1 , r2

r1
r2
rx

7.3 Operations de controle


de flot
Conditionnelle On a vu que lon avait quelquefois le choix pour limplementation dun if-then-else
dutiliser soit la predication soit des branchements. La predication permet de calculer deux instructions en parall`ele et de nutiliser quun resultat suivant la valeur du predicat. Pour les petits
59

blocs de base cela peut valoir le coup mais si les blocs de base sont grands on risque de perdre du
parallellisme. Par exemple, sur une machine pouvant executer deux instructions en parall`ele :
U nite1
unite2
comp.andbranch.
U nite1
unite2
L1 :
Op1
Op2
comparaison r1
Op3
Op4
(r1 ) Op1
(r1 ) Op7
Op5
Op6
(r1 ) Op2
(r1 ) Op8
jumpI Lout
(r1 ) Op3
(r1 ) Op9
L2 :
Op7
Op8
(r1 ) Op4
(r1 ) Op10
Op9
Op10
(r1 ) Op5
(r1 ) Op11
Op11
Op12
(r1 ) Op6
(r1 ) Op12
jumpI Lout
Lout : nop
Le choix de compilateur peut aussi prendre en compte le fait que les branches then et else
a` linterieur.
sont desequilibrees et quil peut y avoir dautres operations de controle
Boucles Limplementation dune boucle (For, Do, While) suit le schema suivant :

1. e valuer lexpression controlant


la boucle
2. si elle est fausse, brancher apr`es la fin de la boucle, sinon commencer le corps de boucle

3. a` la fin du corps de boucle, ree valuer lexpression de controle


4. si elle est vraie, brancher au debut de la boucle sinon continuer sur linstruction suivante
Il y a donc un branchement par execution du corps de boucle. En general un branchement
produit une certaine latence (quelques cycles). On peut cacher cette latence en utilisant les delay
slot (i.e. en executant des instructions apr`es linstruction de branchement pendant un ou deux
cycles).
Exemple de generation de code pour une boucle for :
loadI
loadI
loadI
cmp LT
cbr

for (i = 1; i <= 100; i + +)


{
corps de la boucle
}

1
1
100
r1 , r 3
r4

r1
r2
r3
r4
L1 , L2

L1 :
corps de la boucle
add
cmp LT
cbr

r1

r6

L1 , L2
L2 :
suite
On peut aussi, a` la fin de la boucle, renvoyer sur le test de la condition du debut mais cela cree

une boucle avec plusieurs blocs de base qui sera plus difficile a` optimiser. On choisira donc plutot
ce schema l`a a` moins que la taille du code ne soit tr`es critique. Les boucles while ou until se
compilent de la meme facon (duplication du test).
Dans certains langages (comme Lisp), les iterations sont implementees par une certaine forme
de recursion : la recursion terminale (tail recursion). Si la derni`ere instruction du corps dune fonction est un appel a` cette fonction, lappel est du type recursion terminale. Par exemple considerons
la fonction Lisp suivante :
(define (last alon)
(cond
((empty ? alon) empty)
((empty ? (rest alon)) (first alon))
(else (last (rest alon)))

60

r1 , r 2
r1 , r 3
r6

Lorsquon arrive a` lappel recursif, le resultat de lappel en cours sera le resultat de lappel recursif,
tous les calculs de lappel en cours ont e te faits, on na donc plus besoin de lAR de lappel en cours.
Lappel recursif peut e tre execute sans allouer un nouvel AR mais simplement en recyclant lAR de
lappel en cours. On remplace le code dun appel de procedure par un simple jump au debut du
code de la procedure.
case La difficulte dimplementation des instructions case (e.g. switch en C) est de selectionner
la bonne branche efficacement. Le moyen le plus simple est de faire une passe lineaire sur les
conditions comme si cetait une suite imbriquee de if-then-else. Dans certains cas, les differents
tests de branchement peuvent e tre ordonnes (test dune valeur par exemple), dans ce cas on peut
faire une recherche dichotomique du branchement recherche (O(log(n)) au lieu de O(n)).

7.4 Tableaux
Le stockage et lacc`es aux tableau sont extremement frequents et necessitent donc une attention particuli`ere. Commencons par la reference a` un simple vecteur (tableau a` une dimension).
Considerons que V a e te declare par V [min . . . max]. Pour acceder a` V [i], le compilateur devra
calculer le decalage de cet e lement du tableau par rapport a` ladresse de base a` partir de laquelle
il est stocke, le decalage est (i min) w ou w est la taille des e lements de w. Si min = 0 et w est
une puissance de deux,le code genere sen trouvera simplifie.
Si la valeur de min est connue a` la compilation, le compilateur peut calculer ladresse (virtuelle) @V0 quaurait le tableau sil commencait a` 0 (on appelle quelquefois cela le faux zero), cela
e vite une soustraction a` chaque acc`es aux tableaux :
V[3..10]

min
3

@V0

7 8

10

@V

Si la valeur de min nest pas connue a` la compilation, le faux zero peut e tre calcule lors de linitialisation du tableau. On arrive alors a` un code machine pour acceder a` V [i] :
loadI
lshif t
loadA0

@V0
ri , 2
r1 , r2

r1
r2
r2

// faux zero pour V


// i taille des e lements
// valeur de V [i]

Pour les tableaux multidimensionnels, il faut choisir la facon denvoyer les indices dans la
memoire. Il y a essentiellement trois methodes, les schemas par lignes et par colonnes et les tableaux de vecteurs.
Par exemple, le tableau A[1..2, 1..4] comporte deux lignes et quatre colonnes. Sil est range par
lignes (row major order), on aura la disposition suivante en memoire :
1,1

1,2

1,3

1,4

2,1

2,2

2,3

2,4

2,2

1,3

2,3

1,4

2,4

Sil est range par colonne on aura :


1,1

2,1

1,2

Lors dun parcours du tableau, cet ordre aura de limportance. En particulier si lensemble du
tableau ne tient pas dans le cache. Par exemple avec la boucle suivante, on parcourt les cases par
lignes ; si lon intervertit les boucles i et j, on parcourera par colonnes :
for i 1 to 2
for j 1 to 4
A[i, j] A[i, j] + 1

61

Certains langages (java) utilisent un tableau de pointeurs sur des vecteurs. Voici le schema
pour A[1..2, 1..4] et B[1..2, 1..3, 1..4]
1,1,1

1,1

2,1

1,2

2,2

1,3

2,3

1,1,2

1,1,3

1,2,1

1,2,2

1,2,3

1,3,1

1,3,2

1,3,3

1,1,4

1,2,4

1,4
1,3,4

2,4
2,1,1

2,2,1

2,3,1

2,1,2

2,2,2

2,3,2

2,1,3

2,2,3

2,3,3

2,1,4

2,2,4

2,3,4

Ce schema est plus gourmand en place (la taille des vecteurs de pointeur grossit quadratiquement avec la dimension) et lacc`es nest pas tr`es rapide.
Considerons un tableau A[min1 ..max1 , min2 ..max2 ] range par ligne pour lequel on voudrait
acceder a` lelement A[i, j]. Le calcul de ladresse est : @A[i, j] = @A + (i min1 ) (max2 min2 +
1) w + (j min2 ) w, ou w est la taille des donnees du tableau. Si on nomme long2 = (max2
min2 +1) et que lon developpe on obtient : @A[i, j] = @A+ilong2 w +j w (min1 long2 +
min2 w). On peut donc aussi precalculer un faux zero @A0 = @A(min1 long2 w+min2 w)
et acceder lelement A[i, j] par @A0 +(ilong2 +j)w. Si les bornes et la taille ne sont pas connues
a` la compilation, on peut aussi effectuer ces calculs a` linitialisation. Les memes optimisations sont
faites pour les tableaux ranges par colonnes.
Lacc`es aux tableaux utilisant des pointeurs de vecteurs necessite simplement deux instructions par dimension (chargement de la base de la dimension i, decalage jusqu`a lelement dans
la dimension i). Pour les machines ou` lacc`es a` la memoire e tait rapide compare aux operations
arithmetiques, cela valait le coup (ordinateur avant 1985).
Lorsquon passe un tableau en param`etre, on le passe generalement par reference, meme si
dans le programme source, il est passe par valeurs. Lorsquun tableau est passe en param`etre a`
une procedure, la procedure ne connat pas forcement ses caracteristiques (elles peuvent changer suivant les appels) comme en C. Pour cela le syst`eme a besoin dun mecanisme permettant
de recuperer ces caracteristiques (dimensions, taille). Cela est fait grace au descripteur de tableau (dope vector). Un descripteur de tableau contient en general un pointeur sur le debut du
tableau et les tailles des differentes dimensions. Le descripteur a une taille connue a` la compilation (dependant uniquement de la dimension) et peut donc e tre alloue dans lAR de la procedure
appelee.
Beaucoup de compilateurs essaie de detecter les acc`es en dehors des bornes des tableaux. La
methode la plus simple est de faire un test sur les indices avant chaque appel a` lexecution. On
peut faire un test moins precis en comparant le decalage calcule a` la taille totale du tableau.

7.5 Chanes de caract`eres


La manipulation des chanes de caract`eres est assez differente suivant les langages. Elle utilise
en general des instructions operant au niveau des octets. Le choix de la representation des chanes
de caract`eres a` un impact important sur les performance de leur manipulation. On peut stocker
une chane en utilisant un marqueur de fin de chane comme en C traditionnel :
a

ou en stockant la longueur de la chane au debut (ou la taille effectivement occupee, si ce nest


pas la taille allouee) :

62

10

Les assembleurs poss`edent des operations pour manipuler les caract`eres (cloadAI, cstoreAI
par exemple pour Iloc). Par exemple pour traduire linstruction C : a[1] = b[2] ou` a et b sont des
chanes de caract`eres, on e crit :
loadI
cloadAI
loadI
cstoreAI

@b
rb
rb , 2 r2
@a ra
r2
ra , 1

Si le processeur ne poss`ede pas dinstruction specifique pour les caract`eres, la traduction est beaucoup plus delicate : en supposant que a et b commencent sur une fronti`ere de mot (adresse en octet
multiple de 4), on aura le code :
loadI
load
andI
lshif tI
loadI
load
andI
or
storeAI

@b
rb
r1 , 0x0000F F 00
r2 , 8
@a
ra
r4 , 0xF F 00F F F F
r 3 , r5
r6

rb
r1
r2
r3
ra
r4
r5
r6
ra , 0

// adresse de b
// premier mot de b
// masque les autres caract`eres
// deplacement du caract`ere
// adresse de a
// premier mot de a
// masque le deuxi`eme caract`ere
// on rajoute le caract`ere de b
// on remet le premier mot de a en memoire

Pour les manipulations de chanes (comme pour les tableaux dailleurs), beaucoup dassembleurs poss`edent des instructions de chargement et de stockage avec autoincrement (pre ou post),
cest a` dire que linstruction incremente ladresse servant a` lacc`es en meme temps quelle fait
lacc`es, ce qui permet de faire directement lacc`es suivant au mot (resp. caract`ere suivant) juste
apr`es. On peut aussi choisir dutiliser des instructions sur les mots (32 bits) tant que lon arrive
pas a` la fin de la chane.

7.6 Structures
Les structures sont utilisees dans de nombreux langages. Elles sont tr`es souvent manipulees a`
laide de pointeurs et creent donc de nombreuses valeurs ambigues. Par exemple, pour faire une
liste en C, on peut declarer les types suivants :
struct ValueNode {
int Value
};

Union Node{
struct ValueNode ;
struct ConsNode ;
};

structu ConsNode {
Node *Head ;
Node *Tail ;
};
Afin demettre du code pour une reference a` une structure, le compilateur doit connatre
ladresse de base de lobjet ainsi que le decalage et la longueur de chaque e lement. Le compilateur construit en general une table separee comprenant ces informations (en general une table
pour les structures et une table pour les e lements).

63

Table des structures


Nom

Taille

Premier elt

ValueNode

ConsNode

Table des lments


Nom

Longueur

decalage

ValueNode.Value

Type
int

ConsNode.Head

Node *

ConsNode.Tail

Node *

Prochain elt

Avec cette table, le compilateur gen`ere du code. Par exemple pour acceder a` p1 Head on peut
generer linstruction :
loadA0

rp1 , 0

r2

// 0 est le decalage de Head

Si un programme declare un tableau de structure dans un langage dans lequel le programmeur ne peut pas avoir acc`es a` ladresse a` laquelle est stockee la structure, le compilateur peut
choisir de stocker cela sous la forme dun tableau dont les e lements sont des structures ou sous
la forme dune structure dont les e lements sont des tableaux. Les performances peuvent e tre tr`es
differentes, l`a encore a` cause du cache.

64

8 Implementation des langages objet


Fondamentalement, lorientation objet est une reorganisation de lespace des noms du programme dun schema centre sur les procedures vers un schema centre sur les donnees. En pratique, les langages orientes objet ne sont pas tous bases sur le meme principe. Dans cette section,
on presente limplementation dun cas particulier de langage oriente objet.

8.1 Espace des noms dans les langages objets


Dans les langages traditionnels, les portees sont definies a` partir du code (en entrant dans une
procedure ou un bloc, etc.). Dans les langages objets, les portees des noms sont construites a` partir
des donnees. Ces donnees qui gouvernent les r`egles de nommage sont appelees des objets (tout
nest pas objet, il y a aussi des variables locales aux procedures qui sont implementees comme
dans les langages classiques).
Pour le concepteur de compilateur, les objets necessitent des mecanismes supplementaires a`
la compilation et a` lexecution. Un objet est une abstraction qui poss`ede plusieurs membres. Ces
membres peuvent e tre des donnees, du code qui manipule ces donnees ou dautres objets. On peut
donc representer un objet comme une structure (avec la convention que les e lements peuvent e tre
des donnees, des procedures ou dautres objets).
proc1
.....

x: 2
y: 0
z:

proc2
.....

proc1

x: 5

proc2

y: 3

proc1
.....

z: Nil
proc1

proc2
.....

proc2

Dans le schema ci-dessus, on voit deux objets appeles a et b. On voit que les deux variables (x et y)
de a et b ont des variables differentes. En revanche, il utilisent les memes procedures. Comme on
a represente lacc`es aux codes par des pointeurs, on pourrait imaginer quil partagent un meme
code :
a

x: 2
y: 0
z:

proc1

x: 5

proc2

y: 3

proc1
.....

z: Nil
proc1
proc2

proc2
.....

Mais la manipulation de code avec des pointeurs (melange du code et des donnees) pourra assez
vite amener des situations inextricables, cest pour cela que lon introduit la notion de classe. Une
classe est une abstraction qui groupe ensemble des objets similaires. Tous les objets dune meme
classe ont la meme structure. Tous les objets dune meme classe utilisent les memes portions de
code. Mais chaque objet de la classe a ses propres variables et objets membres. Du point de vue de
limplementation, on va pouvoir stocker les portions de code dune classe dans un nouvel objet
qui represente la classe :

65


n: 2
proc1
proc2
a

x: 2
y: 0
z:

b
x: 5

proc1
.....

y: 3
z: Nil
proc2
.....

Ici est la classe a` laquelle appartiennent a et b. Les objets a et b ont un nouveau champ qui
represente la classe a` laquelle ils appartiennent (la classe e tant un objet, elle a aussi ce nouveau
champ). Attention, ces diagrammes sont des representations de limplementation de ces objets,
pas de leur declaration dans le langage source. Dans la declaration de la classe on trouvera la
declaration des variables x et y. On peut maintenant introduire la terminologie utilisee pour les
langages objets :
instance Une instance est un objet appartenant a` une certaine classe (cest en fait la notion intuitive lie au mot objet). Cest un objet logiciel (tel quon le rencontre dans le code source).
enregistrement dobjet Un enregistrement dobjet (object record) est la representation concr`ete
dune instance (son implementation dans la memoire par exemple).
variable dinstance Les donnees membres dun objet sont des variables dinstance (exemple,
la variable dinstance x de a vaut 2, en java on appelle cela les champsfield).
methode Une methode est une fonction ou procedure commune a` tous les objets dune classe.
Cest une procedure classique. La difference avec les langages classiques tient dans la mani`ere
de la nommer : elle ne peut e tre nommee qu`a partir dun objet. En java, on ne la nomme
pas proc1 mais x.proc1 ou` x est une instance dune classe qui implemente proc1 .
recepteur Comme les procedures sont invoquees depuis un objet, chaque procedure poss`ede
en plus un param`etre implicite qui est un pointeur sur lenregistrement dobjet qui la invoque. Dans la methode, le recepteur est accessible par this ou self.
Classe Une classe est un objet qui decrit les proprietes dautres objets. Une classe definit
les variables dinstance et les methodes pour chaque objet de la classe. Les methodes de la
classe deviennent des variables dinstance pour les objets qui implementent la classe.
variables de classe Chaque classe est implementee par un objet. Cet objet a des variables dinstance. Ces variables sont quelquefois appelees variables de classe, elles forment un stockage
persistant visible de nimporte quelle methode de la classe et independant du recepteur.
La plupart des langages orientes objet implementent la notion dheritage. Lheritage permet
de definir une classe de classes. Une classe peut avoir une classe parent (ou super-classe).
Toutes les methodes de la classe sont alors accessibles par un objet de la classe . Toutes les
variables dinstance de sont variables dinstance de et la resolution de noms des methodes se
fait recursivement (comme pour les portees des variables dans les langages classiques).
Voici un exemple dheritage. Les objets a et b sont membres de la classe dont est une superclasse. Chacune des trois classes , et implemente une methode nommee proc1 . On voit un
objet c qui est une instance de la classe , les objets de cette classe ont les variables dinstance x et
y mais pas z.

66


n: 2

proc1

proc4
n: 2
proc1

x: 5

y: 3

proc3
n: 2
proc1
proc1
.....

proc2
a
proc1
.....

x: 2
y: 0
z:

b
x: 5

proc4
.....

proc1
.....

y: 3
proc3
.....

z: Nil
proc2
.....

Si un programme invoque a.proc1 , il executera la procedure definie dans la classe , sil invoque
b.proc4 on executera la procedure definie dans la classe . Le mecanisme pour retrouver le code de
la procedure appelee a` lexecution est appele dispatch. Le compilateur met en place une structure
e quivalente a` la coordonnee de distance statique : une paire distance, decalage . Si la structure
de la classe est statique tout cela peut e tre determine a` la compilation. Si lexecution peut changer
la hierarchie des classes et lheritage, generer du code efficace devient beaucoup plus difficile.
Quelles sont les r`egles de visibilite pour une procedure appelee (par exemple a.proc1 ) ? Dabord,
proc1 a acc`es a` toutes les variables declarees dans la procedure. Elle peut aussi acceder les param`etres. Cela implique quune methode active poss`ede un enregistrement dactivation (AR) comme
dans les langages classiques. Ensuite, proc1 peut acceder aux variables dinstance de a qui sont
stockees dans lenregistrement dobjet de a. Ensuite, elle a acc`es aux variables de classe de la classe
qui a defini proc1 (en loccurrence ) ainsi quaux variables dinstance de ses super-classes. Les
r`egles sont un peu differentes de celles des langages classiques, mais on voit bien que lon peut
mettre en place un mecanisme similaire a` celui utilisant les coordonnees de distance statique.
central joue par les donnees dans les langages objets, le passage de param`etres
Du fait du role
plus important. Si une methode appelle une autre methode, elle ne peut le faire que
joue un role
si elle poss`ede les objets recepteurs pour cette methode. Typiquement, ces objets sont soit globaux
soit des param`etres.

8.2 Generation de code


Considerons le probl`eme de generation de code pour une methode individuelle. Comme la
methode peut acceder tous les membres de tous les objets qui peuvent e tre recepteurs, le compilateur doit e tablir des decalages pour chacun de ces objets qui sexpriment uniformement depuis
la methode. Le compilateur construit ces decalages lorsquil traite les declarations de classe (les
objets eux-memes nont pas de code).
Le cas le plus simple est le cas ou` la structure de la classe est connue a` la compilation et quil
ny a pas dheritage. Considerons une classe grand qui poss`ede les methodes proc1 , proc2 , proc3
et les membres x et y qui sont des nombres. La classe grand poss`ede une variable de classe, n, qui
enregistre le nombre denregistrements dobjets de cette classe qui ont e te cree s. Pour autoriser la
creation dobjet, toutes les classes implementent la methode new (au decalage 0 dans la table des
methode).
Dans ce cas, lenregistrement dobjet pour une instance de la classe grand est juste un vecteur
de longueur 3. La premi`ere case est un pointeur sur sa classe (grand), qui contient ladresse de
la representation concr`ete de la classe grand. Les deux autres cases contiennent les donnees x et
y. Chaque objet de la classe grand a le meme type denregistrement dobjet, cree par la methode
new.
Lenregistrement de classe poss`ede un espace pour ses variables de classe (n ici ) et pour toutes

67

ses methodes. Le compilateur mettra donc en place une table de ce type pour la classe de grand :
decalage

new
0

proc1
4

proc2
8

proc3
12

n
16

Apr`es que le code ait cree deux instances de grand (ici emmanuel et jean michel) les structures
a` lexecution devraient ressembler a` cela :

emmanuel
%grand.new

13

%grand.proc1
%grand.proc2

14

jeanmichel

%grand.proc3

15

16

La table des methodes de la classe grand contient des labels (symbole cree par le compilateur).
Ces labels sont associes aux adresses auxquelles sont stockees les procedures.
Considerons maintenant le code que le compilateur devra generer lors de lexecution de la
methode new dun objet de classe grand. Il doit allouer la place pour le nouvel enregistrement
dobjet, initialiser les donnees membres et retourner un pointeur sur cette nouvelle structure. La
procedure doit aussi respecter les r`egles de ledition de lien (sauvegarder les registres, creer un
AR pour les variables locales et restaurer lenvironnement avant de finir). En plus des param`
etres
formels, le compilateur ajoute le param`etre implicite this.
Lorsquon rencontre lappel jean michel.proc1 , le code genere produit les actions suivantes :
acc`es au decalage 0 a` partir de ladresse de jean michel (adresse de la classe grand), chargement
de ladresse de la methode proc1 , et generation dun appel de procedure a` cette adresse, en mettant
comme param`etre this ladresse de jean michel.
Le schema est plus complexe lorsque lheritage est autorise. Du fait de lheritage, chaque enregistrement de classe poss`ede un pointeur de classe (qui pointe sur lobjet classe, qui implemente
la methode new par defaut), et un pointeur de super-classe dont elle herite. Tant que la structure
des classes est fixee, le compilateur peut resoudre les invocations de methodes par une structure
du type coordonnees de distance statique. Considerons le schema suivant :
class
humain
Nil
%class.new_
%humain.new_
%humain.proc1_

...
homme

%humain.calc_
0
%homme.new_
%homme.proc1_

grand

emmanuel

.....
13

0
%grand.new_
%grand.proc1_
%grand.proc2_
%grand.proc3_
2

68

14

jeanmichel
15
16

Les coordonnees des methodes sont :


grand
homme
humain
class

new
0, 8
0, 8
0, 8
0, 8

proc1
0, 12
0, 12
0, 12

proc2
0, 16

proc3
0, 20

calc
2, 16
1, 16
0, 16

Lorsque jeanmichel.calc est invoque, le compilateur recup`ere le nom et ladresse de la classe


geant a` laquelle appartient jeanmichel. Les coordonnees de la methode calc pour jeanmichel
sont 2, 16 . Le compilateur gen`ere alors le code pour remonter le long des pointeurs de superclasse jusqu`a la classe humain, recup`ere ladresse de la procedure calc et appelle la procedure.
Ce schema est simple, mais implique une condition que doit respecter le rangement des donnees
dans les enregistrement dobjets : lorsque jean michel est recepteur de la methode calc de la
classe humain, la methode ne connat que les variables dinstance que jean michel herite de
humain et de ses ancetres (pas celles heritees de la classe homme). Pour que les methodes heritees
fonctionnent, ces variables doivent avoir le meme decalage dans un objet de classe humain que
dans un objet de classe grand. Autrement la methode designerait, suivant le recepteur, des variables differentes avec un meme decalage.
Cela sugg`ere que les membres des methodes soient ranges en memoire dans lordre dheritage.
Les variables dinstance dun enregistrement de la classe grand seront rangees de la mani`ere suivante :
pointeurs de classes data humain data homme data grand
La meme contrainte sapplique aux enregistrements de classes. Dans la cas de lheritage multiple,
la situation est un peu plus compliquee.

69

9 Generation de code : selection dinstructions


On a vu comment produire un arbre syntaxique, comment generer du code pour des structures
appels de procedure, etc.). En general pour generer le code
particuli`eres (structures de controle,
explicitement, il faut :
1. Selectionner les instructions de la machine cible a` utiliser a` partir de la representation intermediaire (selection dinstructions, instruction selection).
2. Choisir un ordre dexecution de ces instructions (ordonnancement dinstructions, instruction
scheduling).
3. Decider quelles valeurs resideront dans les registres (allocation de registre, register allocation)
On reviendra sur les deux derniers traitements plus loin. Ici, le traitement qui nous interesse est
la selection dinstructions.
La difficulte du processus de selection dinstructions vient du fait quil y a en general plusieurs
mani`eres dimplementer un calcul donne avec le jeu dinstructions de la machine cible (instruction set architecture, ISA). Par exemple, pour copier de registre a` registre on utilise naturellement
loperation Iloc :
i2i ri rj
mais on peut aussi utiliser les operations suivantes :
addi
multIi
lshif tI
orI

ri , 0
ri , 1
ri , 0
ri , 0

rj
rj
rj
rj

subI
divI
rhif tI
xorI

ri , 0 rj
ri , 1 rj
ri , 0 rj
ri , 0 rj

En general la fonction a` optimiser est la vitesse dexecution, mais on peut imaginer dautres
fonctions (consommation e lectrique, taille de code, etc.). De plus, un certain nombre de contraintes
specifiques a` larchitecture cible sont rajoutees : certains registres sont dedies aux operations sur
les entiers, ou les flottants. Certaines operations ne peuvent sexecuter que sur certaines unites
fonctionnelles. Certaines operations (multiplication, division, acc`es memoire) peuvent prendre
plus dun cycle dhorloge. Le temps des acc`es memoire est difficilement predictible (`a cause du
cache).
Lapproche systematique de la selection dinstructions permet de construire des compilateurs
portables, cest a` dire qui peuvent cibler differentes machines sans avoir a` ree crire la majeure
partie du compilateur. Les trois operations mentionnees plus haut op`erent sur le code assembleur
de la machine cible, elles utilisent donc des informations specifiques a` la cible : par exemple,
latence des operations, taille des fichiers de registre, capacites des unites fonctionnelles, restriction
dalignement, etc. En realite, beaucoup de ces caracteristiques sont necessaires pour des e tapes
precedant la selection dinstructions (par exemple : les restrictions dalignement, les conventions
pour les AR).
Larchitecture dun generateur automatique de selecteur dinstructions est la suivante :
Description de la
machine cible

Gnrateur
de back end

Tables

Moteur
"pattern matching

Nous allons voir deux techniques utilisees pour automatiser la construction dun selecteur
dinstructions : la premi`ere utilise la theorie des ree critures darbres pour creer un syst`eme de
70

ree criture ascendant (bottom up rewriting system, BURS), il fonctionne sur les IR a` base darbre.
Le second utilise une optimisation classique : loptimisation par fenetrage (peephole opimization)
qui essaie de trouver, parmi une librairie de morceaux de code predefinis (pattern), la meilleure
mani`ere dimplementer un morceau de lIR limite par une fenetre (fonctionne sur les IR lineaires).

9.1 Selection dinstruction par parcours darbre (BURS)


considerons linstruction suivante :
w x2y
On peut choisir de la representer en representation intermediaire par un arbre ou une IR lineaire.

op

*
2

Arg1
2
x

Arg2
y
t1

result
t1
w

Le schema le plus simple pour generer du code est le parcours darbre. Par exemple, si on ne
sinteresse qu`a la partie arithmetique x 2 y, on peut utiliser la fonction expr suivante :
expr(noeud){
int result, t1, t2 ;
switch(type(noeud))
{
case , , +, :
t1 expr(f ils gauche(noeud)) ;
t1 expr(f ils droit(noeud)) ;
result N extRegister() ;
emit(op(noeud), t1, t2, result) ;
break ;
case Identif icateur :
t1 base(noeud) ;
t2 decalage(noeud) ;
result N extRegister() ;
emit(loadAI, t1, t2, result) ;
break ;
case N umber :
result N extRegister() ;
emit(loadI, val(noeud), result) ;
break ;
}
return result
}
Le code genere lors du parcours de larbre correspondant a` x 2 y est :
loadAI
loadI
loadAI
mult
sub

rarp , @x
2
rarp , @x
r2 , r y
rx , r3
71

rx
r2
ry
r3
r4

Mais pour avoir un code efficace il faudrait des traitements plus complexes sur les noeuds. Par
exemple, voici un code qui ne serait pas traduit optimalement. Pour traduire a 2 (en supposant
que les coordonnees de distance statique de a sont arp, 4 , on aura :

au lieu de

loadAI
loadI
mult

rarp , 4
2
r1 , r2

r1
r2
r3

loadAI
multI

rarp , 4
r1 , 2

r1
r2

Ce deuxi`eme cas expose un probl`eme intrins`eque a` lalgorithme de parcours darbre simple. Il y a


utilisation dune information non locale au noeud (le fait que lun des arguments de la multiplication est une constante). On pourrait aussi reutiliser localement des valeurs comme les decalages
en memoire.
Une solution utilisee est de raffiner la description de larbre en descendant a` une description
de niveau proche du code final. Pour notre exemple, cela pourrait donner :

Val
ARP

Num
4

Ref
*

Num
2

Ref

Ref

Val
ARP

Num
16

Lab
@G

Num
12

Dans cet arbre, V al indique une valeur dont on sait quelle reside dans un registre (ici lARP).
Un noeud Lab (pour Label) represente un symbole relogeable (relocatable symbol) cest a` dire un
label de lassembleur utilise pour designer lemplacement dune donnee ou dun symbole (par
exemple, une variable statique) qui sera resolu a` ledition de lien. Un Ref signifie un niveau dindirection. On voit apparatre des informations de bas niveau comme par exemple le fait que x est
un param`etre passe par reference.
Lalgorithme de ree criture darbre va cibler une representation en arbre de lassembleur cible
(ici Iloc). On definit une notation pour decrire les arbres en utilisant une notation prefixe. Par
exemple larbre ci-dessus se decrira :
Gets(+(V al1 , N um1 ), (Ref (Ref (+(V al2 , N um2 ))), (N um3 , Ref (+(Lab1 , N um4 )))))
On dispose donc dun ensemble darbres doperations (expression des instructions Iloc sous
forme darbre) et lon va essayer de recouvrir lAST par des arbres doperations, on appelle cette
operation un pavage (tiling). Un pavage de lAST est un ensemble de couples du type
(noeud AST, arbre d operation) qui indique que arbre d operation implemente noeud AST .
Une fois que lon a un pavage qui couvre tous les noeuds de larbre, on peut generer le code par
un simple parcours en profondeur de larbre (comme on la fait pour lexpression arithmetique
72

x 2 y). La difficulte est de generer rapidement un bon pavage sachant quune operation peut
a` chaque arbre doperation
implementer plusieurs noeuds de lAST. En general, on associe un cout
total.
et on essaie de minimiser le cout
R`egles de ree criture On va definir un certain nombre de r`egles de ree criture. Une r`egle de
ree criture comporte une production exprimee sur la grammaire darbre, un code produit et un
Par exemple, une addition +(r1 , r2 ) correspond a` la r`egle : Reg +(Reg1 , Reg2 ). Le symcout.
bole en partie gauche correspond a` un symbole non-terminal representant un ensemble de sous
arbres que la grammaire darbre peut generer. Les symbole Reg1 et Reg2 representent aussi le nom
terminal Reg, il sont numerotes pour pouvoir les identifier (comme pour les attributs). Le noeud
+ decrit ainsi prend obligatoirement deux registres en argument et range son resultat dans un
registre, il correspond a linstruction Iloc add. Ces grammaires sont en general ambigues a` cause
du fait quil y a plusieurs mani`eres dexprimer un meme code.
associe a` chaque production doit indiquer au generateur de code une estimation
Le cout
de lexecution du code. La plupart des techniques utilisent des couts
fixes, cerrealiste du cout
variables (i.e. refletant les choix faits prealablement, au cours du pavage).
taines utilisent des couts
Considerons lensemble possible de r`egles suivantes pour ree crire lAST de bas niveau mentionne
precedemment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

r`egle
Goal
Assign
Assign
Assign
Assign
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
Reg
N um

Assign
Gets(Reg1 , Reg2 )
Gets(+(Reg1 , Reg2 ), Reg3 )
Gets(+(Reg1 , N um2 ), Reg3 )
Gets(+(N um1 , Reg2 ), Reg3 )
Lab1
V al
N um1
Ref (Reg1 )
Ref (+(Reg1 , Reg2 ))
Ref (+(Reg1 , N um2 ))
Ref (+(N um1 , Reg2 ))
+(Reg1 , Reg2 )
+(Reg1 , N um2 )
+(N um1 , Reg2 )
(Reg1 , Reg2 )
(Reg1 , N um2 )
(N um1 , Reg2 )
(Reg1 , Reg2 )
(Reg1 , N um2 )
(N um1 , Reg2 )
Lab1

cout
0
1
1
1
1
1
0
1
1
1
1
1
1
1
1
1
1
1
1
1
1
0

code
store
storeAO
storeAI
storeAI
loadI

r2
r3
r3
r3
l1

r1
r1 , r2
r1 , n2
r2 , n1
rnew

loadI
load
loadAO
loadAI
loadAI
add
addI
addI
sub
subI
subI
mult
multI
multI

n1
r1
r1 , r2
r 1 , n2
r 2 , n1
r1 , r2
r 1 , n2
r 2 , n1
r1 , r2
r 1 , n2
r 2 , n1
r1 , r2
r 1 , n2
r 2 , n1

rnew
rnew
rnew
rnew
rnew
rnew
rnew
rnew
rnew
rnew
rnew
rnew
rnew
rnew

Notons que la grammaire proposee est ambigue (par exemple, la r`egle 11 peut e tre generee en
combinant les r`egles 14 et 9). De plus, certains symboles (par exemple Reg) sont a` la fois terminaux
et non-terminaux.
Pour comprendre comment utiliser ces r`egles, une notion importante est le type de stockage
en memoire de chaque valeur. Ici, on a deux types de stockage : Reg pour des registres et N um
pour des valeurs (Lab est considere comme une valeur, pour e viter ce typage supplementaire, on
` chaque r`egle R1 , on peut associer un type de
pourrait rajouter une r`egle gratuite N um Lab). A
stockage pour la partie gauche et un type de stockage pour les arguments de la partie droite, pour
lalgorithme mentionne plus loin, on appellera respectivement lef t(R1 ), f ils droit(right(R1 )),
f ils gauche(right(R1 )) ces types de stockage. Par exemple, R1 est la r`egle Reg +(N um, Reg),
f ils droit(right(R1 )) vaut N um et f ils gauche(right(R1 )) vaut Reg. Un ensemble de r`egle plus
complet comprendra la distinction entre valeur stockee en registre et valeur stockee en memoire.
73

La r`egle que nous avions utilisee pour laddition dans la traduction simple e tait la r`egle 13.
On voit que lon a des r`egles plus complexes qui decrivent des schemas de plusieurs niveaux
de noeuds. Par exemple : Ref (+(Reg, N um)). Decouvrir un tel schema dans larbre ne peut pas
se faire avec un simple parcours en profondeur. Pour appliquer ces r`egles sur un arbre, on va
chercher une sequence de ree criture qui reduira larbre a` un simple symbole.
Par exemple, pour le sous-arbre Ref (+(@G, 12)), on peut trouver la sequence 6, 11 qui effectuera la reduction suivante (que lon peut resumer en un simple schema, ici a` droite) :
11

Ref

Ref

11
Ref

Ref

Reg
+

6
Lab
@G

Num
12

Lab
@G

Num
12

Reg

Lab
@G

Num
12

Pour cet exemple particulier, nous avons dautres possibilites : les sequences 22, 8, 12 (car Lab a`
2 e galement, les sequences 6, 8, 10 , 22, 8, 6, 10 , 6, 14, 9
un stockage de type N um)qui a un cout
3 et les sequences 6, 8, 13, 9 et 22, 8, 6, 13, 9 ont un cout
4:
et 8, 15, 9 ont un cout

loadI
loadAI
6, 11
loadI
loadI
loadA0
8, 6, 10
loadI
loadI
add
load
6, 8, 13, 9

@G
ri
ri , 12 rj

12
@G
ri , rj
@G
12
ri , rj
rk

ri
rj
rk

ri
rj
rk
rl

loadI
loadAI
22, 8, 12
loadI
addI
load
6, 14, 9
loadI
loadI
add
load
8, 6, 13, 9

12
ri
ri , @G rj

@G
ri , 12
rj
12
@G
ri , rj
rk

ri
rj
rj

loadI
loadI
loadAO
6, 8, 10
loadI
addI
load
22, 8, 15, 9

@G
12
ri , r j

ri
rj
rk

12
ri
ri , @G rj
rj
rk

ri
rj
rk
rl

Comment trouver un pavage ?


Lidee derri`ere les BURS (bottom up rewriting system) est la suivante : par un parcours en profondeur remontant (post-order, ou bottom up), on va associer a` chaque noeud de larbre un ensemble
de r`egles qui peuvent e tre utilisees pour ree crire le noeud. On pourra alors par parcours en profondeur descendant (pre-order, ou top down) generer la meilleure instruction a` chaque noeud.
Comme lensemble des configuration des noeuds de lAST est finit, on va pouvoir generer
statiquement lensemble des ensembles de r`egles qui pourrait sappliquer sur un noeud dun
certain type, sans connatre le programme a` compiler. On peut donc precalculer les transitions a`
effectuer a` chaque noeud (comme pour un parseur LL(1)). Ceci permettra decrire un generateur
de generateur de code (comme on vu quil existait des generateur de parseurs).
Definissons letiquette dun noeud de lAST comme un ensemble de r`egles de ree criture. Par
exemple on a vu que le noeud +1 dans +1 (Lab1 , N um1 ) peut utiliser les r`egles 13,14 ou 15 suivant les r`egles que lon avait utilisees pour ses fils (on a mis des indices pour bien signifier quil
sagit dune instance particuli`ere, un morceau de lAST). Donc letiquette du noeud +1 peut e tre
composee de ces trois r`egles :{Reg +(Reg, Reg), Reg +(Reg, N um), Reg +(N um, Reg)}.
On peut restreindre le nombre de r`egles applicables si lon connat les fils du noeud +1 (et les

74

Num
12

r`egles applicables sur ses fils). Par exemple si lon a +2 (Reg1 , Reg2 ), comme il nexiste pas de
r`egle transformant un resultat de type registre en un resultat de type valeur, on sait que la seule
r`egle qui peut sappliquer est Reg +(Reg, Reg).
On note Label(n) letiquette dun noeud. Par extension on note lef t(Label(n)) lensemble des
type de stockage des parties gauches des r`egles contenues dans letiquette (ici lef t(Label(+1 )) =
{Reg}). Pour illustrer lalgorithme, supposons que toutes les operations ont au plus deux arguments et que les parties droite des r`egles ne poss`edent quune operation. Cela ne change rien au
traitement car on peut toujours rajouter des non terminaux. On peut par exemple ree crire les r`egle
10, 11 et 12 en :
10 : Reg Ref (+(Reg, Reg))

devient

Reg Ref (R10P 2) R10P 2 +(Reg, Reg)

11 : Reg Ref (+(Reg, N um))

devient

Reg Ref (R11P 2)

R11P 2 +(Reg, N um)

12 : Reg Ref (+(N um, Reg))

devient

Reg Ref (R12P 2)

R12P 2 +(N um, Reg)

total de chaque r`egle sur la premi`ere. Voici lalgorithme qui permet


On choisit de laisser le cout
de calculer les e tiquettes dun AST.
T ile(n)
Label(n)
si n est un noeud binaire
alors
T ile(f ils gauche(n))
T ile(f ils droit(n))
Pour chaque r`egle r qui implemente n
// i.e. si oper(r) = oper(n)
si f ils gauche(right(r)) lef t(Label(f ils gauche(n)))
et f ils droit(right(r)) lef t(Label(f ils droit(n)))
// i.e. si les types des fils sont compatibles
alors
Label(n) Label(n) {r}
sinon
//(node unaire)
si n est un noeud unaire
alors
T ile(f ils(n))
Pour chaque r`egle r qui implemente n
// i.e. si oper(r) = oper(n)
si f ils(right(r)) lef t(Label(f ils(n)))
alors
Label(n) Label(n) {r}
sinon
//(n est une feuille)
Label(n) { toutes les r`egles qui implementent n }
En prenant toujours le meme sous-AST : Ref1 (+1 (Lab1 , N um1 )), on peut executer lalgorithme.
En arrivant sur le noeud Lab1 les seules r`egles qui implementent ce noeud sont les r`egles 6 et 22 :
Reg Lab et N um Lab. On construit donc letiquette de ce noeud par :
Label(Lab1 ) = {Lab, Reg Lab, N um Lab}
De meme, la seule r`egle implementant le noeud N um est la r`egle 8 et 6 :
Label(N um1 ) = {N um, Reg N um}
En revanche, lorsquon remonte dun cran (noeud +1 (Lab1 , N um1 )), on a beaucoup plus de possibilites. On arrive a` letiquette suivante :
Label(+1 ) =

Reg +(Reg1 , Reg2 ), Reg +(Reg1 , N um2 ), Reg +(N um1 , Reg2 ),
R10P 2 +(Reg, Reg), R11P 2 +(Reg, N um), R12P 2 +(N um, Reg),

et enfin pour le noeud Ref1 on a les r`egles 9, 10, 11, 12.


Label(Ref1 ) =

Reg Ref (Reg1 ), Reg Ref (R10P 2),


Reg Ref (R11P 2), Reg Ref (R12P 2)
75

Lidee utilisee ici est la meme pour pour le parsing LL(1) : on peut precalculer tous les e tats
possibles. Il y en a un nombre fini. Si R est le nombre de r`egles, letiquette dun noeud contient
cest toujours beaucoup
moins de R e lements donc il y en a moins de 2R : |ens label| < 2R (bien sur
moins que cela vu la structure de la grammaire). Si chaque r`egle a un operateur et deux operandes,
on calcule compl`etement letiquette dun noeud a` partir de loperation du noeud, de letiquette de
son fils droit et de letiquette de son fils gauche. On peut precalculer les transitions dune e tiquette
a` une autre dans une table de taille :
|ens arbres operation| |ens label| |ens label|
Cette table est en general creuse et il existe des techniques efficaces pour la comprimer.
On a donc la possibilite de determiner avant la compilation, lautomate qui va associer lensemble des r`egles que lon pourra affecter a` chaque noeud. On peut donc generer lensemble des
generations de code possibles pour un AST donne. En associant a` chaque generation de code son
on peut choisir la meilleure. Bien sur
cela represente un nombre exponentiel de possibicout,
lites, on va voir tout de suite comment faire cela plus efficacement grace a` un algorithme de type
programmation dynamique.
a` chaque r`egle calcule a` partir des couts
de chaque r`egle et des
On peut associer des couts
des fils droits et gauches. On note R1 @val1 pour indiquer que le choix de la r`egle R1 pour
couts

le noeud coutera
val1 . Pour notre exemple cela donne :
Label(N um1 ) = {N um@0, Reg N um@1}
Label(Lab1 ) = {Lab@0, N um Lab@0, Reg Lab@1}
Label(+1 ) =

Reg +(Reg1 , Reg2 )@3, Reg +(Reg1 , N um2 )@2, Reg +(N um1 , Reg2 )@2,
R10P 2 +(Reg, Reg)@2, R11P 2 +(Reg, N um)@1, R12P 2 +(N um, Reg)@1,
Label(Ref1 ) =

Reg Ref (Reg1 )@3, Reg Ref (R10P 2)@3,


Reg Ref (R11P 2)@2, Reg Ref (R12P 2)@2

total du sous arbre pour chaque r`egle


Avec ce syst`eme un noeud contient linformation du cout
pouvant implementer le noeud. Ici on a effectue une premi`ere selection en ne conservant que les
noeuds fils les moins chers pour un type de stockage en memoire donne (sachant que lon nenl`eve
pas les r`egles provenant de decomposition de r`egles complexes du type R10P 11). Si maintenant
dans chaque noeud on ne conserve que les noeuds les moins chers pour un stockage donne, on
obtient :
Label(N um1 ) = {N um@0, Reg N um@1}
Label(Lab1 ) = {Lab@0, N um Lab@0, Reg Lab@1}
Label(+1 ) =

Reg +(Reg1 , N um2 )@2, Reg +(N um1 , Reg2 )@2,


R10P 2 +(Reg, Reg)@2, R11P 2 +(Reg, N um)@1, R12P 2 +(N um, Reg)@1,
Label(Ref1 ) =

Reg Ref (R11P 2)@2, Reg Ref (R12P 2)@2

Dans notre exemple, on voit que lon peut ne garder que deux pavages pour ce noeud Ref1 :
les pavages correspondant a` lapplication des r`egles 6, 11 et 22, 8, 12 . Si lon choisi une r`egle
pour la racine, par exemple la r`egle 11, en re-parcourant une derni`ere fois larbre en partant de la
racine, on peut supprimer les r`egles ne devant pas e tre utilisees, cela donne :
Label(N um1 ) = {N um@0}
Label(Lab1 ) = {Reg Lab@1}
Label(+1 ) =
Label(Ref1 ) =

R11P 2 +(Reg, N um)@1


Reg Ref (R11P 2)@3

Cette methode permet de generer le code en 3 passes sur larbre : une passe bottom up qui ne
conserve que les meilleures r`egles pour chaque noeud, une passe top down qui choisit la r`egle a`
pour le noeud racine et enfin un nouvelle
utiliser pour chaque noeud en fonction du meilleur cout
76

passe bottom up pour generer le code. Dans cette approche, les e tiquettes sont calculees au moment
de la compilation. Si maintenant on decide de precalculer ces nouvelles e tiquettes, on ne va plus
pouvant grossir arbitrairement. Si lon desire utiliser
avoir un ensemble fini detiquette, les cout
cette approche (pour e crire un generateur de generateur de code), on va normaliser les e tiquettes.
En effet, linformation statique de chaque noeud est combien le choix de telle r`egle rajoute au
dej`a calcule. Lorsquon va e valuer les couts
associes a` chaque r`egle dans une e tiquette, on
cout
de la r`egle la moins ch`ere a` chacun des couts.
Pour notre
va normaliser en retranchant le cout
exemple, cela ne change rien pour les noeuds N um1 et Lab1 mais pour les autres, on obtiendrait :
Label(+1 ) =

Reg +(Reg1 , N um2 )@1, Reg +(N um1 , Reg2 )@1,


R10P 2 +(Reg, Reg)@1, R11P 2 +(Reg, N um)@0, R12P 2 +(N um, Reg)@0,
Label(Ref1 ) =

Reg Ref (R11P 2)@0, Reg Ref (R12P 2)@0

On peut faire cela systematiquement pour toutes les possibilites comme on la fait precedemment,
(pour un type stockage de
mais cette fois en ne gardant que les r`egles qui on le meilleur cout
resultat donne).
On peut montrer que lon recup`ere un ensemble fini detiquettes et on peut construire la table

de transition que lon appelle la table de transition consciente des couts


(cost conscious next state
table). On a donc une technique qui permet de produire un generateur de generateurs de code :
en fonction des operateurs et de lensemble de r`egles proposees, on construit un automate qui
` la compilation, cet automate annote chaque noeud avec letat. On peut
travaillera sur un arbre. A
total associe a` chaque noeud. Ensuite, le processus est le meme que
aussi, calculer au vol le cout
precedemment. Cest la technique dite BURS (bottom up rewriting system).
Plusieurs variantes de cette technique existent :
On peut e crire a` la main le programme de ree criture darbre (pattern macher), un peu comme
lalgorithme T ile ci dessus. Cela permet des optimisations et e vite de manipuler une table
qui peut e tre grosse (le generateur de code est compact).
On peut utiliser la technique BURS vue ci-dessus, ce qui permet de faire de la compilation
reciblable.
On peut aussi utiliser des techniques encore plus proche du parsing, en e tendant les techniques vues en debut de cours pour fonctionner sur les grammaires fortement ambigues
utilisees ici.
Enfin on peut lineariser lAST dans un chane en forme prefixee, le probl`eme devient alors
un probl`eme de ree criture de chanes de caract`eres (string matching). On peut utiliser des
algorithmes specifiques pour cela. Cette approche est souvent referencee sous lappellation
Graham-Glanville du nom de ses inventeurs.

9.2 Selection dinstruction par fenetrage


Une autre technique populaire permet de matriser la complexite du traitement en noptimisant que localement. Lidee derri`ere loptimisation par fenetrage est que lon peut grandement
ameliorer la qualite du code en examinant un ensemble restreint doperations adjacentes. Loptimiseur travaille sur du code assembleur, il a une fenetre coulissante (sliding windows, peephole) qui
` chaque e tape, il examine le code present dans la fenetre et recherche
se prom`ene sur le code. A
des schemas predefinis (pattern) quil remplacera par dautres plus efficaces. La combinaison dun
nombre limite de schemas et dune fenetre de petite taille peut aboutir a` des traitement tr`es rapides.
Un exemple classique est un store suivi dun load :
storeAI
loadAI

r1
r0 , 8

r0 , 8
r15

storeAI
i2i

r1
r1

r0 , 8
r15

Dautres exemples : une identite algebrique simple, un saut suivi dun saut :
addI r2 , 0
mult r4 , r7

r7
r10

77

mult r4 , r2

r10

L10 :

jumpI
jumpI

L10
L11

jumpI
L10 : jumpI

L11
L11

` lorigine, appliquant des schemas de ree criture tr`es simple, les selecteurs dinstructions par
A
fenetrage ont progressivement integre des techniques de calcul symbolique. Le traitement est
maintenant decompose en trois e tapes :
1. Expansion (expander), reconnaissance du code et construction dune representation interne
de bas niveau (low level IR : LLIR). Par exemple, les effet de bords comme laffectation des
registres de code condition sont mentionnes explicitement. Cette ree criture peut e tre faite
localement sans connaissance du contexte.
2. Simplification (simplifier), application de r`egles de ree criture en une passe simple sur la LLIR.
Les mecanismes les plus utilises sont la substitution (forward substitution), les simplifications
algebriques (x+0 = x), la propagation de constante (12+5 = 17), tout en nobservant quune
fenetre limitee du code a` chaque e tape. Cest la partie cle du processus.
3. Generation (matcher), transformation de lIR en lassembleur cible.
Voici un exemple de transformation compl`ete. Considerons le code suivant correspondant a` w
x y :

op

Arg1
2
x

Arg2
y
t1

r10
r11
r12
r13
r14
result
r15

t1
r16
w
r17
r18
r19
r20
M (r20 )

2
@y
rarp + r11
M (r12 )
r10 r13
@x
rarp + r15
M (r16 )
r17 r14
@w
rarp + r19
r18

Supposons une de fenetre 3 instructions :


r10
r11
r12

2
@y
rarp + r11
e tape 1

r10
r12
r13

r13
r14
r15

M (rarp + @y)
2 r13
@x
e tape 4

r14
r17
r18

2
rarp + @y
M (r12 )
e tape 2

2 r13
M (rarp + @x)
r17 r14
e tape 7

r18
r17 r14
r20
rarp + @w
M (r20 ) r18
e tape 10

r10
r13
r14

r14
r15
r16

2 r13
@x
rarp + r15
e tape 5

r17
r18
r19

M (rarp + @x)
r17 r14
@w
e tape 8

2
M (rarp + @y)
r10 r13
e tape 3
r14
r16
r17

r18
r17 r14
M (rarp + @w) r18
e tape 11

Le code restant est donc le suivant et apr`es ree criture :

78

2 r13
rarp + @x
M (r16 )
e tape 6
r18
r19
r20

r17 r14
@w
rarp + r19
e tape 9

r13
M (rarp + @y)
r14
2 r13
r17
M (rarp + @x)

r18
r17 r14
M (rarp + @w) r18
e tape 11

loadAI
mult
loadAI
sub
storeAI

rarp , @y
r13 , 2
rarp , @x
r17 , r14
r18

r1 3
r14
r1 7
r18
rarp , w

Notons que pour effectuer certaines transformations (la suppression de certains registres), on

a besoin de savoir que les valeurs sont mortes. La reconnaissance des valeurs mortes joue un role
essentiel dans loptimisation. Pourtant cest typiquement une information non-locale. En realite,
linformation utile est recherchee pendant la phase dexpansion, loptimiseur peut maintenir une
liste de valeurs vivantes par instruction en traduisant le code a` rebrousse poil lors de lexpansion (en partant de la fin). Simultanement, il peut detecter les valeurs mortes apr`es leur derni`ere
utilisation (cela est effectivement applique uniquement a` linterieur dun bloc de base)
du flot complique la tache du simplifieur : la methode la
La presence doperations de controle
plus simple est deffacer la fenetre lorsquon entre dans un nouveau bloc de base (cela e vite de travailler avec des instructions qui nauront peut-etre pas lieu a` lexecution). On peut aussi essayer
de travailler sur le contexte des branchements. Lelimination des labels morts est essentielle, loptimiseur peut maintenir un compteur de reference des labels et agreger des blocs lorsquun label est
supprime, voir supprimer carrement des blocs entiers. Le traitement des operations prediquees
doit e tre prevu aussi.
Comme de nombreuses cibles offrent du parallelisme au niveau des instructions, les instructions consecutives correspondent souvent a` des calculs independants (pour pouvoir faire les calculs en parall`ele). Pour ameliorer son efficacite, loptimiseur peut travailler avec une fenetre vir quune fenetre physique. Avec une fenetre virtuelle, loptimiseur consid`ere un entuelle plutot
semble de calculs qui sont connectes par les flots de valeurs (technique des echelles). Pour cela,
pendant lexpansion, loptimiseur peut relier chaque definition avec la prochaine utilisation de la
valeur dans le meme bloc.

Etendre
cette technique au del`a du bloc necessite une analyse plus poussee des utilisations
atteintes (reaching definitions). Une definition peut atteindre plusieurs premi`eres utilisations et une
utilisation peut correspondre a` plusieurs definitions.
Loptimisation par fenetrage a e te beaucoup utilise pour les premi`eres machines, mais avec
lav`enement des instructions sur 32 bits, il est devenu tr`es difficile de generer tous les schemas
possibles a` la main. On a donc propose des outils qui gen`erent le matcher a` partir dune description de lassembleur de la machine cible. De meme, de plus en plus de compilateurs gen`erent
directement la LLIR en maintenant a` jour les listes de valeurs mortes.
Le compilateur GCC utilise ces principes. La passe doptimisation travaille sur une LLIR appelee register transfer langage, la phase de generation de code peut recibler le code pour une grande
variete darchitectures.

79

10 Introduction a` loptimisation : e limination de redondances


de loptimisation est de decouvrir a` la compilation, des informaDe mani`ere generale, le role
tions sur le comportement a` lexecution et dutiliser cette information pour ameliorer le code
genere par le compilateur. Historiquement, les premiers compilateurs comprenaient tr`es peu
doptimisation, cest pourquoi le terme doptimizing compiler a e te introduit (par opposition aux
debugging compilers utilises pour la mise au point puisque le code produit e tait proche du code
source). Avec larrivee des architectures RISC et des mecanismes au comportement complexe (delay slot, pipeline, unites fonctionnelles multiples, operations memoire non bloquantes, . . .), loptimisation devint de plus en plus necessaire et occupe maintenant une place importante dans le
processus de compilation.
Il serait tr`es long detudier toutes les optimisations en detail, louvrage qui fait reference pour
les optimisations est le livre de Muchnick [Muc]. La figure 2 donne une vision possible de len il existe dautres mani`eres de les
semble des optimisations utilisees dans un compilateur. Bien sur
agencer puisque beaucoup doptimisations sont inter-dependantes, lordre dans lequel on va les
realiser va influencer le resultat.
Sur cette figure, Les lettres indiquent les choses suivantes :
A : ces optimisations sont appliquees sur le code source ou sur une representation intermediaire haut niveau.
B et C : ces optimisations sont faites sur une representation intermediaire de moyen ou bas
niveau.
D : ces optimisations sont faites sur une representation bas niveau qui sera assez dependante
de la machine ( LLIR)
E : ces optimisations peuvent e tre realisees a` ledition de lien pour manipuler les objets
relocables.
Les dependances en pointilles indiquent que ces optimisations peuvent e tre appliquees a` chaque
niveau.
que detudier beaucoup doptimisations superficiellement, on va e tudier une optimisaPlutot
tion particuli`ere : lelimination dexpressions redondantes.
Considerons lexemple suivant : on aimerait bien que le code de gauche soit remplace par le
code de droite dans lequel lexpression 2 z nest calculee quune fois.
t0 2 y
m t0 z
n3yz
o t0 z

m2yz
n3yz
o2yz

10.1 Elimination
dexpressions redondantes avec un AST
Si lon a une representation a` base darbre, on va par exemple representer le code de gauche
par larbre suivant :
InstrList
InstrList

InstrList
m

*
z

*
2

*
2

z
y

On peut alors transformer cet arbre en un graphe acyclique (directed acyclic graph : DAG), dans
80

Scalar replacement of array references


Data cache optimisations
Procedure integration
Tailrecursion optimisation
Scalar replacement of agregates
Sparse conditional constant propagation
Interprocedural constant propagation
Procedure specialization and cloning
Sparse conditional constant propagation

Global value numbering


Local and global copy propagation
Sparse conditional constant propagation
Dead code elimination

Local and global common subexpression elimination


Loop invariant code motion

Constant folding
Algebraic simplifications

Partial redundancy elimination

Dead code elimination


Code hoisting
Inductionvariable strength reduction
Linear function test replacement
Inductionvariable removal
Unnecessary bound checking elimination
Controlflow optimisations

Inline expansion
Leaf routine optimization
Shrink wrapping
Machine idioms
Tail merging
Branch optimization and conditionnal moves
Dead code elimination
Softwarepipelining, loop unrolling, variable expansion
register renaming and hierachical reduction
Basic block and branch scheduling 1
Register allocation by graph coloring
Basic block and branch scheduling 2
Intraprocedural Icache optimization
Instruction prefretching
Data prefretching
Branch prediction

Interprocedural register allocation


Aggregation of global references
Interprocedural Icache optimisation

F IG . 2 Ordre des optimisations dapr`es S. Muchnick

81

un tel DAG, chaque noeud ayant plusieurs parents doit representer une expression redondante.
Par exemple :
InstrList
InstrList

InstrList
m

*
z

*
2

Notons a` loccasion que cette representation ne permet pas de detecter la reutilisation de y z.


pour faire cela, il faudrait faire intervenir les proprietes dassociativite et de commutativite des
operateurs.
La cle est de retrouver de tels schemas pour rendre explicite lutilisation dexpressions redondantes. Le moyen le plus simple est de le faire lors de la construction de lAST. Le parseur
peut construire une table de hachage pour detecter les expressions identiques. Cependant un
mecanisme de reconnaissance purement syntaxique ne peut suffire puisque deux expressions
identiques syntaxiquement peuvent representer deux valeurs differentes, il faut un mecanisme
pour refleter linfluence des assignations.
Le mecanisme utilise est simple : a` chaque variable on associe un compteur qui est incremente
lors des assignation sur la variable. Dans le constructeur de la table de hachage, on rajoute le
compteur a` la fin du nom de la variable, cela permet de ne pas identifier des expressions dont les
valeurs des sous-expressions ont change.
Le probl`eme vient une fois encore des pointeurs. un assignation telle que p = 0 peut potentiellement modifier nimporte quelle variable. Si le compilateur narrive pas a` connatre les
endroits ou` peut pointer p, il est oblige dincrementer les compteurs de toutes les variables. Une
des motivations de lanalyse de pointeurs est de reduire lensemble des variables potentiellement
modifiees par une assignation de pointeur.
Cette approche fonctionne sur les representations intermediaires a` base darbres, elle permet
aussi de diminuer la taille de la representation intermediaire.

10.2 Valeurs numerotees


Pour les representations intermediaires lineaires, la technique utilisee est celle des valeurs
numerotees (value numbering). Cette methode assigne un nombre unique a` chaque valeur calculee
a` lexecution avec la propriete que deux expressions ei et ej ont le meme numero de valeur si et
seulement si elles sevalueront toujours a` la meme valeur dans toutes les executions possibles.
Le principe est similaire a` la technique precedente : le compilateur utilise une table de hachage
pour identifier les expressions qui produisent la meme valeur. La notion de redondant diff`ere
un peu : deux operations ont le meme numero de valeur si elles ont les memes operateurs et
leurs operandes ont les memes numeros de valeur. Lassignation de pointeur a` toujours leffet
desastreux dinvalider tous les numeros de valeurs des variables pouvant e tre pointees par le
pointeur.
Lalgorithme est assez simple quand on se limite a` un bloc de base : a` chaque operation, on
construit une cle de hachage pour lexpression a` partir des numeros de valeurs des operandes. Si
cette cle existe, on peut reutiliser une valeur sinon on introduit la valeur dans la table de hachage.

82

On le fait tourner sur lexemple suivant :


ab+c
bad
cb+c
dad
Code original

a3 b1 + c2
b5 a3 d4
c6 b5 + c2
d5 a3 d4
numero de valeurs

ab+c
bad
cb+c
db
Code apr`es ree criture

Cet algorithme de base peut e tre augmente de plusieurs mani`eres. Par exemple, on peut
prendre en compte la commutativite des operateurs en ordonnant les operandes des operateurs
commutatifs dune mani`ere systematique, on peut aussi remplacer les expressions constantes par
leurs valeurs, decouvrir les identites algebriques etc. Un algorithme possible pour une representation
de type Iloc serait :
Pour chaque operation i dans le bloc, loperation i e tant de la forme opi x1 , x2 , . . . , xk1 xk
1) recuperer les numeros de valeurs de x1 , x2 , . . . , xk1
2) si toutes les entrees de opi sont constantes
e valuer la constante et enregistrer sa valeur
3) Si loperation est une identite algebrique
la remplacer par une operation de copie
4) si opi est commutative
trier les operandes par numero de valeur
5) Construire la cle de hachage dapr`es loperateur et les numeros de valeur de x1 , x2 , . . . , xk1
6) Si la valeur existe dej`a dans la table
remplacer loperation i par une copie
Sinon
assigner un nouveau numero de valeur a` la cle et lenregistrer pour xk .
Dans cette approche le nom des variables a une importance primordiale. Considerons lexemple
suivant :
ax+y
bx+y
a 17
cx+y
Code original

a3 x1 + y 2
b3 x1 + y 2
a4 174
c3 x1 + y 2
numero de valeurs

ax+y
ba
a 17
cx+y
Code apr`es ree criture

La valeur x+y nexiste plus lorsque lon arrive a` la definition de c car elle avait e te enregistree pour
a qui a e te e crasee entre temps. La solution a` ce probl`eme passe par le forme SSA. La ree criture du
code en forme SSA donnera :
a0 x0 + y0
b0 x0 + y0
a1 17
c0 x0 + y0
Code en SSA
On pourra alors non seulement reconnatre que les trois utilisations de x + y sont les memes
mais aussi facilement supprimer les variables b0 et c0 dans le reste du code car on sait quelles ne
risquent pas de changer de valeur.

10.3 Au del`a dun bloc de base


D`es que lon sort dun bloc de base, les choses deviennent nettement plus compliquees. On
rappelle la definition dun bloc de base (dans un IR lineaire) : cest une suite dinstructions qui ne
comporte aucun saut (sauf la derni`ere instruction) et aucun autre point dentree que la premi`ere
instruction (un bloc de base a donc la propriete que, sauf si une exception est levee, toutes les
operations du bloc sexecutent a` chaque execution du bloc).
83

Considerons lexemple suivant :


A

p
r

m
n

a+b
a+b

c+d
c+d

a
r

e
s
u

b+18
a+b
e+f

y
w

a+b
c+d

v
w
x

e
t
u

a+17
c+d
e+f

a+b
c+d
e+f

a+b
c+d

On distingue 4 types de methodes :


1. Les methodes locales travaillent sur les blocs de base.
2. Les methodes superlocales op`erent sur des blocs de bases etendus. Un bloc de base e tendu B
est un ensemble de blocs de base 1 , 2 , . . . , n ou` 1 a de multiples predecesseurs et chaque
i , 2 i n a i1 comme unique predecesseur. Un bloc de base e tendu a donc un unique
point dentree mais plusieurs points de sorties. Sur lexemple ci-dessus, {A, B}, {A, C, D},
{A, C, E}, {F }, {G} sont des blocs de base e tendus (extended basic blocs, EBB)
3. Les methodes regionales travaillent sur des regions de blocs mais pas sur une procedure
enti`ere, par exemple lensemble des blocs de base formant une boucle. Elle prennent en
entrant (ici en F ou G
compte les point de synchronisation ou confluent deux flots de controle
par exemple).
4. Les methodes globales ou intra-procedurales travaillent sur lensemble de la procedure et
utilisent lanalyse flot de donnee pour decouvrir les informations transmises entre blocs
5. les methodes inter-procedurales.
Approche super-locale On peut par exemple calculer la table de hachage correspondant au bloc
A, puis lutiliser comme e tat initial pour numeroter les valeurs du bloc C, puis utiliser la table
resultante comme entree pour le bloc D. Il faudra ensuite recalculer la table pour le bloc A afin de
lutiliser pour le calcul sur le super-bloc {A, B}.
On pourra e viter les calculs redondants de table en utilisant une table de hachage imbriquee
lexicalement comme pour les acc`es aux variables de differentes portees. Lexemple courant est
represente ci-dessous. Lordre de recherche des expressions redondantes pourra e tre :
A, B, C, D, E, F, G. Les assignations entourees representent les calculs redondants decouverts qui
peuvent e tre supprimees (lettre L pour methode locale, S pour methode super-locale).

84

p
r

c+d
c+d

m
n

a+b
a+b

L
a
r

e
s
u

b+18
a+b
e+f

y
w

e
t
u

v
w
x

a+b
c+d

a+17
c+d
e+f

a+b
c+d
e+f

a+b
c+d

La methode super-locale ameliore la detection dexpressions redondantes, mais elle est limitee
aux super-blocs : lexpression a + b dans le bloc F nest pas detectee comme e tant redondante. Un
autre cas qui nest pas detecte est le calcul de e + f dans le bloc F . Le calcul est redondant avec
mais ces
soit e + f du bloc D soit e + f du bloc E suivant le chemin pris par le flot de controle,
deux valeurs ne sont pas e gales.
Approche regionale En arrivant au bloc F , on ne peut utiliser aucune des tables de D ou E
En revanche, on peut utilise la table resultant du
car on ne sait pas par ou est passe le controle.
passage de lalgorithme sur le bloc e tendu {A, C}. En effet, tout programme passant par le bloc
F est obligatoirement passe par les bloc A et C. On peut donc utiliser la table de hachage du bloc
C comme initialisation pour le traitement du bloc F . Attention cependant, il faut verifier que les
valeurs presentes dans la table de hachage du bloc C ne sont pas tuees par un des blocs entre C
et F . Par exemple ici, comme a est redefini dans le bloc C, lexpression a + b du bloc A est tuee et
ne peut e tre reutilisee dans le bloc G. Pour reperer cela, on peut soit passer en SSA soit maintenir
une liste des variables modifiees par bloc et mettre a` jour la table de hachage en consequence. Ce
principe est represente ci-dessus (le D indique les instructions retirees par des considerations de
domination) :
A

p
r

c+d
c+d

m
n

a+b
a+b

L
a
r

e
s
u

b+18
a+b
e+f

y
w

a+b
c+d

v
w
x

a+b
c+d
e+f

e
t
u

a+17
c+d
e+f

a+b
c+d

Le bloc utilise pour initialiser une table de hachage est le dominateur immediat de ce bloc. On

85

de flot, tous les chemins allant


dit quun bloc B1 domine un bloc B2 si, dans le graphe de controle
de lentree du programme au bloc B2 passent par B1 . On note B1
B2 . si B1 = B2 , B1 domine
strictement B2 (B1
B2 ). Cest une relation dordre partielle, lensemble des dominateurs dun
bloc B1 est note Dom(B1 ). Par exemple, Dom(F ) = {A, C, F }. Le dominateur immediat de B1 est
le dominateur strict de B1 le plus proche de B1 : iDom(F ) = C.
On voit assez facilement que le dominateur immediat est celui qui est utilise pour lalgorithme
de detection de redondance : pour demarrer lanalyse dexpressions redondantes dun bloc B1 on
utilise la table de hachage du bloc iDom(B1 ).
Detection de redondances globale Cette approche marche bien tant quil ny a pas de circuits
de flot. On sent bien que le processus se mord la queue puisque pour
dans le graphe de controle
initialiser la table de hachage dun bloc qui se trouve sur un circuit, on a besoin des tables de blocs
qui vont eux-memes e tre initialises avec la table du bloc courant.
Pour resoudre cela on decompose lalgorithme de detection dexpressions redondantes en
deux phases : une premi`ere phase qui identifie toutes les expressions redondantes puis une phase
qui ree crit le code. La phase de detection se fait en utilisant une analyse de flot de donnee (data
flow analysis) pour calculer lensemble dexpressions disponibles a` lentree de chaque bloc. Le terme
disponible a une definition precise :
de flot (CFG) si sa
Une expression e est definie (defined) a` un point p du graphe de controle
valeur est calculee a` p.
Une valeur est tuee (killed) a` un point p du CFG si un de ses operande est defini a` p.
une expression est disponible (available) a` un point p du CFG si chaque chemin qui m`ene a` p
contient une definition de e, et e nest pas nest pas tue entre cette definition et p.
Une fois quon a linformation quune valeur est disponible, on peut creer une variable temporaire
et inserer des copies au lieu de recalculer.
Le compilateur va donc annoter chaque bloc B avec un ensemble Avail(B) qui contiendra
lensemble des expressions disponibles a` lentree de B. Cet ensemble peut e tre specifie par un
ensemble dequations deduites de la definition de disponibilite.
On definit, pour chaque bloc B, lensemble Def (B) contenant lensemble des expressions
definies dans B et non tuees ensuite dans B. Donc e Def (B) si et seulement si B e value e
et aucun des operandes de e nest definit entre levaluation de B et la fin du bloc.
Ensuite, on definit lensemble N otKilled(B) qui contient les expressions qui ne sont pas tuees
par une definition dans B. Cest a` dire que si e est disponible en entree de B (e Avail(B)) et que
e N otKilled(B) alors e est disponible a` la fin de B.
Une fois que ces deux ensembles sont calcules, on peut e crire une condition que doit verifier
lensemble Avail(B) :
Avail(B) =

(Def (p) (Avail(p) N otKilled(p)))

(1)

ppred(B)

Le terme Def (p) (Avail(p) N otKilled(p)) specifie lensemble des expressions qui sont
disponibles en sortie de p. Cette e quation ne definit pas Avail(B) a` proprement parler (sauf si le
CFG ne contient pas de circuit) car Avail(B) est peut-
etre defini en fonction de lui-meme. On sait
simplement que les ensembles Avail(B) forment un point fixe de cette e quation. On va donc les
trouver par un algorithme de point fixe qui consiste a` iterer cette e quation sur tous les blocs de
base B du CFG jusqu`a ce que les ensembles Avail(B) ne changent plus.
Le calcul des ensembles Def (p) est immediat, le calcul des ensembles N otKilled(p) est plus
delicat. Voici un algorithme qui calcule les deux en meme temps :
//On travaille sur un bloc B avec les operations o1 , o2 , . . . , ok .
Killed
Def (B)
for i=k to 1
//supposons que oi soit : x y + z
si (y
/ Killed) et (z
/ Killed) alors
ajouter x + y a` Def (B)
86

ajouter x a` Killed
N otKilled(B) {toutes les expressions}
Pour chaque expression e
Pour chaque variables v e
si v Killed alors
N otKilled(B) N otKilled(B) e
Pour calculer les ensembles Avail on peut utiliser un simple algorithme iteratif (on suppose que
les blocs du programme ont e te numerotes de b0 a` bn ).
for 0 i n
Calculer Def (bi ) et N otKilled(bi )
Avail(bi )
Changed T rue
Tant que (Changed)
Changed F alse
for 0 i n
OldV alue Avail(bi )
Avail(bi ) := ppred(bi ) (Def (p) (Avail(p) N otKilled(p)))
si Avail(bi ) = OldV alue alors
Changed T rue
On peut montrer que ce probl`eme a` une structure specifique qui en fait un calcul de point
fixe. Les ensembles Avail ne vont faire que grossir jusqu`a ce quils ne bougent plus. Ces ensembles Avail(bi ) realisent lequation (1) (il sont en fait obtenus en iterant de nombreuses fois
cette e quation dans un certain ordre). Les e quations decrivant les ensembles Avail plus haut sont
des equations dun probl`eme danalyse de flot de donnee global (global data flow analysis problem). On
explore ce concept plus en detail dans la section suivante.
La phase de ree criture de lelimination de redondances doit remplacer toute expression e tiquetee
comme redondante par une copie de la valeur calculee anterieurement (on suppose en general que
ces copies peuvent e tre e liminees par une passe ulterieure doptimisation).
Le traitement est effectue en deux passes lineaires sur le code : la premi`ere passe identifie les
endroits ou des expressions redondantes sont recalculees. La table des valeurs de chaque bloc b
est initialisee avec Avail(b), chaque expression redondante e decouverte est redefinie avec une
copie dun nouveau temporaire tempe associe a` e. La deuxi`eme passe initialisera le temporaire
dans tous les blocs de base ou lexpression e est definie. Le resultat final sur sur notre exemple est
illustre ci-dessous a` gauche (la lettre G signifiant global). A droite on a represente le code apr`es la
passe de ree criture. On suppose en general quune passe doptimisation ulterieure essayera denlever des copies par substitution.

87

m
t1
n

A
A

p
r

c+d
c+d

m
n

a+b
a+b

L
a
r

e
s
u

y
w

a+b
c+d

a+b
c+d

b+18
a+b
e+f

a+b
m
t1

v
w
x

p
t2
r

e
t
u

a+17
c+d
e+f

c+d
p
t1

a
r
t2

e
s
u
t3

b+18
a+b
e+f
u

a+b
c+d
e+f

D
G
G

G
G

y
w

t1
c+d
r

v
t1
w
x

e
t
u
t3

a+17
t2
e+f
u

a+b
v
t2
t3

t1
t2

Pour aller plus loin La methode utilisant lanalyse data flow pour resoudre ce probl`eme nest
pas forcement la meilleure. On peut essayer de pousser encore les methodes super-locales qui
sont relativement simples. Pour e viter les probl`emes poses par les expressions e + f dans F et
c + d dans G, on peut faire de la replication de code. il y a deux methodes utilisees : la replication
de blocs (cloning) et le deroulement de procedure (inline substitution).
La replication de bloc permet de supprimer des points de convergence du graphe qui faisaient
perdre linformation des expressions calculees. Le compilateur va simplement rajouter une copie
des blocs F et G a` la fin des bloc C et D ainsi quune copie du bloc G a` la fin du bloc B.
A

B
G

p
r

c+d
c+d

y
w

a+b
c+d

m
n

a+b
a+b

e
s
u
v
w
x

b+18
a+b
e+f
a+b
c+d
e+f

y
w

a+b
c+d

a
r

a+b
c+d

e
t
u
v
w
x
y
w

a+17
c+d
e+f
a+b
c+d
e+f
a+b
c+d

Malgre lexpansion de code, cette transformation a plusieurs avantages : elle cree des blocs plus
longs qui se comportent mieux pour diverses optimisations, elle supprime des branchements a`
lexecution et elle cree des opportunites doptimisation qui nexistait pas avant a` cause de lam
bigute introduite par la convergence des flots de controle.
le deroulement de procedure (inline substitution) consiste a` remplacer le code dun appel de
procedure par une copie du code de la procedure appelee. Cela permet de faire une optimisation
plus precise : le contexte de lappel de procedure est transmis au corps de la procedure. En particulier, certaines branches compl`etes peuvent e tre supprimees du fait que certaines conditions
du flot peuvent e tre resolues statiquement. Il faut utiliser cette transforutilisees dans le controle
mation avec precaution, en particulier si la pression sur les registres est forte.
88

11 Analyse flot de donnees


Lanalyse flot de donnees (data flow analysis) est un raisonnement statique (`a la compilation)
sur les flots dynamiques des valeurs (`a lexecution). Cela consiste en la resolution dun ensemble
dequations deduites dune representation graphique du programme pour decouvrir ce qui peut
arriver lorsque le programme est execute. On lutilise essentiellement pour trouver des opportunites doptimisation.

11.1 Exemple des variables vivantes


Une variable est vivante a` un point p si il y a un chemin entre p et une utilisation de v suivant lequel v nest pas redefinie. Cette information est utilisee par de nombreuses optimisations
(allocation de registres, passage en SSA, etc.).
Lensemble Live(b) des variables vivantes a` la sortie du bloc b est defini par lequation suivante :
Live(b) =

U sed(s) (Live(s) N otDef (s))

(2)

ssucc(b)

Une variable est vivante a` lentree dun bloc s si elle est utilisee dans s ( U sed(s)) ou si elle est
vivante en sortie de s sans avoir e te redefinie dans le s.
Ici, lensemble Live dun noeud est une fonction des ensembles Live de ses successeurs, lin On appelle cela un probl`eme
formation remonte le long des arcs du graphe de flot de controle.
remontant (backward problem). Avail e tait un probl`eme descendant (forward problem).
Pour resoudre ce type de probl`eme il faut construire le graphe de dependance, rassembler des
informations locales a` chaque bloc (ici U sed et N otDef ) puis utiliser un algorithme de point fixe
iteratif pour propager linformation dans le CFG.
se
Construction du graphe de flot de controle

La construction du graphe de flot de controle


fait en general a` partir dune representation intermediaire lineaire. On identifie dabord les instructions leader (qui sont en tete dun bloc de base). Une instruction est leader si cest la premi`ere
de la procedure ou si elle poss`ede un label qui est la cible potentielle dun branchement. Si le
programme ne poss`ede par de saut sur registre (jump r1 en Iloc), seul les labels cibles de
branchements sont leader. Si il y a une telle instruction, tous les labels deviennent leader. Les
compilateurs ins`erent quelquefois des instructions virtuelles suivant les sauts qui indiquent les
labels susceptibles detre cibles du saut correspondant (instruction tbl en Iloc)
On identifie ensuite la fin des blocs, qui correspondent a` un branchement conditionnel, un
saut (il faut e ventuellement prendre en compte les delay slots) ou un autre leader.
Informations locales aux blocs

Le calcul des ensembles U sed et N otDef est simple ici :

for i=1 to #operations


//supposons que oi soit : op x, y z
si (oi est leader) alors
b numero de bloc de oi
U sed(b)
N otDef (b) {toutes les variables}
si x N otDef (b) alors
ajouter x a` U sed(b)
si y N otDef (b) alors
ajouter y a` U sed(b)
N otDef (b) N otDef (b) z

89

Resolution des e quations Pour resoudre les e quations on utilise une variante de lalgorithme
vu pour la detection dexpressions redondantes.
for i=1 to #blocs
Live(bi )
Changed T rue
Tant que (Changed)
Changed F alse
for i=1 to #blocs
OldV alue Live(bi )
recalculer Live(bi ) en utilisant lequation
si Live(bi ) = OldV alue alors
Changed T rue
Considerons le CFG suivant :
i gt 100

B0

i lt 100

B1

B2

b
c
d

...

...

...
...
...

B4

y
z
i

a+b
c+d
i+1

...
...

B5

...

B6

a
d

B3

...

...

i lt 100

i gt 100

Le deroulement de ces algorithmes donne les resultats suivants :

U sed
N otDef

B0

a, b, c, d, y, z
iteration B0
0

3
i
3
i
3
i

B1

b, d, i, y, z
B1

a, i
a, i
a, c, i
a, c, i

B2

a, i, y, z

B3

b, c, i, y, z

B2
B3

a, b, c, d, i

a, b, c, d, i

a, b, c, d, i a, c, d, i
a, b, c, d, i a, c, d, i
a, b, c, d, i a, c, d, i

90

B4

a, b, c, i, y, z

B4

a, c, d, i
a, c, d, i
a, c, d, i
a, c, d, i

B5

a, c, d, i
a, c, d, i
a, c, d, i
a, c, d, i

B5

a, b, d, i, y, z

B6

a, c, d, i, y, z

B6
B7

a, b, c, d, i
a, b, c, d, i i
a, b, c, d, i i
a, b, c, d, i i
a, b, c, d, i i

B7
a, b, c, d, i
a, b, c, d

11.2 Formalisation
Nous allons introduire une formalisation pour la resolution de probl`emes data flow par une
methode iterative (telle que celle que nous avons vu jusqu`a present). On introduit la notion formelle de probl`eme data flow iteratif comme e tant un quadruplet G, L, F, M ou` :
du morceau de code analyse (CFG dans
G est un graphe qui represente le flot de controle
une procedure, graphe dappel dans un programme).
L est un semi-treillis qui represente les faits analyses. Un semi-treillis est un triplet S, , .
S est un ensemble, est loperateur minimum (meet operator) et est lelement minimal
(bottom) de L. On va associer un e lement de S a` chaque noeud de G.
F est un espace de fonction :F : L L. Lanalyseur utilise les fonctions de F pour modeliser
les transmissions de valeurs dans L le long des arcs G.
M associe les arcs et les noeuds de G a certaines fonctions de F.
Si L est un semi-treillis, alors loperateur doit e tre idempotent (a a = a), commutative et
associatif. Cet operateur induit un ordre partiel sur L : a b a b = b. Lorsque a b = c avec
c = a et c = b, a et b ne sont pas comparable. Lelement minimal est lunique e lement tel que
a = (donc a , a L).
Pour la propriete de vivacite, on a la formalisation suivante :
On recherche des ensembles de variables associes a` chaque bloc, lensemble S peut donc
e tre lensemble des parties de V : S = 2V (V e tant lensemble des variables du programme).
loperateur minimum doit specifier comment combiner deux ensemble venant de deux flots
qui convergent, ici les ensembles sont reunis : est lunion. La relation dordre
de controle
partiel est alors : a b b a.
lelement minimal est lensemble de toutes les variables = V
Lespace des fonctions doit permettre dexprimer les operations ensemblistes realisees sur un
bloc donne dans les e quations data flow. Ici, on utilise uniquement la fonction : f (x) = c1 (xc2 )
(U sed(b) et N otDef (b) sont des constantes pour tout b). Lespace des fonctions est donc lensemble
des fonctions de la forme f (x) = c1 (x c2 ) pour toutes les valeurs possibles de c1 et c2 .
Enfin, il faut definir M, cest a` dire associer a` chaque arcs et noeuds du CFG une fonction de
F. On va associer a` un arc x, y entre deux blocs de base, la fonction f x,y (t) = U sed(y) (t
N oT Def (y)). Lalgorithme que nous avons donne plus haut peux donc secrire :
for i=1 to #blocs
Live(bi )
Changed T rue
Tant que (Changed)
Changed F alse
for i=1 to #blocs
OldV alue Live(bi )
Live(bi ) = bk succ(bi ) f bi ,bk (Live(bk ))
si Live(bi ) = OldV alue alors
Changed T rue
On peut maintenant analyser plus formellement cet algorithme, cest a` dire montrer sa terminaison, sa correction et e tudier son efficacite.
Terminaison Une chane decroissante est une sequence x1 , x3 , . . . , xn ou xi L et xi > xi+1 .
Considerons lensemble des chanes decroissantes possibles dans L, si la longueur des chanes
dans cet ensemble est bornee (par exemple par d), alors on dit que L poss`ede la propriete des
chanes decroissantes finies. Ici, L e tant fini, il a forcement la propriete des chanes decroissantes
finies.
On remarque ensuite que les fonctions dans F sont monotones : x y x y = x, f (x) =
f (x y) = f (x) f (y) ( en effet : f (x y) = c1 ((x y) c2 ) = c1 ((x c2 ) (y c2 )) =
`
(c1 (x c2 )) (c1 (y c2 )) = f (x) f (y)). Et donc x y f (x) = f (x) f (y) f (x) f (y) A

91

chaque e tape de lalgorithme, on peut former un ensemble de chanes decroissantes au sens large
dans L en partant de Live(b). Par exemple, a` letape i :
Liveetapei (b) =

fb,bk (Liveetapei1 (bk ))


bk succ(b)

bk succ(b), Liveetapei (b) fb,bk (Liveetapei1 (bk )) = Liveetapei (b)


bk succ(b), Liveetapei (b) fb,bk (Liveetapei1 (bk ))
P uis :
bk succ(bl )fb,bk (Liveetapei1 (bk )) = fb,bk ( bl succ(bk ) fbk ,bl (Liveetapei2 (bl )))
bl succ(bk ) fb,bk (fbk ,bl (Liveetapei2 (bl )))
donc :
bk succ(b), bl succ(bk ), Liveetapei (b) fb,bk (Liveetapei1 (bk )) fbk ,bl (fbk ,bl (Liveetapei2 (bl )))
etc.
Donc en deroulant lalgorithme on peut construire un ensemble de chane decroissantes terminant par les Live(bi ) a` letape courante, ces chanes ont une longueur bornee donc lalgorithme
termine forcement (les Live(bi ) ne changent plus).
Correction Le concepteur doit ensuite donner du sens aux e lements de L : si v Live(bi ), alors
il existe un chemin depuis la sortie du bloc bi jusqu`a une operation qui utilise la valeur de v
et suivant lequel v nest pas redefini (appelons cela un chemin v-libre). La solution donnee par
lalgorithme doit e tre telle que pour chaque variable v Live(b), il existe un chemin v-libre de b
a` une utilisation de v. La plupart des probl`emes data-flow sont formules en utilisant lensemble
de tous les chemins possibles ayant une certaine propriete. On parle de meet over all path solution.
Comment relier le point fixe donne par lexecution de lalgorithme (qui assure une propriete entre
voisin) a` la propriete ci-dessus qui est une propriete sur un ensemble de chemin ?
La methode habituelle est darriver a` prouver que la solution que lon recherche est un point
fixe extremal de lequation. Dans notre exemple, le point fixe maximal correspondra a` des ensembles avec le moins delements possibles. Or on peut montrer (sous certaines hypoth`eses qui
sont remplies ici) que lalgorithme que lon propose va precisement converger vers ce point fixe
extremal.
En deroulant un nombre infini de fois lequation (2), on trouve la propriete e quivalente :
Live(b) =

fp ()

(3)

chemin p de nf a b

Ou nf est le bloc de sortie du CFG et fp est la composition des fonctions f induites par les arcs
du chemin de nf a` p (le chemin est parcouru dans le sens oppose aux arcs : le probl`eme est
remontant). En effet, on doit avoir Live(nf ) = et on a lequation (2) qui specifie la relation entre
Live(b) et ses suivants.
On voit facilement que si les ensemble Live(b) sont definis par lequation (3) alors il forment
un point fixe de lequation (2) :
ssucc(b)

U sed(s) (Live(s) N otDef (s))

= ssucc(b) U sed(s) ( p:nf s fp () N otDef (s))


= ssucc(b) f b,s ( p:nf s fp ())
= p:nf s fp ()
= Live(b)

Donc en resume :
1. Comme le probl`eme est admissible, lalgorithme de point fixe iteratif donne lunique plus
grand point fixe du probl`eme.
2. Comme la solution que nous recherchons est le plus grand point fixe du probl`eme, cest bien
la solution donnee par lalgorihme
92

Complexite Le probl`eme ayant un unique plus grand point fixe, la reponse ne depend pas de
lordre dans lequel lalgorithme calcule les ensembles Live des blocs a` chaque e tape. Pour un
probl`eme remontant on aura interet a` visiter les noeuds fils avant le noeud p`ere (postorder). De
meme pour les ensemble Avail, on utilisera un ordre inverse postorder (profondeur dabord).
Pour la plupart des analyses data flow, il y a deux ordres de parcours du graphe qui sont importants : les ordres postorder et reverse postorder. Un ordre postorder ordonne autant de des fils
possibles dun noeud (dans un certain ordre) avant de visiter le noeud, un ordre reverse postorder
visite autant de p`eres possibles dun noeud avant de visiter le noeud :

Postorder

Reverse postorder

Dans certain cas, on peut dire plus sur la complexite : si le probl`eme a la propriete que
f, g F, x F, f (g()) g() f (x) (x)
alors lalgorithme sarretera en au plus d(G) + 3 iterations ou d(G) est le nombre maximal darc
de retour dans un chemin acyclique du graphe. On parle de probl`eme data flow rapide (rapid
framework).
Notre probl`eme est un probl`eme rapide car si f (x) = c1 (x c2 ), et g(x) = c3 (x c4 ),
f (g()) = c1 ((c3 ( c4 )) c2 ) = c1 ((c3 c4 )) c2 ) et dautre part g() f (x) (x) =
(c3 ( c4 )) c1 (x c2 ) x = c3 c4 c1 (x c2 ) x = c3 c4 c1 x et on a bien
((c3 ( c4 )) c2 ) c3 c4 c1 x et donc f (g()) g() f (x) (x)
Limitations Lanalyse data flow suppose que tous les noeuds du CFG peuvent e tre pris, en pratique on peut souvent prouver a` la compilation que de nombreux chemins ne sont jamais pris et
donc ameliorer le resultat obtenu par lanalyse data flow. On dit que linformation est precise a`
hauteur de lexecution symbolique.
Un autre probl`eme vient des imprecisions generees par les pointeurs et les tableaux. Une
reference a` un tableau A[i, j, k] nomme un seul e lement : A. Sans une analyse poussee sur les
valeurs de i,j et k le compilateur ne peut pas dire quel e lement du tableau A est accede. Dans ce
cas, lanalyse doit e tre conservatrice.
Pour les pointeurs, si on ne sait pas ou pointent les pointeurs, une affectation a` un objet pointe
peut empecher de conserver les variables dans des registres.

11.3 Autres probl`emes data-flow


Reaching definitions Le compilateur a quelquefois besoin de connatre lendroit ou` un operande
est defini. Lensemble des definitions de cet operande qui peuvent atteindre linstruction que
lon e tudie est appele une reaching definition. Une definition d dune variable v peut atteindre
loperation i si et seulement si i lit la valeur de v et quil existe un chemin v-libre entre d et i.
Lequation data flow pour les ensemble Reaches(n) qui contient lensemble des definitions pouvant atteindre le bloc n est :
Reaches(n) =

Def (y) (Reaches(y) Survived(y))


ypreds(n)

93


Survived est conceptuellement la meme chose que N otKilled mais concerne des definitions plutot
que des expressions.
Very busy Une expressions e est consideree comme tr`es utilisee (very busy) a` un point p si e est
e valuee et utilisee selon tous les chemins qui partent de p et que levaluation de e a` p produirait le
meme resultat que levaluation e a` lendroit ou elle est e valuee (sur les differents chemins partant
de p). Lanalyse very busy est un probl`eme remontant :
V eryBusy(b) =

U sed(a) (V eryBusy(a) Killed(a))


asucc(b)

Propagation de constante La propagation de constante (constant propagation) peut avoir lieu


quand le compilateur sait quune variable v atoujours la meme valeur c a` un point p du code.
Le domaine de ce probl`eme est un ensemble de paires : vi , ci ou vi et une variable et ci une
constante ou une valeur speciale . Lanalyse va annoter chaque noeud du CFG avec un ensemble
Constant(b) qui contient toutes les paires variable-constante que le compilateur peut prouver a`
lentree du bloc b. Lequation est la suivante :
fp (Constant(p))

Constant(b) =
ppreds(b)

ou compare deux paires : v1 , c1 et v1 , c2 de la mani`ere suivante : v1 , v1 , c2 = v1 , si


c1 ou c2 vaut ou si c1 = c2 et v1 , c1 v1 , c2 = v1 , c1 si c1 = c2 =
La fonction fp est un peu plus compliquee a` specifier car elle doit e tre detaillee en fonction de
chaque instruction oi du bloc, exemple :
xy
x y op z

si Constant(p) = { x, l1 , y, l2 , . . .} alors foi (Constant(p)) = Constant(p) x, l1 x, l2


si Constant(p) = { x, l1 , y, l2 , z, l3 , . . .} alors
foi (Constant(p)) = Constant(p) x, l1 x, l2 op l3

Le compilateur peut utiliser le resultat de cette analyse pour e valuer certaines operations a`
la compilation, e liminer des branchements lors de tests sur des valeurs constantes. Cest une des
transformations qui peut amener des ameliorations spectaculaires de performances.

94

12 Static Single Assignment


les analyses data flow prennent beaucoup de temps pour analyser les memes choses (utilisation de valeurs, etc.). La forme SSA permet de construire une representation intermediaire qui
et le flot de donnees.
encode directement des informations sur le flot de controle
Apr`es passage en SSA, le code respecte deux r`egles : 1) chaque definition cree un nom unique
et 2) chaque utilisation se ref`ere a` une definition unique.

12.1 Passage en SSA


Un algorithme simple pour passer en SSA est le suivant :
` chaque jointure du CFG, inserer des fonction pour tous les
1. insertion de fonctions : A
noms de variables. Par exemple au debut dun bloc ayant deux predecesseurs, le compilateur inserera linstruction y (y, y) pour chaque nom y. Lordre dans lequel les fonctions sont introduites na pas dimportance car la semantique dit que toutes les fonctions
doivent lire leur argument en parall`ele, puis e crire leurs resultats simultanement.
2. Renommage : Apr`es avoir insere les fonctions , le compilateur peut calculer les reaching
definition, du fait que les fonctions sont au debut des blocs, il y a necessairement une
definition par utilisation. On peut alors renommer les utilisations en fonction de la definition
qui latteint (`a linterieur dun bloc), par exemple :
y (y, y)
y (y, y)
1
... y
. . . y1
Puis le compilateur trie les definitions de chaque variable v dans chaque bloc pour extraire
celle qui atteint la fonction phi dans les successeurs du bloc.
La forme resultante est appelee la forme SSA maximale. Un des probl`eme de la forme SSA maximale est quelle contient beaucoup trop de fonctions . On va maintenant voir comment on
construit une SSA semi-simplifiee (semi-pruned SSA). La cle pour reduire le nombre de fonctions
est dutiliser la notion de fronti`ere de dominance.
fronti`ere de dominance On peut calculer les ensembles Dom(b) des blocs qui dominent le bloc
B par une e quation data flow simple :
Dom(b) =

({n} Dom(p))
ppreds(n)

avec les conditions initiales Dom(n) = {n} pour tout n. Ce probl`eme est un probl`eme rapide descendant (tr`es proche de Live). Par exemple, pour le graphe suivant, il trouve la solution en une
iteration (pour un order postorder particulier du graphe) :
B0

B0

B1

B1

B2

B2

B3

B4

B3

B5

B4

B6

B6

B7

B7

Iteration/bloc

1
2

0
{0}
{0}
{0}

1
{1}
{0, 1}
{0, 1}

B5

2
{2}
{0, 1, 2}
{0, 1, 2}

3
{3}
{0, 1, 3}
{0, 1, 3}
95

4
{4}
{0, 1, 3, 4}
{0, 1, 3, 4}

5
6
7
{5}
{6}
{7}
{0, 1, 3, 5} {0, 1, 3, 6} {0, 1, 7}
{0, 1, 3, 5} {0, 1, 3, 6} {0, 1, 7}

On peut maintenant determiner plus precisement quelle variable va necessiter une fonction
a` chaque jointure du CFG. Considerons une definition dune variable dans un bloc n du CFG,
cette valeur peut atteindre toutes les instructions des noeuds m pour lesquels n Dom(m) sans
necessiter une fonction puisque chaque chemin qui passe par m passe par n (si cest une autre
definition de la meme variable, elle e crase la definition du bloc n).
Une definition dans un noeud n du CFG force une fonction dans un noeud qui rassemble
des noeuds qui sont en dehors de la region du CFG que n domine. Plus formellement : une
definition dans le noeud n force la creation dune fonction dans tout noeud m ou (1) n domine
un predecesseur de m et (2) n ne domine pas strictement n
/ Dom(m) {m}. Lensemble des
noeuds ayant cette propriete par rapport a` n est appelee la fronti`ere de dominance de n (dominance
frontier) DF (n).
Pour calculer une fronti`ere de dominance DF (n), on consid`ere les proprietes suivantes : Les
noeuds de DF (n) doivent e tre des noeuds de jointure (ayant plusieurs p`eres), les predecesseurs
dun tel noeud j DF (n) doivent avoir le noeud j dans leur fronti`ere de dominance sinon ils domineraient j (et comme n les dominent, n dominerait j). Enfin, les dominateurs des predecesseurs
de j doivent aussi avoir j dans leur fronti`ere de dominance car sinon il domineraient j aussi. On
en deduit lalgorithme suivant :
1. Identifier les noeuds de jointure j (noeuds a` plusieurs predecesseurs).
2. Examiner chaque predecesseur p de j dans le CFG. On remonte alors dans larbre de domination en rajoutant j dans la fronti`ere de dominance du noeud courant tant que lon ne
rencontre pas le dominateur immediat de j. (intuitivement, le reste des dominateurs de j
sont partages par tous les predecesseurs de j, comme ils dominent j il nauront pas j dans
leur fronti`ere de dominance).
Lalgorithme est le suivant :
pour chaque noeud b
Si le nombre de predecesseurs de b est > 1
pour chaque predecesseurs p de b
courant p
Tant que courant = iDom(b)
ajouter b a` DF (courant)
courant iDom(courant)

Placement des fonctions Considerons un ensemble de noeuds S contentant toutes les definitions
de la variable x. Appelons J(S) lensemble des noeuds joints de S (set of joint nodes of S) defini par
J(S)={noeud z du CFG tel quil existe deux chemins pxz et pyz ayant z comme premier noeud
commun (pour tout x S et y S)}. Tous les noeud dans J(S) vont necessiter lajout dune
fonction pour la definition de x.
Mais cette nouvelle fonction cree une nouvelle definition de x, donc on doit de nouveau
calculer un ensemble de noeud joint, on definit ainsi J + (S) : lensemble itere des noeuds joints de J
comme la limite de la suite : J1 = J(S) et Ji+1 = J(S Ji ). J + (S) est exactement lensemble des
noeuds necessitant une fonction pour la definition de x. Un resultat important pour placer les
est le suivant, on peut montrer que :
J + (S) = DF + (S)
ou DF + (S) est la fronti`ere de dominance iteree, obtenu de la meme mani`ere comme limite de la
suite : DF1 = DF (S) et DFi+1 = DF (S DFi ) (on voit mieux maintenant pourquoi on sest
interesse au calcul efficace de la fronti`ere de dominance des noeuds).
Renommage des variables Une fois que les fonctions sont placees, il faut renommer les variables pour passer en assignation unique (pour linstant le code est plein dinstructions du type
x (x, x)). On fait cela par un parcours pre-order en maintenant une liste de nom pour chaque
variable.
96

13 Quelques transformations de code

13.1 Elimination
de code mort
Si le compilateur peut prouver quune operation na aucun effet exterieur visible, loperation
est dite morte (le compilateur peut la supprimer). Cette situation arrive dans deux cas : soit
ne contient loperation),
loperation est inatteignable (aucun chemin possible dans le flot de controle
soit loperation est inutile (aucune operation nutilise le resultat).

Elimination
de code inutile Lelimination de code inutile se fait un peu comme la gestion du
tas (mark-sweep collector), elle agit en deux passes. La premi`ere passe commence par reperer les
operation critiques : entrees/sorties, appels de procedure, codes dentree et de sortie des procedures,
passages de resultats et marque toutes ces operations comme utiles. Ensuite elle cherche les
definitions des operations definissant les operandes de ces operations utiles, les marque et re` la fin de la premi`ere passe, toutes les
monte ainsi recursivement de definition en definition. A
operations non marquees sont inutiles. En general on pratique cela sur une forme SSA car on sait
quune utilisation correspond a` une seule definition. La deuxi`eme passe va simplement e liminer
les operations inutiles.
Lors de la premi`ere passe, les seules operations dont le traitement est delicat sont les branchements et les sauts. Tous les sauts sont consideres comme utiles, les branchements ne sont
consideres comme utiles que si lexecution dune operation utile depend de leur presence. Pour
relier une operation au branchement qui la rend possible, on utilise la notion de dependance de
grace a` la notion de postdomination.
controle. On definit les dependances de controle
Dans le CFG, un noeud j postdomine un noeud i si chaque chemin de i vers le noeud de sortie
du CFG passe par j (i.e. j domine i dans le CFG inverse). Un noeud j est dependant dun noeud i
si et seulement si :
du point de vue du controle
1. Il existe un chemin non nul de i a` j dans lequel j postdomine chaque noeud sur le chemin
apr`es i.
2. j ne postdomine pas strictement i. Un arc quite i et m`ene a` un chemin ne contenant pas j
qui va vers le noeud de sortie de la procedure.
Notons que cette notion correspond a` la fronti`ere de postdominance qui est exactement la fronti`ere
de dominance du graphe inverse (fronti`ere de dominace inversee, reverse dominance frontier). Lidee
est la suivante : si un noeud j postdomine un noeud i, alors
derri`ere les dependances de controle
quel que soit le resultat de levaluation dun branchement dans le noeud i, le noeud j sera atteint
quand meme, donc les branchements du noeud i ne sont pas utiles pour atteindre le noeud j.
Voici un exemple :
r
r

1
1

2
4

exit

{}

{r}

2
3
4
5
6
7

{2,r}
{2}
{r}
{3}
{3}
{2}

exit
7

CFG inverse

exit

Postdominateurs

Frontire de postdominance

Graphe de dpendence de controle

CFG

Lorsquune operation dun bloc bi est marquee comme utile par la premi`ere passe, on visite
chaque bloc sur la fronti`ere de dominance inversee de bi . Chacun de ces blocs se finit par un

97

branchement qui est utile pour arriver au bloc bi , donc on marque les branchements de fin de
blocs comme utiles pour tous ces blocs et on les rajoute a` la liste dinstructions a` traiter.
Si un branchement est non marque alors ses successeurs jusqu`a son post-dominateur immediat
ne contiennent aucune operation marquee. Dans la deuxi`eme phase, chaque branchement de
fin de bloc non marque est remplace par un saut au premier post dominateur qui contient une
operation marquee comme utile. Le graphe resultant peut e ventuellement contenir des blocs vides
qui peuvent e tre enleves par la suite.

Elimination
doperations de controle
de flot inutile Cette operation fait appel a` quatre transformations (voir schema ci-dessous). Le court circuit (branch hoisting) arrive lorsque lon rencontre
un bloc vide qui contient un branchement en fin de bloc, on peut alors recopier le branchement
dans les blocs precedents. Il faut bien distinguer les branchement des sauts, si il y a un saut en fin
de bloc dun bloc vide, on peut supprimer le bloc.

B1

B1

B1

B2

B2

B2

B2

Suppression dun bloc vide

Branchement redondant

B1

B1

B1
B2

B2

B1

B2

Agregation de blocs

B2

Courtcirctuit dun bloc vide

Aucune de ces transformations ne permet de supprimer une boucle vide : considerons par exemple
le graphe ci-dessous et supposons que B2 soit vide. Le branchement de B2 nest pas redondant,
B2 ne termine pas par un saut, il ne peut donc pas e tre combine avec B3. son predecesseur B1
finit par un branchement, il ne peut pas e tre fusionne avec et il ne peut pas e tre court-circuite.

Original

B1

B1

B2

B2

B3

B3
code inutile

B1

B1

B3

B3

suppression B2

suppression redondance

Une combinaison des deux e liminations vues ici peut supprimer la boucle, lelimination de code
mort ne marquera pas le branchement finissant B2 comme utile (B3 postdomine B2 donc B2
/
RDF (B3)). le branchement sera alors remplace par un saut et les operations de simplification du
de flot feront le reste.
controle

98


Elimination
de code inateignable Lelimination des blocs ne se trouvant sur aucun chemin de
est facile apr`es la premi`ere passe du processus precedent.
controle

13.2 Strength reduction


La transformation designee par strength reduction remplace une suite repetitive doperations
complexes par une suite repetitive doperations simples qui calculent la meme chose. Elle est
essentiellement utilisee pour les acc`es aux tableaux dans les boucles.
Considerons la boucle simple suivante :
sum 0
for i 1 to 100
sum sum + a(i)
Un exemple code Iloc pouvant e tre genere apr`es passage en SSA est donne ici (on suppose que
le tableau est declare entre en a[1..100]) :

l1

l2

loadI
loadI
loadI
phi
phi
subI
multI
addI
load
add
addI
cmp LE
cbr
...

0
1
100
ri0 , ri2
rs0 , rs2
ri1 , 1
r1 , 4
r2 , @a
r3
r4 , rs1
ri1 , 1
ri2 , r100
r5

rs0
ri0
r100
ri1
rs1
r1
r2
r3
r4
rs2
ri2
r5
l1 , l2

l1

l2

rs0
ri0
r100
ri1
r s1
r1
r2
r3
r4
rs2
ri2
r5
cbr r5
...

0
1
100
phi(ri0 , ri2 )
phi(rs0 , rs2 )
ri1 1
r1 4
r2 + @a
mem(r3 )
r4 + rs1
ri1 + 1
ri2 <? r100
l1 , l2

Naturellement, a` chaque iteration de la boucle, ladresse de a(i) est recalculee et les registres
r1 , r2 et r3 sont l`a uniquement pour calculer cette adresse. Lidee de la transformation de strength
reduction est de calculer directement cette adresse a` partir de sa valeur a` literation precedente. En
effet, les valeurs prises par r3 lors des iterations successives sont : @a, @a + 4, @a + 8, ..., @a + 396.
En plus du gain lie aux registres supprimes et au remplacement de la multiplication par une
addition, cette transformation permet davoir une forme de code plus agreable pour la suite,
en particulier si le jeu dinstructions contient des modes dadressage avec auto-increment des
operateurs. Nous presentons ici un algorithme simple pour la transformation de strength reduction
appele OSR.
Graphe reductible Pour optimiser les boucles, il faut avoir une definition precise de ce que cest
quune boucle. Une boucle est un ensemble de noeud S du CFG qui contient un noeud dentree h
(header) avec les proprietes suivante :
De nimporte quel noeud de S il existe un chemin dans S arrivant a` h.
Il y a un chemin de h a` nimporte quel noeud de S.
il ny a aucun arc venant de lexterieur de S et aboutissant a` un autre noeud de S que h.
On peut montrer que cest e quivalent a` dire que que S est une composante fortement connexe du
CFG et que h domine tous les noeuds de S. Une boucle peut donc avoir plusieurs sorties mais une
seule entree. Voici deux exemples de graphes ne correspondant pas a` des boucles.

99

1
1

2
2

La notion de boucle ainsi definie correspond exactement a` la notion de graphe reductible. Un


graphe est reductible si il peut e tre ramene a` un seul sommet par application repetitive de la
transformation suivante : un sommet qui a un seul predecesseur peut e tre fusionne avec ce
predecesseur. Les graphes presentes ci dessus ne sont pas reductibles. On peut montrer que
de flot obtenus a` partir de programmes contenant des mecanismes de
les graphes de controle
structures (cest a` dire tout -meme les break- sauf les goto) sont des graphes reductibles.
controle
On suppose ici que lon travaille sur des graphes reductibles.
Algorithm OSR Lalgorithme va rechercher des points ou` une certaine operation (par exemple
une multiplication) sexecute a` linterieur dune boucle et dont les arguments sont :
1. une valeur qui ne varie pas lors des differentes iterations de la boucle (constante de region,
region constant), et
2. une valeur qui varie systematiquement diteration en iteration (variable dinduction).
Une telle operation est appelee une operation candidate. Pour simplifier lexpose de lalgorithme,
on ne va considerer que les operations candidates ayant la forme suivante :
xij

xji xij xj+i

ou i est une variable dinduction et j est une constante de region. Si le code est en SSA, le compilateur peut determiner si une variable est une constante de region simplement en regardant sa
definition, si cest une constante ou si le bloc domine la region de la boucle alors la variable est
une constante de region.
Pour determiner si une variable est une variable dinduction, lalgorithme verifie la forme des
instructions qui mettent a` jour la variable, les formes de mises a` jours autorisees sont :
1. une variable dinduction plus une constante de region
2. une variable dinduction moins une constante de region
3. une fonction avec comme arguments une variable dinduction et une constante de region.
4. une copie de registre a` registre a` partir dune variable dinduction.
Une variable dinduction est definie par rapport a` une certaine boucle. Il peut y avoir des boucles
imbriquees, et une variable dinduction pour une boucle peut e tre une constante de region pour
une autre. Une boucle est reperee par une composante fortement connexe du CFG. On parle donc
du status dune variable par rapport a` une certaine boucle. On va reperer les boucles, et lancer
OSR sur une r
egion particuli`ere de CFG qui correspond a` une boucle.
Lalgorithme va travailler sur le graphe de dependance mais pour des raisons pratiques on va
lorienter de mani`ere inhabituelle : il y aura un arc i j si j est utilise pour calculer i (cest le
graphe qui indique les definitions a` partir des utilisation appele aussi graphe de SSA). Pour notre
code, le graphe est le suivant :

100

l1

l2

rs0
ri0
r100
ri1
rs1
r1
r2
r3
r4
rs2
ri2
r5
cbr r5
...

0
1
100
phi(ri0 , ri2 )
phi(rs0 , rs2 )
ri1 1
r1 4
r2 + @a
mem(r3 )
r4 + rs1
ri1 + 1
ri2 <? r100
l1 , l2

rs0
ri0

rs1

ri1
4

r1

@a

r2

r3

1
r4

load

100
r100

+
rs2

ri2

l1

l2

<
r5

cbr
pc

Chaque variable dinduction est necessairement definie dans une composante fortement connexe
(CFC) du graphe de SSA, lalgorithme peut identifier toutes le CFC du graphe et tester chaque
operation pour verifier que cest une operation valide pour la mise a` jour dune variable dinduction la fonction Classif yIV fait cela. Lalgorithme complet est le suivant :
Process(r)
si r a` un seul e lement n alors
si n est une operation candidate
Replace(n, iv, rc)
sinon n.Header N U LL
sinon
ClassifyIV(r)

OSR(G)
nextN um 0
Tant quil existe un noeud
n non visite dans G
DFS(n)

DFS(n)
n.N um nextN um + +
ClassifyIV(r)
n.V isited true
Pour chaque noeud n r
n.Low n.N um
si n nest pas une mise a` jour
push(n)
valide pour une variable dinduction
pour chaque operande o de n
r nest pas une variable dinduction
si o.V isited = f alse alors
si r est une variable dinduction
DFS(o)
header n r avec le plus petit n.Low
n.Low min(n.Low, o.Low)
Pour chaque noeud n r
si o.N um < n.N um et o est sur la pile
n.Header header
n.Low min(n.Low, o.Low)
sinon
endfor
Pour chaque noeud n r
si n.Low = n.N um alors
si n est une operation candidate
SCC
Replace(n, iv, rc)
jusqu`a ce ce que x = n faire
sinon
x pop()
n.Header N U LL
SCC SCC {x}
Process(SCC)
Pour trouver les CFC dans le graph de SSA, OSR utilise lalgorithme de Tarjan : la fonction DFS.
Elle effectue une recherche en profondeur dabord sur le graphe. Elle assigne a` chaque noeud un
numero, qui correspond a` lordre dans lequel DFS visite les noeuds. Elle pousse chaque noeud sur
une pile et e tiquette le noeud avec la plus petite e tiquette quelle peut recuperer de ses enfants

101

(cest a` dire le plus petit numero lors dune recherche en profondeur dabord). Lorsquelle a finit
de traiter ses enfants, si letiquette recuperee a` le meme numero que n alors n est le noeud dentree
dune composante fortement connexe. Tous les noeud de la CFC sont dans la pile.
Cette methode pour trouver les CFC a une propriete interessante : lorsquune CFC est depilee,
DFS a` deja visite tous ses enfants. Les operations candidates sont donc visitees apr`es que leurs arguments aient e te traites. On va donc reperer les variables dinduction avant dessayer de reduire
les operations candidates qui lutilisent.
Lorsque DF S trouve une nouvelle CFC, elle est traitee avec P rocess. La procedure Replace
remplace effectivement une operation candidate et gen`ere le nouveau code.
La fonction Replace prend comme argument une operation candidate, et ses arguments : une
variable dinduction (iv) et une constante de region (rc). Replace verifie si la variable dinduction
existe dej`a (en utilisant une table de hachage indexee par loperation et les deux arguments). Si
elle existe, il remplace loperation candidate par une copie. Si elle nexiste pas encore, Replace cree
une nouvelle CFC dans le graphe de SSA, ajoute au graphe de SSA les operations correspondant
au nouveau code, ajoute le nom de la variable dans la table de hachage et remplace loperation
candidate par une copie de la nouvelle variable dinduction. (lors de ce traitement, Replace devra
tester chaque operation ajoutee pour voir si cest une operation candidate et e ventuellement la
reduire).
Sur notre exemple, supposons quOSR commence avec le noeud rs2 et quil visite les fils gauche
` ri1 il fait
avant les fils droit. Il fait des appels recursifs a` DF S sur les noeuds r4 , r3 , r2 , r1 et ri1 . A
des appels recursifs sur ri2 puis ri0 . Il trouve les deux CFC simple contenant la constant 1. Elles
ne sont pas candidates. P rocess les marque comme non-variables dinduction en mettant leur
champs header a` Null. La premi`ere CFC non triviale decouverte est {ri1 , ri2 }, toutes les operations
sont valides pour une variable dinduction donc Classif yIV marque chaque noeud comme variable dinduction en mettant leur champ header qui pointe sur le noeud avec le plus petit numero
de profondeur (ici ri1 ).
Ensuite DF S revient au noeud r1 , son fils gauche est une variable dinduction et son fils droit
est une constante de region. Replace est invoquee pour creer une variable dinduction. Cette variable r1 vaut ri1 1, la nouvelle variable dinduction a` donc le meme increment que r1 . On ajoute
une copie r1 rt1 et on marque r1 comme une variable dinduction. Voici les deux morceaux de
graphe de SSA construit a` cet instant sont les deux graphe ci-dessous sur la gauche :
0

rt0

rt3

rs0

rt7

ri0

rt1

rt4

rs1

rt7

ri1

Copy

Copy

r1

Copy

r2

r2

r3

loqa

100

ri2

+
rt2

r100

l1

rt5

rs2

rt8

l2

<
r5

cbr
pc

Pour i

Pour r*1

Pour r2

Pour r3

Pour su,

Ensuite, on essaye de reduire r2 r1 4, cette operation est candidate, la procedure Replace


duplique la variable dinduction r1 , ajuste lincrement en fonction de la multiplication et ajoute
une copie vers r2 . Linstruction r3 r2 + a duplique linduction precedente en ajustant linitialisation.
Ensuite, DF S arrive sur la CFC qui calcule sum, ce nest pas une variable dinduction car r4
nest pas une constante de region. Restent les noeuds non visites depuis le branchement : il y a

102

une comparaison avec une variable dej`a rencontree. Le graphe de SSA a` ce niveau est represente
ci-dessus.
` ce stade on peut reperer que les variables r1 et r2 sont morte et la variable r3 est calculee
A
plus simplement quauparavant. On peut en general encore optimiser le resultat en ne conservant,
comme utilisation des variables dinductions originales que le test de fin de boucle. Pour cela, on
effectue un remplacement de fonction de test lineaire (linear function test replacement). Le compilateur rep`ere les variables dinductions qui ne sont pas utilisees dans le test de fin de boucle. Pour
cela, le compilateur (1) rep`ere les comparaisons qui sont faites sur des variables dinductions non
utilisees par ailleurs, (2) recherche la nouvelle variable dinduction sur laquelle le test doit e tre
fait, (3) calcule la constante de region necessaire pour ree crire le test et (4) ree crire le code.
Certaines de ces operations peuvent e tre preparees dans OSR, en particulier (1), OSR peut
aussi ajouter un arc fictif entre les noeuds qui ont donne lieu a` des copies, en e tiquetant ces arcs
par les transformations apportees. Dans notre exemple, la serie de reduction cree une chane de
noeuds sur lesquels on rencontre les e tiquettes 1, 4 et +a. Par exemple, voici le graphe de SSA
augmente des arcs fictifs et transforme par remplacement des fonctions de test lineaires.
0

rt0

1
ri0

rt3

0
rs0

rt7
+a

x4

rt1

rt4

rt7

rs1

ri1

Copy

Copy

r1

Copy

r2

r3

r2

load

r4

+a

x4

+
rt2

rt5

396+a

rt8

+
ri2

rs2

l1
<
r5

cbr
pc

Voici le code apr`es ree criture :

l1

l2

loadI
loadI
addI
phi
phi
load
add
addI
cmp LE
cbr
...

0
@a
rt6 , 396
rt6 , rt8
rs0 , rs2
rt7
r4 , rs1
rt7 , 4
rt7 , rlim
r5

103

rs0
rt6
rlim
rt7
rs1
r4
rs2
rt8
r5
l1 , l2

l2

14 Ordonnancement dinstructions
En introduction, on avait utilise cette portion de code Iloc qui utilisait peu de registres. Ici on
a rajoute les dates dexecution de chaque instruction (en nombre de cycles), en supposant : quun

chargement depuis la memoire coutait


trois cycles et quune multiplication coutait
deux cycles.
On a aussi suppose que les operateurs e taient pipelines, cest a` dire que lon pouvait commencer
un nouveau chargement alors que le chargement precedent netait pas termine.
1
loadAI
rarp , @w
r1
// load w
4
add
r1 , r 1
r1
// r1 w 2
5
loadAI
rarp , @x
r2
// load x
8
mult
r1 , r 2
r1
// r1 (w2) x
9
loadAI
rarp , @y
r2
// load y
12
mult
r1 , r 2
r1
// rw (w2) x y
13
loadAI
rarp , @z
r2
// load z
16
mult
r1 , r 2
r1
// r1 (w2) x y z
18
storeAI
r1
rarp , @w
// e criture de w
Lordonnancement des instructions ci-dessous est meilleur car il peut sexecuter en 11 cycles au
lieu de 18 (mais il utilise un registre de plus).
1
loadAI
rarp , @w
r1
// load w
2
loadAI
rarp , @x
r2
// load x
3
loadAI
rarp , @y
r3
// load y
4
add
r1 , r 1
r1
// r1 w 2
5
mult
r1 , r 2
r1
// r1 (w2) x
6
loadAI
rarp , @z
r2
// load z
7
mult
r1 , r 3
r1
// rw (w2) x y
9
mult
r1 , r 2
r1
// r1 (w2) x y z
11
storeAI
r1
rarp , @w
// e criture de w
Lordonnancement de code essaye de cacher les latences des operations, comme les chargements en memoire ou les branchements, pour cela il exploite le parallelisme de niveau instruction
(instruction level parallelism, ILP) qui est un parallelisme de grain tr`es fin par opposition au parallelisme de tache ou au parallelisme entre iterations de boucles.
Lordonnancement dinstructions prend comme entree une suite partiellement ordonnee dinstructions, et produit comme sortie une liste des memes instructions. Le travail dordonnancement
dinstructions est extremement lie a` celui de lallocation de registres, mais il est generalement admis que resoudre les deux probl`emes simultanement est trop complexe. En general lordonnancement passe avant lallocation de registre, mais de plus en plus il est repasse apr`es lallocation
de registre pour ameliorer le code.
Lordonnanceur travaille sur le graphe de precedence P = (N, E) qui est un graphe de dependance
entre instructions. Chaque noeud n N est une instruction du code original, un arc e = (n1 , n2 )
E si et seulement si n2 utilise le resultat de n1 comme argument. Chaque noeud a en plus deux
attributs : un type doperation et une duree (ou latence). le noeud n sexecutera sur lunite fonctionnelle type(n) et son execution durera delai(n) cycles. Les noeuds sans predecesseurs sont appeles feuilles (il peuvent e tre ordonnances nimporte quand). Les noeuds sans successeurs dans
le graphe sont appeles racines, ce sont les noeuds les plus contraints.

Etant
donne un graphe de precedence, un ordonnancement associe chaque noeud n a` un entier
positif S(n) qui represente le cycle dans lequel il sera execute. Il doit respecter les contraintes
suivantes :
1. S(n) 0 (on suppose aussi quil y a au moins une operation telle que S(n) = 0).
2. si (n1 , n2 ) E, S(n1 ) + delai(n1 ) S(n2 ). Cette contrainte assure la validite de lordonnancement.
3. Lensemble des operations sexecutant a` un cycle donne ne contient pas plus doperations
de type t que la machine cible ne peut en executer en parall`ele.

104

On definit alors la longueur ou latence de lordonnancement comme


L(S) = maxnN (S(n) + delay(n))
Le graphe de precedence fait apparatre une quantite interessante : les chemins critiques. Laccumulation des delais en remontant le long des chemins dans le graphe de precedence permet dannoter
chaque noeud avec le temps total minimal quil faudrait pour atteindre la racine (cest a dire en
ajoutant tous les delais des operations sur le plus long chemin jusqu`a une racine). Ci dessous
nous avons represente le graphe de precedence du code original, en annotant chaque noeud avec
ce temps. On voit que sur ce graphe, le chemin critique est le chemin abdf hi.
a
b
c
d
e
f
g
h
i

loadAI
add
loadAI
mult
loadAI
mult
loadAI
mult
storeAI

rarp , @w
r1 , r 1
rarp , @x
r1 , r 2
rarp , @y
r1 , r 2
rarp , @z
r1 , r 2
r1

r1
r1
r2
r1
r2
r1
r2
r1
rarp , @w

// load w
// r1 w 2
// load x
// r1 (w2) x
// load y
// rw (w2) x y
// load z
// r1 (w2) x y z
// e criture de w
a

13

12

10

e10

g8

f7

h5

i3

On sent bien que lordonnanceur a interet a` ordonnancer linstruction a en premier. Le chemin


critique devient alors cdef hi qui sugg`ere dordonnancer c. Ensuite, b et e sont a` e galite mais b a
besoin du resultat de a qui ne sera disponible que dans un cycle. En continuant ainsi on arrive a
lordonnancement acebdgf hi qui est celui que lon cherchait. Cependant, le compilateur ne peut
pas ree crire le code avec cette ordonnancement en utilisant exactement les memes instructions. En
effet, c et e definissent toutes les deux r2, si lon ram`ene e avant d, il faut renommer le resultat de
e. Cette contrainte ne vient pas des dependances de flot mais par des contraintes de nommage sur
les valeurs. On parle danti-dependance : une instruction e crit a` un endroit qui est lu auparavant
par une autre instruction. Il peut aussi y avoir des dependances de sortie lorsque deux instructions
e crivent dans la meme variable. Un code en SSA ne poss`ede ni anti-dependance si dependance de
sortie, il ne poss`ede que des vrai dependances ou dependances de donnees.
Lordonnancement peut prendre cela en compte de deux mani`eres : soit il respecte les antidependences comme les dependances du graphe de precedence et nordonnance pas e avant d,
soit il renomme les valeurs pour e viter les anti-dependances. Ici il y a deux anti-dependances : e
avec d et g avec f et deux dependances de sortie : e avec c et g avec e.
Lordonnancement influe sur la duree de vie des variables, Il doit a` la fois ne pas ordonnancer
une variable avant que ses operandes ne soient prets pour ne pas introduire de cycles inutilises
(idle) et ne pas ordonnancer trop longtemps apr`es que ses operandes soient prets pour ne pas
mettre trop de pression sur les registres. Ce traitement est NP-complet, en pratique, la plupart
des ordonnancements utilises dans les compilateurs sont bases sur le list scheduling.
105

14.1 List scheduling


Le list scheduling est un algorithme glouton pour lordonnancement dinstructions dans un
une approche rassemblant un ensemble dalgorithmes. De nombreuses
bloc de base. Cest plutot
implementations existent qui varient essentiellement dans la priorites des instructions a` choisir
dans la liste.
Le list scheduling travaille sur un bloc de base. Lalgorithme suit les quatre e tapes suivantes :
1. Renomme les variables pour supprimer les anti-dependances. Chaque definition recoit un
nom unique (cest de la SSA limitee a` un bloc). Cette e tape nest pas strictement necessaire.
2. Construit le graphe de precedence P. Lalgorithme parcours le bloc de la derni`ere instruction a` la premi`ere, creant un noeud e tiquete par sa latence a` chaque operation, et ajoutant
un arc vers les operations utilisant la valeur (si les anti-dependances nont pas e te enlevees,
il faut aussi rajouter ces arcs).
3. Assigne des priorite a` chaque operation. Lalgorithme utilisera ces priorites comme guide
pour choisir une operation a` ordonnancer parmi la liste des operations pretes. La priorite la
plus populaire est le poids du chemin critique de ce noeud a` une racine.
4. Selectionne iterativement une operation pour lordonnancer. La structure de donnee centrale du list scheduling est une liste doperations pretes a` e tre ordonnancees au cycle courant. Chaque operation dans cette liste a` la propriete que ses operandes sont disponibles.
Lalgorithme commence au premier cycle et selectionne toutes les operations quil peut ordonnancer dans ce cycle, ce qui initialise la liste. Il avance ensuite au prochain cycle et met
a jour la liste en fonction de la nouvelle date et de la derni`ere operation ordonnancee.
Cest la derni`ere e tape qui est le coeur de lalgorithme. Elle execute une simulation abstraite
de lexecution du code, ignorant les operations et valeurs, mais ne prenant en compte que les
contraintes de temps. Voici un exemple dalgorithme qui limplemente :
Cycle 0
Ready feuilles de P
Active
Tant que (Ready Active = )
si Ready = alors
choisir une operation op dans Ready
S(op) Cycle
Active Active op
Cycle Cycle + 1
Pour chaque op Active
si S(op) + delay(op) Cycle alors
enlever op de Active
Pour chaque successeurs s de op dans P
si s peut e tre execute
Ready Ready s
La qualite de lalgorithme va dependre de letat de la liste Ready a` chaque e tape (si elle contient
une unique operation pour chaque cycle, le resultat sera optimal) et de la qualite de la priorite
choisie. La complexite de lalgorithme est de O(n2 ) au pire car un choix dans la liste Ready peut
demander une passe lineaire sur toute la liste qui peut contenir toutes les operations dej`a rencontrees. On peut optimiser la mise a` jour de la liste Active en conservant pour chaque cycles la
liste des operations qui termine dans ce cycle.
Pour linitialisation, on peut prendre en compte des contraintes (variables decart, slack time)
provenant des predecesseurs du bloc. De plus, les machines modernes peuvent executer plusieurs
operations en parall`eles sur differentes unites fonctionnelles.
Cette heuristique gloutonne pour resoudre un probl`eme NP-complet se comporte relativement bien en general, mais elle est rarement robuste (de petits changement sur lentree peuvent
entrainer de grosses differences dans la solution). On peut reduire ce probl`eme en resolvant de
106

mani`ere precise les e galites (lorsque plusieurs operations ont la meme priorite). Un bon algorithme de list scheduling aura plusieurs sortes de priorite pour resoudre les e galites. Il ny a pas
de priorite universelle, le choix depend e normement du contexte et est difficile a` prevoir.
le rang dun noeud peut e tre le poids total du plus long chemin qui le contient. Cela favorise
la resolution du chemin critique dabord et tend vers une traversee en profondeur de P.
le rang dun noeud peut e tre le nombre de successeurs immediats quil a dans P. Cela favorise un parcours en largeur dabord de P et tend a` garder plus doperations dans la liste
Ready.
le rang dun noeud peut e tre le nombre total de descendant quil a dans P. Cela amplifie le
phenom`ene de lapproche precedente : un noeud qui produit un resultat beaucoup utilise

sera ordonnance plus tot.


le rang peut e tre plus e leve pour les noeud ayant une longue latence. Les operations courtes
peuvent alors e tre utilisees pour cacher cette latence.
Le rang peut e tre plus e leve pour un noeud qui contient la derni`ere utilisation dune valeur.
Cela diminue la pression sur les registres.
que des
On peut aussi choisir dexecuter lalgorithme ci-dessus en partant des racines, plutot
feuilles. On parle alors de list scheduling remontant (backward list scheduling), par opposition au list
scheduling descendant (forward list scheduling). Une approche assez courante est dessayer plusieurs
versions du list scheduling et de garder le meilleur resultat. Si la partie critique a` ordonnancer est
pr`es des feuilles, le scheduling descendant sera sans doute meilleur et reciproquement si
plutot
la partie critique est pr`es des racines.

14.2 Ordonnancement par region


On peut e tendre le list scheduling aux blocs de base e tendus, par exemple, considerons le CFG
suivant :
a
b
c
d

B1

B2

B4

B3

e
f

B5

h
i

B6

j
k

Il contient deux blocs de base e tendus non reduits a` un bloc : (B1 , B2 , B4 ), (B1 , B3 ). Ces deux EBB
interf`erent par leur premier bloc : B1 . Lordonnanceur doit e viter deux types de conflits lorsquil
ordonnance un des EBB : Si lordonnanceur deplace une operation de B1 en dehors du bloc (par
exemple, c apr`es d dans le bloc B2 . Dans ce cas, c ne sera plus sur le chemin de B1 a` B3 , il faut
` linverse, lordonnandonc introduire une copie de c dans B3 (ajout de code de compensation). A
que cela ne modifie pas le sens du
ceur peut remonter une operation de B2 dans B1 , Pour e tre sur
programme, il faudrait e tre en SSA, on peut e ventuellement faire cela uniquement sur une region
du graphe.
Ici lalgorithme ordonnancerait le bloc e tendu (B1 , B2 , B3 ) puisque cest le plus long, puis B3
(comme partie du bloc e tendu (B1 ,B3 ) a` laquelle on a retire B1 ), puis B4 , B5 et B6 .
La technique du trace scheduling utilise des informations sur le comportement du programme
a` lexecution pour selectionner les regions a` ordonnancer. Il construit une trace, ou un chemin

a` travers le CFG qui represente le chemin le plus frequemment execute. Etant


donnee une trace,
lordonnanceur applique le list scheduling a` la trace enti`ere comme pour un bloc de base e tendu.
Il peut e tre e ventuellement oblige dinserer du code de compensation aux points ou` le flot de

107

entre dans la trace. Lordonnancement des blocs de base e tendus est en fait un cas
controle
degenere du trace scheduling.

14.3 Ordonnancement de boucles


Lordonnancement du corps dune boucle a` un effet crucial sur les performances globales
de lexecution des programmes. Les boucles qui posent le plus de probl`emes sont celles qui
contiennent peu dinstructions et pour lesquelles il est difficile de cacher la latence du saut de fin
de boucle. La cle pour cela est de prendre en compte simultanement plusieurs iterations successives et de faire se recouvrir dans le temps lexecution de ces iterations successives. Par exemple
si on regarde trois iterations successives, on tente donc dordonnancer les instructions de fin de
literation i1, celles du milieu de literation i et celle du debut de literation i+1. Cette technique
est appele pipeline logiciel (software pipelining).
Pour atteindre ce regime permanent, il faut executer un prologue qui remplit le pipeline, et
un e pilogue a` la fin de la boucle, cela peut facilement doubler la taille du code de la boucle.
Considerons la boucle suivante :
for (i = 1; i < 200; i + +)
z[i] = x[i] y[i]
Le code qui pourrait e tre genere est represente ci-dessous. Le compilateur a realise la strength
reduction pour calculer les adresses de x, y et z, il a aussi utilise ladresse de x pour effectuer le
test de fin de boucle (linear function test replacement), le dernier e lement de x accede est le 199ieme ,
avec un decalage de (199 1) 4 = 792.
4
3
2
1
1
2
3
4
5
6
7
8
9

loadI
loadI
loadI
addI
L1 : loadAO
loadAO
addI
addI
mult
cmp LT
storeAO
addI
cbr

@x
@y
@z
r@x , 792
rsp , r@x
rsp , r@y
r@x , 4
r@y , 4
rx , ry
r@x , rub
rz
r@z , 4
rcc

r@x
r@y
r@z
rub
rx
ry
r@x
r@y
rz
rcc
rsp , r@z
r@z
L1 , L2

// adresses et valeurs
// prechargees
// valeurs de x[i] et y[i]
// increment sur les adresses
// cachant la latence des chargements
// le vrai travail
// test pendant la latence de la multiplication

En supposant maintenant quon ait une machine avec deux unites fonctionnelles (on suppose
que les load et store doivent e tre executes sur la premi`ere, en comptant toujours 3 cycle pour un
load/store, deux cycles pour une multiplication), on peut effectuer un certain nombre de taches
en parall`ele :
2
1
1
L1 :
2
3
4
5
6
7

loadI
loadI
loadAO
loadAO
nop
mult
nop
storeAO
cbr

@x
@z
rsp , r@x
rsp , r@y

r@x
r@z
rx
ry

rx , ry

rz

rz
rcc

rsp , r@z
L1 , L2

108

loadI
addI
addI
addI
cmp LT
addI
nop
nop
nop

@y
r@x , 792
r@x , 4
r@y , 4
r@x , rub
r@z , 4

r@y
rub
r@x
r@y
rcc
r@z

En fonction de larchitecture cible, le stockage de z peut ou pas bloquer la machine pendant un


cycle, le corps de boucle durera donc 8 ou 9 cycles. Le corps de la boucle passe un temps significatif a` attendre que les chargements arrivent. Ci dessous est represente le code apr`es application
du pipeline logiciel.
loadI
4
3
loadAO
2
loadAO
1
loadI
0
nop
1
L1 : loadAO
2
loadAO
3
cmp LT
4
storeA0
5
cbr
+1
mult
+2
storeA0

@x
rsp , r@x
rsp , r@y
@z

r@x
rx
ry
r@z

rsp , r@x
rsp , r@y
r@x , rub
rz
rcc
r x , ry
rz

rx
ry
rcc
rsp , r@z
L1 , L 2
rz
rsp , r@z

loadI
addI
addI
addI
nop
addI
mult
addI
i2i
addI
nop
nop

@y
r@y
r@x , 4
r@x
r@y , 4
r@y
r@x , 788 rub

// prologue
// premi`ere iteration (instruction 1)
// premi`ere iteration (instruction 2)

r@x , 4
rx , r y
r@y , 4
rx
r@z , 4

// valeur x[i + 1] et MAJ @x = i + 2


// valeur y[i + 1], calcul x[i] y[i]
MAJ @y = i + 2, copie inseree
// stocke z[i], test iter. i + 1
branchement iter. i, MAJ @z = i + 1
// e pilogue :fin derni`ere
// iteration

r@x
rz
r@y
rx
r@z

Lordonnanceur a plie la boucle deux fois, le corps de boucle travaille donc sur deux iterations
successives en parall`ele. Le prologue execute la premi`ere moitie de la premi`ere iteration, puis
chaque iteration execute la seconde moitie de literation i et la premi`ere moitie de literation i + 1.
La representation graphique permet de mieux comprendre :
cycle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

iteration i
rz rx ry
cmp LT rub
[@z] rz
cbr L1 , L2

iteration i+1
rx [@x]
@x i + 2
ry [@y]
@y i + 2
rx rx

iteration i+2

@z i + 1
rz rx ry
cmp LT rub
[@z] rz
cbr L1 , L2

rx [@x]
ry [@y]

@x i + 3
@y i + 3
rx rx

@z i + 2
rz rx ry
cmp LT rub
[@z] rz
cbr L1 , L2

@z i + 3

On voit que les operations de deux iterations peuvent se recouvrir facilement, ci dessous, en
gras les operations de letape i + 1 :
6
7
8
9
10

rx [@x]
rz rx ry
cmp LT rub
rz [@z]
cbr L1 , L2

@x i + 3
ry [@y]
@y i + 3
rx rx
@z i + 1

Limplementation du pipeline logiciel a fait lobjet de nombreuses recherches et continu a` e tre un


domaine tr`es actif.

109

15 Allocation de registres
Lallocateur de registres a une influence tr`es importante sur les performances du code genere
du fait de lecart croissant entre la rapidite de la memoire et des processeurs. Il prend en entree un
code en representation intermediaire lineaire et produit un programme e quivalent qui est compatible avec lensemble de registres disponibles sur la machine cible. Lorsque lallocateur ne peut
pas garder une valeur dans un registre, il ins`ere le code pour stocker cette valeur dans la memoire
et charger cette valeur de la memoire lorsquon lutilise, ce processus est appele spill. Les valeurs
ambigues (pouvant e tre pointees par plusieurs variables) vont generer des spill obligatoirement,
la surpression de valeurs ambigues peut ameliorer significativement les performances du code
genere.
Le traitement de lallocateur recouvre en fait deux traitements distincts : lallocation de registres et lassignation de registres. Ces deux traitements sont generalement faits en une seule
passe.
Lallocation de registres envoi un nombre non limite de noms de registres sur un nombre fini
de noms virtuels correspondant au nombre de ressources disponibles sur la machine cible. Lallocation assure simplement qu`a chaque cycle, la machine possedera suffisamment de registres
pour executer le code. Lassignation de registre assigne effectivement un registre physique de la
machine a` chaque registres du code sortant de lallocation de registre.
Lallocation de registres est quasiment toujours un probl`eme NP-complet. Le cas simple dun
bloc de base avec une taille uniforme de donnees et un acc`es uniforme a` la memoire peut e tre
resolu optimalement en temps polynomial, mais d`es que lon essaye de modeliser une situation
un peu plus realiste (registres dedies, valeurs devant e tre stockees en memoire, plusieurs blocs
de base) le probl`eme devient NP-complet. Lassignation de registres est en general un probl`eme

polynomial
Les registres de la machine cible sont classes par classe de registre. Quasiment toutes les architectures ont des registres generaux (general purpose registers) et des registres flottants (floating point
register). Beaucoup darchitectures imposent que les param`etres des fonctions soient passes dans
certains registres, les registres de code conditions sont aussi specifiques. LIA-64 a une classe de
registres specifiques pour la predication et pour les adresses cibles de branchement.

15.1 Allocation de registres locale


Lorsque lon ne sinteresse qu`a un bloc de base, lallocation de registres est locale. Le bloc
consiste en une suite de N operations op1 , . . . , opN de la forme opi vri1 , vri2 vri3 (vr pour virtual register). Le nombre total de registres virtuels utilises dans le bloc est M axV R, le nombre total
de registres disponibles sur la machine est k. On suppose pour linstant quaucune contrainte ne
vient des autres blocs du programme. Si M axV R > k, lallocateur doit selectionner des registres
a` spiller. On va voir deux approches pour resoudre ce probl`eme.
La premi`ere methode est dutiliser la frequence dutilisation comme heuristique pour le choix
des registres a` spiller (top down approach). Plus un registre sera utilise dans le bloc, moins il aura de
chance detre spille. Lors du compte des registres, il faut prevoir quun petit nombre de registres
en plus seront necessaires pour traiter les operations sur les valeurs stockees en memoire apr`es
les spill : chargement des operandes, calcul et stockage du resultat (typiquement entre deux et
quatre registres, on designe ce nombre par nbRegSup). Un algorithme possible pour ce traitement
est le suivant :
1. Calculer un score pour classer les registres virtuels en comptant toutes les utilisations de
chaque registre (passe lineaire sur le bloc).
2. trier les registres par score.
3. Assigner les registres par priorite : les k nbRegSup registres sont assignes a` des registres
physiques
4. Ree crire le code : Lors dune seconde passe sur le code, chaque registre assigne a` un registre
physique est remplace par le nom du registre physique. Chaque registre virtuel qui nest
pas assigne utilise les registres conserves a` cet effet, la valeur est chargee avant chaque
utilisation et stockee apr`es chaque definition.
110

La force de cette approche cest quelle conserve les registres tr`es frequemment utilises dans
des registres physique. Sa faiblesse, cest quelle dedie un registre physique a` un registre virtuel
sur toute la duree du bloc, alors quil peut y avoir un tr`es fort taux dutilisation dun registre
virtuel au debut du bloc et plus dutilisation apr`es.
Une autre approche part de considerations bas niveau (bottom-up approach). Lidee est de considerer
chaque operation et de remettre en question lallocation de registres a` chaque operation. Lalgorithme global est le suivant :
Pour chaque operation i de 1 a` N , de la forme opi vri1 , vri2 vri3
prx ensure(vri1 , class(vri1 ))
pry ensure(vri2 , class(vri2 ))
si vri1 nest plus utilise apr`es i alors
f ree(prx , class(prx ))
si vri2 nest plus utilise apr`es i alors
f ree(pry , class(pry ))
rz ensure(vri3 , class(vri3 ))
emit opi rx , ry rz
Le fait de verifier si un registre nest pas utilise permet deviter de spiller un registre alors que
loperation lib`ere un registre. La procedure ensure doit retourner un nom de registre physique
utilise pour chaque registre virtuel rencontre, son pseudo-code est le suivant :
ensure(vr, class)
si vr est dej`a assigne au registre physique pr
result pr
mettre a` jour class.N ext[pr]
sinon
result allocate(vr, class)
emettre le code pour charger vr dans result
return result
Si lon oubli pour linstant la mise a` jour de class.N ext qui est detaillee un peu plus loin, lalgorithme ensure est tr`es intuitif. Lintelligence de lalgorithme dallocation est dans les procedures
allocate et f ree. Pour les decrire, nous avons besoin dune description plus precise de ce que lon
va appeler une classe. Dans limplementation, la classe va connatre letat exact de chacun des
registres physiques de cette classe : est-til alloue a` un registre virtuel, si oui lequel et lindex de
la prochaine reference a` ce registre virtuel (le fameux class.next[pr]). cette derni`ere valeur permettra, lorsque lon devra spiller un registre, de choisir celui qui sera recharge de la memoire le
plus tard possible. La classe contient aussi une structure (par exemple une pile) contenant tous
les registres physiques non utilises. Voici un moyen de realiser une telle structure de donnee en C
et linitialisation correspondante :

struct class {
int Size ;
int N ame[Size] ;
int N ext[Size] ;
int F ree[Size] ;
int Stack[Size] ;
int StackT op ;
}

initialize(class, size)
class.Size size ;
class.StackT op 1
pour i de 0 a` size 1
class.N ame[i]
class.N ext[i]
class.F ree[i] true
push(i, class)

On peut maintenant precisement decrire le comportement des procedures allocate et f ree :

111

allocate(vr, class)
si (class.StackT op 0)
pr pop(class)
sinon
pr registre qui minimise class.N ext[pr]
stocker le contenu de pr
class.N ame[pr] vr
class.N ext[pr] dist(vr)
class.F ree[pr] f alse

f ree(pr, class)
si (class.F ree[pr] = true) alors
push(pr, class)
class.N ame[pr]
class.N ext[pr]
class.F ree[pr] true

La procedure allocate renvoi un registre physique libre (i.e. non alloue) de la classe si il existe un.
Sinon, elle selectionne, parmi les registres physiques de la classe, celui qui contient une valeur
qui est utilisee le plus loin possible dans le futur. Elle stocke cette valeur en memoire et alloue le
registre physique pour vr. La fonction dist(vr) renvoi lindex dans le bloc courant de la prochaine
reference a` vr. Le compilateur peut annoter chaque reference avec les valeurs de la fonction dist
en faisant une simple passe remontante sur le bloc. La procedure f ree fait les actions necessaires
pour indiquer que le registre est libre (en particulier, elle empile le registre physique sur la pile de
sa classe).
Cet algorithme a e te propose par Sheldon Best dans les annees 50 pour le premier compilateur
Fortran, Il produit des allocations excellentes. Certain auteurs ont pretendus que les allocations
e taient optimales, mais il existe des situations pour lesquelles dautres phenom`enes doivent e tre
pris en compte. Par exemple, si une valeur a e te obtenue par un load et na pas e te modifiee. Si on
doit la spiller on na pas besoin de generer un store : la valeur est dej`a en memoire. Une telle valeur
(qui ne necessite pas une recopie en memoire) est dite propre, alors quune valeur necessitant une
recopie est dite sale.
Considerons une machine avec deux registres et supposons que les valeurs x1 et x2 sont dej`a
dans les registres, x1 e tant propre et x2 e tant sale. Supposons que lon rencontre les trois valeurs
dans cette ordre : x3 x1 x2 . Lallocateur doit spiller une des valeurs x1 ou x2 pour charger x3 .
Comme la prochaine utilisation de x2 est la plus lointaine, lalgorithme de Best va la spiller et
produire la sequence suivante :
store x2
load x3
load x2
Or si lon avait choisi de spiller la valeur propre x1 on obtiendrait le code :
load x3
load x1
Ce scenario sugg`ere de spiller les valeurs propres dabord, mais on peut trouver des cas ou ce
nest pas vrai : considerons la sequence dacc`es x3 x1 x3 x1 x2 avec les memes conditions initiales.
En spillant systematiquement les valeurs propres on obtient le code suivant :
load x3
load x1
load x3
load x1
Alors quen spillant x2 on utilisera un acc`es a` la memoire de moins :
store x2
load x3
load x2
Le fait de prendre en compte la distinction entre valeurs propres et valeurs sale rend le probl`eme
NP-complet.
112

De mani`ere generale, ces allocateurs produisent des resultats bien meilleurs que les allocateurs
top down car ils allouent un registre physique a` un registre virtuel uniquement pour la distance
entre deux references au registre virtuel et reconsid`erent cette decision a` chaque appel de la fonction allocate
Notion de duree de vie Lorsque lon prend en compte plusieurs blocs, on ne peut plus faire
lhypoth`ese que les donnees ne sont plus utilisees apr`es la fin du bloc, il faut donc utiliser une
notion plus compliquee : la notion de duree de vie (live range, aussi appelee web) basee sur la
vivacite (liveness).
une duree de vie est constituee dun ensemble de definitions et dutilisations dune valeur qui
sont relies dans le sens que si une definition peut atteindre une utilisation, elle sont dans la meme
duree de vie. On rappelle la definition de vivacite :
` un point p dune procedure, une valeur est vivante si elle a e te definie sur un chemin allant
A
du debut de la procedure a` p et il existe un chemin de p a` une utilisation de v selon lequel v nest
pas redefini.
Considerons le code suivant :
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.

loadI
loadAI
loadI
loadAI
loadAI
loadAI
mult
mult
mult
mult
storeAI

@stack
rarp , 0
2
rarp , 8
rarp , 16
rarp , 24
rw , r 2
rw , r x
rw , r y
rw , r z
rw

rarp
rw
r2
rx
ry
rz
rw
rw
rw
rw
rarp , 0

La valeur de rarp est definie une fois au debut du bloc, ensuite elle est utilisee jusqu`a la fin
du bloc. On en deduit la duree de vie [1, 11]. Considerons rw , elle est definie par les operations
2,7,8,9,10 et utilisee par les operations 7 a` 11, elle definit donc les durees de vie : [2, 7], [7, 8], [8, 9], [9, 10]
et [10, 11]. Le bloc definit aussi les durees de vie suivantes : [3, 7] pour r2 , [4, 8] pour rx , [5, 9] pour
ry , [6, 10] pour rz .
Lorsque lon consid`ere plusieurs blocs de base, les durees de vie ne sont plus des intervalles.
Considerons lexemple suivant :

def y

def x

def x
use y

def y
use x
use y

use x
def x

use x

Les durees de vie sont les suivantes :

113

def y

LR1

use x

use y

def x

def y

def x

use y

LR2

use x

def x

LR3

use x

LR4

Chaque valeur du programme a une duree de vie, meme si elle na pas de nom dans le programme (par exemple les valeurs intermediaires produites par les calculs dadresse). Une variable
peut avoir comme duree de vie plusieurs intervalles distincts correspondant chacun a` la duree de
vie dune valeur differente stockee dans la variable, on peut alors renommer la variable en autant
de noms differents quil y a de durees de vie. On peut donc identifier les valeurs a` stocker dans
les registres (variables, valeurs, temporaires, constantes, etc.) et les durees de vie.
Lallocateur de registre va utiliser ces durees de vie pour placer cette variable dans differents
registres.
On peut facilement trouver les valeurs tuees ou definies dans chaque bloc. Pour un ensemble
de bloc, le compilateur doit decouvrir les valeurs vivantes en entree de chaque bloc et les valeurs
vivantes en sortie de chaque bloc : LiveIn(b) et LiveOut(b) en utilisant une analyse data flow.
Lallocateur travaillant sur une region peut alors utiliser ces informations : une valeur vivante
en fin dun bloc doit e tre stockee en memoire. Cela peut amener des stockages suivis de chargements redondants, mais cela est assez difficile a` detecter sans avoir une vision globale.

15.2 Allocation globale


Levaluation dune allocation de registre est delicate car elle peut se reveler plus ou moins
bonne suivant les executions. Lallocateur global diff`ere de lallocateur local en deux points importants : Le calcul des durees de vie est plus complexe (les durees de vie ne sont plus que des
dun spill nest plus uniforme car plusieurs references a` une meme variable
intervalles) et le cout
peuvent ne pas sexecuter le meme nombre de fois.
Pour decouvrir les duree de vie, il semble judicieux dutiliser la SSA. les fonctions rendent
explicite le fait que des definitions distinctes sur des chemins distincts atteignent une meme
reference. Deux definitions atteignant la meme fonction definissent la meme duree de vie car la
fonction phi cree un nom representant les deux valeurs.
Pour construire les durees de vie a` partir de la forme SSA, lallocateur assigne un ensemble
diff`erent a` chaque nom. Ensuite, il examine les fonctions du programme et fait lunion des
ensembles pour tous les param`etres dune meme fonction . Lorsque toutes les fonctions ont
e te traitees, les ensembles resultant representent les durees de vie maximales du programme.
dun spill. En general les donnees sont alLe compilateur doit avoir une e valuation du cout
louees sur la pile, ce qui e vite dutiliser dautres registres pour le calcul de ladresse. Le com du spill dune variable.
pilateur peut alors e valuer pour une duree de vie particuli`ere, le cout
negatif. Par exemple, un store suivi dun load avec
Certaines duree de vie peuvent avoir un cout
aucune utilisation entre temps devra systematiquement e tre spille. dautres durees de vie auront
infini. Par exemple, une utilisation qui suit immediatement une definition, aucun spill
un cout
infini si aucune
ne pourra reduire le nombre de registres utilise. Une duree de vie aura un cout
duree de vie qui interf`ere ne finit entre la definition et lutilisation et si moins de k 1 valeurs
sont definies entre les definitions et les utilisations (il faut e viter la situation ou k durees de vie
infini).
qui ninterf`erent pas ont toutes un cout
Enfin, pour tenir compte de la frequence dutilisation des differents blocs la plupart des compilateurs utilisent des heuristiques assez primaires : chaque boucle sexecute 10 fois (donc une
du spill dune
double boucle 100 fois), les branchements sont pris une fois sur deux, etc. Le cout

114

reference est alors :


cout
des operations memoire ajoutees frequence dexecution

Graphe dinterference Les interferences representent la competition entre les valeurs pour les
noms des registres physiques. Considerons deux variables dont la durees de vie : LRi et LRj
(dans la suite on identifie les variables avec leur duree de vie). Si il existe une operation ou les
deux variables LRi et LRj sont vivantes, alors ces deux variables ne peuvent pas utiliser le meme
registre physique. On dit alors que LRi et LRj interf`erent.
Le compilateur va construire un graphe dinterference I. Les noeuds de I representent les
durees de vie (ou les variables). Les arcs non orientes representent les interferences. Il existe un
arc (ni , nj ) si et seulement si les variables correspondantes sont toute deux vivantes a` une meme
operation. Considerons lexemple suivant :
B0
LRh

...

LRh
B1

LRi

...

LRj

...

...

LRi
...

LRk

LRj

LRk

B3

...

LRh

...

LRk

LRk

B2

LRi

LRj

Si le compilateur peut construire une k-coloration du graphe dinterference, cest a` dire colorer
chaque noeud dune couleur differente de tous ses voisins, avec uniquement k couleurs, alors il
peut associer une couleur a` chaque registre physique et produire une allocation legale. Pour notre
exemple, si lon associe une couleur LRh , les trois autres variables peuvent recevoir la meme
couleur, differente de celle de LRh , le graphe est 2-colorable.
Le probl`eme de determiner si un graphe est k-colorable est NP-Complet, les compilateurs
utilisent donc des heuristiques rapides qui ne trouvent pas toujours la solution lorsquelle existe.
Ensuite, le compilateur a besoin dune strategie a` executer lorsque lon ne peut pas colorer un
noeud. Il peut soit spiller la variable soit separer la duree de vie. Splitter signifie que toutes les
references a` la variable sont placees en memoire et la duree de vie est separee en une multitude
de minuscules durees de vie. Separer la duree de vie signifie que lon cree des durees de vie plus
petites mais non triviales, en inserant, par exemple des copies ou des spill a` certains endroits.
Dans les deux cas les graphes dinterferences resultant sont differents, si la transformation est
effectuee intelligemment, le nouveau graphe est plus facile a` colorer.
Coloration top down Une premi`ere approche consiste a` partitionner le graphe en deux types de
noeuds : les noeuds non contraints qui ont moins de k 1 voisins et les noeuds contraints qui ont
au moins k voisins. Quelque soient les couleurs des noeuds contraints, les noeuds non contraints
pourront toujours e tre colores. Les premiers noeuds contraints a` e tre colores doivent e tre ceux
dont les temps estimes de spill sont les plus importants. Cet allocateur alloue les couleurs suivant
cet ordre de priorite. Lorsquil rencontre un noeud quil ne peut pas colorer, il separe ou spill
le noeud (pas de backtrack), en esperant que ce noeud sera plus facile a` spiller que les noeuds
dej`a alloues. Separer le noeud peut se reveler interessant car il peut creer des duree de vie non
contraintes.
Coloration bottom up La seconde strategie utilise les memes principes de base : on cherche les
durees de vie, on construit un graphe dinterference, on essaye de le colorer et on ins`ere des spill
lorsque lon ne peut pas. La difference vient de lordre dans lequel on consid`ere les variables.
Lalgorithme utilise est le suivant :

115

Initialiser la pile
Tant que (N = )
si n N avec deg(n) < k
noeud n
sinon
noeud n choisi dans N
enlever noeud et ses arcs de I
empiler noeud sur la pile.
Lallocateur detruit le graphe dans un certain ordre, Le point important ici cest quil empile les
noeuds non contraints avant les noeuds contraints et le fait denlever un noeud contraint dans
un graphe qui na plus de noeud non contraints peut faire apparatre des noeud non contraint.
Lalgorithme reconstruit ensuite le graphe dinterference dans lordre de la pile :
Tant que (pile = )
noeud sommet de pile
inserer noeud et ses arcs dans I
colorer le noeud
Si un noeud ne peut pas e tre colore, il est laisse incolore. Lorsque le graphe a e te reconstruit, si
tous les noeuds sont colores, lallocation est terminee. Si il reste des noeuds incolores, lallocateur
separe ou spill. Le code est alors ree crit et le processus complet recommence (en general deux ou
trois iterations en tout).
Le succ`es de lalgorithme vient du fait quil choisit les noeuds dans un ordre intelligent.
Pour cela, lorsque tous les noeuds du graphe sont contraints, il utilise la spill metric : il prend le
noeud qui minimise le rapport cout
estime du noeud/degre courant du noeud.
Amelioration Lorsque deux durees de vie LRi et LRj qui ninterf`erent pas sont reliees par une
copie de registre a` registre : i2i LRi LRj . On peut fusionner les deux durees de vie, supprimer
loperation de copie et remplacer partout LRj par LRi . Cela ne peut que faire decrotre le degre
des noeuds du graphe (en particulier ceux qui interferaient avec LRi et LRj ).
Lordre dans lequel on effectue ces fusion (coalescing) est important car il peut arriver que deux
fusions soient possibles mais incompatibles. En pratique, les fusions sont effectuees dabord pour
les boucles les plus imbriquees. Le schema global dun allocateur de registre est donc :

Trouver les
dures de vie

Constuire
le graphe

Fusion

spill et itrer

Calculer
cots spill

Trouver un
coloriage

pas de spill

insrer les
spill

On peut generaliser le graphe dinterference et ajouter les contraintes sur les types de registres
(par exemple que les valeurs flottantes ne peuvent pas aller dans les registres entiers). Pour cela,
il suffit de rajouter un noeud au graphe par registre reel et de mettre un arc entre une variable et
un registre physique si la variable ne peut pas e tre stockee dans le registre.

116

References
A Operation de lassembleur lineaire Iloc
La syntaxe generale est opCode Sources T arget
Ci-dessous, ri represente un registre, ci une constante li un label
Opcode
Sources cible Semantique
nop

ne fait rien
add
r1 , r2
r3
r1 + r2 r3
addI
r1 , c1
r2
r1 + c1 r2
sub
r1 , r2
r3
r1 r2 r3
subI
r1 , c1
r2
r1 c1 r2
mult
r1 , r2
r3
r1 r2 r3
multI
r1 , c1
r2
r1 c1 r2
div
r1 , r2
r3
r1 r2 r3
divI
r1 , c1
r2
r1 c1 r2
lshif t
r1 , r2
r3
r1
r2 r3
lsif tI
r1 , c1
r2
r1
c1 r2
rshif t
r1 , r2
r3
r1
r2 r3
rshif tI
r1 , c1
r2
r1
c1 r2
and
r1 , r2
r3
r1 r2 r3
andI
r1 , c1
r2
r1 c1 r2
or
r1 , r2
r3
r1 r2 r3
orI
r1 , c1
r2
r1 c1 r2
xor
r1 , r2
r3
r1 xor r2 r3
xorI
r1 , c1
r2
r1 xor c1 r2
loadI
c1
r2
c1 r2
load
r1
r2
M emory(r1 ) r2
loadAI
r1 , c1
r2
M emory(r1 + c1 ) r2
loadAO
r1 , r2
r3
M emory(r1 + r2 ) r3
cload
r1
r2
load pour caract`eres
cloadAI
r1 , c1
r2
loadAI pour caract`eres
cloadAO
r1 , r2
r3
loadAO pour caract`eres
store
r1
r2
r1 M emory(r2 )
storeAI
r1
r2 , c1 r1 M emory(r2 + c1 )
storeAO
r1
r2 , r3 r1 M emory(r2 + r3 )
cstore
r1
r2
store pour caract`eres
cstoreAI
r1
r2 , c1 storeAI pour caract`eres
cstoreAO
r1
r2 , r3 storeAO pour caract`eres
i2i
r1
r2
r1 r2
c2c
r1
r2
r1 r2
c2i
r1
r2
convertit un caract`ere en entier
i2c
r1
r2
convertit un entier en caract`ere

117

de flot :
Pour le controle
Opcode
Sources cible
cbr
r1
l1 , l2
jumpI

l1
jump

r1
cmp LT
r1 , r2
r3
cmp LE
r1 , r2
r3
cmp EQ
r1 , r2
r3
cmp N E
r1 , r2
r3
cmp GE
r1 , r2
r3
cmp GT
r1 , r2
r3
tbl
r1 , l2

Semantique
r1 = T rue l1 P C, r1 = F alse l2 P C
l1 P C
r1 P C
r1 < r2 T rue r3 , r1 r2 F alse r3
r1 r2 T rue r3
r1 = r2 T rue r3
r1 = r2 T rue r3
r1 r2 T rue r3
r1 > r2 T rue r3
r1 peut contenir l1

Autre syntaxe pour les branchements


Opcode Sources cible Semantique
comp
r1 , r2
cc1 definit cc1
cbr LT
cc1
l1 , l2 cc1 = LT l1 P C, cc1 = LT l2 P C,
cbr LE
cc1
l1 , l2 cc1 = LE l1 P C
cbr EQ
cc1
l1 , l2 cc1 = EQ l1 P C
cbr GE
cc1
l1 , l2 cc1 = GE l1 P C
cbr GT
cc1
l1 , l2 cc1 = GT l1 P C
cbr N E
cc1
l1 , l2 cc1 = N E l1 P C

118

B Assembleur, e diteur de liens, lexemple du Mips


Le processus de compilation ne produit pas directement un code executable, il reste les processus dassemblage, dedition de lien et de chargement de lexecutable qui peuvent modifier le
programme. Ces phases ne sont plus uniquement dependantes du jeu dinstruction de la machine,
elles dependent des conventions utilisees pour le format de lexecutable, des taches realisees
par le syst`eme dexploitation ainsi que du materiel present sur le circuit (unite de gestion de
la memoire par exemple, memoy management unit, MMU). Cette annexe decrit ces e tapes pour
le processeur Mips, elle est tiree de lannexe du Hennessy-Patterson [HP98], e crit par J. R. Larus (http://www.cs.wisc.edu/larus/SPIM/HP_AppA.pdf). Il existe simulateur du jeu
dinstruction du Mips (Instruction Set Simulateur : ISS) appele Spim qui peut e tre telecharge a`
ladresse : http://www.cs.wisc.edu/larus/spim.html (voir le figure 4 plus loin). Ce
type de simulateur est utilise d`es que lon developpe des portions critiques de logiciel (performance, pilote de peripherique, etc.).
Le langage assembleur2 est une representation symbolique du langage machine (binaire) de la
machine. Il permet en particulier lutilisation detiquette (labels3 ) pour designer des emplacements
memoire particuliers. Loutil appele assembleur permet de traduire le langage assembleur vers le
langage machine. Lors de la compilation dun programme, plusieurs sources assembleur sont
compiles independamment vers des programmes objets et des biblioth`eques pre-compilees sont
aussi utilisees. Un autre outil, lediteur de lien, combine tous les programmes objets en un programme executable. Le schema global est donc le suivant :

Les compilateurs usuels incluent directement lassembleur dans le traitement effectue sur le langage de haut niveau, mais plusieurs chemin de compilation sont possibles :

Lorsque les performances du programme executable sont critiques (par exemple pour les syst`emes
embarques), il est encore dusage de programmer en assembleur, ou au moins de verifier que lassembleur genere par le compilateur nest pas trop inefficace (on peut generalement restreindre la
partie traitee en assembleur a` une petite partie critique du programme global). Pour cette raison,
il est important de bien connatre le langage assembleur ainsi que les processus travaillant dessus
(assembleur et e diteur de liens).
2 Attention, en francais on utilise le m
eme terme assembleur pour designer le langage (assembleur ou langage dassemblage) et loutils qui compile le langage (assembleur)
3 jutiliserai le terme labels dans la suite

119

.text
.align 2
.globl main
main:
subu
$sp, $sp, 32
sw
$ra, 20($sp)
sd
$a0, 32($sp)
sw
$0, 24($sp)
sw
$0, 28($sp)
loop:
#include <stdio.h>
lw
$t6, 28($sp)
int
mul
$t7, $t6, $t6
main (int argc, char *argv[])
lw
$t8, 24($sp)
{
addu
$t9, $t8, $t7
int i;
sw
$t9, 24($sp)
int sum = 0;
addu
$t0, $t6, 1
for (i = 0; i <= 100; i = i + 1)
sw
$t0, 28($sp)
sum = sum + i * i;
ble
$t0, 100, loop
printf ("The sum from 0 .. \
la
$a0, str
100 is %d\n", sum);
lw
$a1, 24($sp)
}
jal
printf
move
$v0, $0
lw
$ra, 20($sp)
addu
$sp, $sp, 32
jr
$ra
.data
.align 0
str:
.asciiz "The sum from 0 .. 100 is %d\n"
F IG . 3 Un exemple de programme C et de code assembleur Mips correspondant

B.1 Assembleur
Un exemple de programme assembleur Mips, ainsi que le programme C correspondant, est
montre en figure 3. Les noms commencant par un point sont des directives (aussi appelees balises)
qui indiquent a` lassembleur comment traduire un programme mais ne produisent pas dinstruction machine directement. Les nom suivit de deux points (main, str) sont des labels. La directive
.text indique que les lignes qui suivent contiennent des instructions. La directive .data indique
que les lignes qui suivent contiennent des donnees. La directive .align n indique que les objets
qui suivent doivent e tre alignes sur une adresse multiple de 2n octets. Le label main est declare
comme global. La directive .asciiz permet de definir une chane de caract`eres terminee par le
caract`ere null.
Le programme assembleur traduit un fichier de format assembleur en un fichier contenant
les instructions machine et les donnees au format binaire (format objet). Cette traduction se fait
en deux passes : la premi`ere passe rep`ere les labels definis, la deuxi`eme passe traduit chaque
instruction assembleur en instruction binaire legale.
Le fichier resultant est appele un fichier objet. Un fichier objet nest en general pas executable car
il contient des references a` des labels externes (appele aussi global, ou non-local). Un label est local si
il ne peut e tre reference que depuis le fichier ou il est defini. Dans la plupart des assembleurs, les
labels sont locaux par defaut, a` moins detre explicitement specifie comme globaux. Le mot cles
static en C force lobjet a` netre visible que dans le fichier ou il est defini. Lassembleur produit
donc une liste de labels qui nont pu e tre resolus (i.e. dont on ne trouve pas ladresse dans le fichier
ou ils sont definis).
Le format dun fichier objet sous unix est organise en 6 sections :

120

Le header decrit la taille et la position des autres sections dans le fichier.


La section de texte (ou segment de texte) contient la representation binaire des instructions contenues dans le fichier. Ces instructions peuvent ne pas e tre executables a` cause
des references non resolues
La section de donnees (ou segment de donnees) contient la representation binaire des donnees
utilisees dans le fichier source.
Linformation de relocalisation (ou de relocation) identifie les instructions ou donnees qui
referencent des adresses absolues. Ces references doivent e tre changees lorsque certaines
parties du code sont deplacees dans la memoire.
la table des symboles associe une adresse a` chaque label global defini dans le fichier et liste
les references non resolues a` des labels externes.
les informations de deboggage contiennent quelques renseignements sur la mani`ere avec
laquelle le programme a e te compile, de mani`ere a` ce que le debugger puisse retrouver a`
quelle ligne du fichier source correspond linstruction courante.
En labsence de la connaissance de ladresse ou sera place le code objet, lassembleur suppose
que chaque fichier assemble commence a` une adresse particuli`ere (par exemple ladresse 0) et
laisse a` lediteur de lien le soin de reloger les adresses en fonctions de ladresse de lexecutable
final.
En general, lassembleur permet un certain nombre de facilites utilisees frequemment :
Les directives permettant de decrire des donnees de mani`ere plus conviviale quen binaire,
par exemple :
.asciiz "la somme est....%n"
Les macros permettent de reutiliser une sequence dinstructions a` plusieurs endroits sans la
repeter dans le texte.
Les pseudo-instructions sont des instructions proposees par lassembleur mais non implementees
dans le materiel, cest le programme assembleur qui traduit ses instructions en instructions
executables par larchitecture.

B.2 Edition
de lien
La compilation separee permet a` un programme detre decomposee en plusieurs parties qui
vont e tre stockees dans differents fichiers. Un fichier peut e tre compile et assemble independamment
des autres. Ce mecanisme est autorise grace a` ledition de lien qui suit lassemblage. Lediteur de
lien (linker) effectue trois taches :
Il recherche dans le programme les biblioth`eques (libraries) utilisees, cest a` dire les appels a`
des fonctions dej`a compilees ailleurs.
Il determine les emplacements memoire que vont occuper les differents fichiers composant
le programme et modifie le code de chaque fichier en fonction de ces adresses en ajustant
les references absolues
Il resout les references entre les fichiers.
Un programme qui utilise un label qui ne peut e tre resolu par lediteur de lien apr`es le traitement de tous les fichiers du programme et les biblioth`eques disponibles nest pas correct. Lorsquune fonction definie dans une biblioth`eque est utilisee, lediteur de lien extrait son code de la
biblioth`eque et lint`egre dans le segment de texte du programme compile. Cette procedure peut a`
son tour en appeler dautres et le processus est reproduit. Voici un exemple simple :

121

B.3 Chargement
Une fois ledition de lien reussie, le programme executable est stocke sur un syst`eme de stockage secondaire, tel quun disque dur. Lors de lexecution sur un syst`eme de type Unix, le
syst`eme dexploitation charge le programme dans la memoire vive avant de lexecuter. Le syst`eme
effectue donc les operations suivantes :
1. Lire le header de lexecutable pour determiner la taille des segments de texte et de donnee.
2. Creer un nouvel espace dadressage pour le programme, cet espace est assez grand pour
contenir les segments de texte, de donnee ainsi que la pile.
3. Copier les instructions et donnees de lexecutable dans le nouvel espace dadressage.
4. Copier les arguments passes au programme sur la pile.
5. initialiser les registres de la machine. En general les registres sont remis a` 0, sauf le pointeur
de pile (SP) et le compteur dinstruction (PC).
6. Executer une procedure de lancement de programme qui copie les argument du programme
dans certains registres et qui appelle la procedure main du programme. Lorsque le main se
termine, la procedure de lancement termine avec lappel syst`eme exit.

B.4 Organisation de la memoire


La plupart des architectures materielles de processeurs nimposent pas de convention sur lutilisation de la memoire ou le protocole dappel de procedure. Ces conventions sont imposees par
le syst`eme dexploitation, elles representent un accord entre les differents programmeurs pour
suivre le meme ensemble de r`egles de mani`ere a` pouvoir partager les developpements realises.
Nous detaillons ici les conventions utilisees pour les architectures basees sur le Mips.
Sur un processeur Mips, la memoire vue par le programme est divisee en trois parties. Pour
cette vision de la memoire, on parle encore de memoire virtuelle :

122

La premi`ere partie, commencant a` ladresse 400000hex est le segment de texte qui contient les instructions du programme. La deuxi`eme partie, au dessus du segment de texte est le segment de
donnees. Il est lui meme decoupe en deux parties : les donnees statiques, commencant a` ladresse
1000000hex et les donnees dynamiques. Les donnees statiques contiennent toutes les variables
declarees statiquement dans le programme dont on connait la taille a` la compilation, ces donnees
ont donc une adresse absolue dans lespace dadressage du programme. Les donnees dynamiques
sont utilisees pour creer des variables dynamiquement (avec la fonction C malloc par exemple).
La partie donnees dynamique peut e tre agrandie dynamiquement (lors de lexecution du programme) par le syst`eme dexploitation grace a` lappel syst`eme sbrk (sur les syst`emes Unix). La
troisi`eme partie, le segment de pile, commence a` ladresse 7f f f f f f fhex , cest a` dire a` la fin de
lespace adressable et progresse dans le sens des adresses decroissantes. Il est aussi e tendu dynamiquement par le syst`eme au fur et a` mesure que le programme fait descendre le pointeur de
pile.
Remarque : Du fait que les segments de donnees commencent a` ladresse 1000000hex , on ne
peut pas acceder a` ces donnees avec une adresse de 16 bits. Le jeu dinstruction Mips propose un
certain nombre de mode dadressage utilisant un decalage code sur 16 bits. Par convention, Pour
e viter davoir a` decomposer chaque chargement en deux instructions, le syst`eme dedie un registre
pour pointer sur le debut du segment de donnees : Le global pointeur ($gp) contient ladresse
10008000h ex

B.5 Conventions pour lappel de procedure


La convention logicielle pour lappel de procedure du Mips est tr`es proche de celle que nous
avons expliquee en section 6, GCC ajoute quelques conventions sur lutilisation des registres, elle
doivent donc e tre utilisees si lon veut produire du code compatible avec celui de GCC. Le CPU du
Mips contient 32 registres a` usage general qui sont numerotes de 0 a` 31. La notation $r permet de
designer un registre par autre chose quun numero.
Le registre $0 contient systematiquement la valeur 0.
Les registres $at (registre numero 1), $k0 (reg. 26) et $k1 (reg. 27) sont reserves pour le
syst`eme dexploitation.
Les registre $a0-$a3 (reg. 4-7) sont utilises pour passer les quatre premiers arguments des
procedures (les arguments suivants sont passes sur la pile). Les registres $v0 et $v1 sont
utilises pour stocker les resultats des fonctions.
Les registres $t0-$t9 (reg. 8-15,24,25) sont des registres caller-saved (temporaires), qui sont
e crases lors dappel de procedure.
Les registres $s0-$s9 (reg. 16-23) sont des registres callee-saved qui sont conserves a` travers lappel de procedure.
Le registre $gp (reg. 28) est le pointeur global qui pointe sur le milieu dun block de 64K
dans le segment de donnees statiques de la memoire.
Le registre $sp, (reg. 29, stack pointer) est le pointeur de pile. Le registre $fp (reg. 30,
frame pointer) est lARP. Linstruction jal (jump and link) e crit dans le registre $ra (reg.
31) ladresse de retour pour un appel de procedure.
Voici la separation des taches (calling convention) sur la plupart des machines Mips :
La procedure appelante effectue :
1. Passer les arguments : les 4 premiers sont passes dans les registres $a0-$a4, les autres
123

arguments sont empiles sur la pile.


2. Sauvegarder les registres caller-save $t0-$t9 si cela est necessaire (c.a.d. si lon veut
conserver les valeurs de ces registres apr`es lappel de la procedure).
3. Executer linstruction jal qui saute a` ladresse de la procedure appelee et copier ladresse
de retour dans le registre $ra.
La procedure appelee effectue :
1. Allouer la taille de lenregistrement dactivation sur la pile en soustrayant la taille au
registre $fp.
2. Sauvegarder les registres callee-saved $s0-$s9, $fp et $ra dans lenregistrement
dactivation.

3. Etablir
la valeur du nouvel ARP dans le registre $fp.
Lorsquelle a fini son execution, la procedure appelee effectue les operations suivantes :
1. Si la fonction retourne une valeur, placer cette valeur dans le registre $v0.
2. Restaurer les registres callee-saved.
3. Depiler lenregistrement dactivation en ajoutant la taille de lenregistrement au registre $sp.
4. Retourner, en sautant (jr) a` ladresse contenue dans le registre $ra.

B.6 Exceptions et Interruptions


En general on appelle exception ce qui vient du processeur (e.g. division par 0) ou des coprocesseurs associes et interruption ce qui est genere hors du processeur (par exemple un depassement
de capacite sur la pile). Dans la suite on ne fera pas la distinction.
Dans le processeur Mips, une partie specifique du materiel sert a` enregistrer les informations utiles au traitement des interruptions par le logiciel. Cette partie est referencee comme
le co-processeur 0, pour une architecture generique on parle de controleur dinterruptions. Le coprocesseur 0 contient un certain nombre de registres, nous ne decrivons ici quune partie de ces
registres (la partie qui est implementee dans le simulateur Spim). Voici les quatre registres decrits
du co-processeur 0 :
Nom du registre
BadVAddr
Status
Cause
EPC

Numero du
registre
8
12
13
14

Utilisation
Registre contenant ladresse memoire incorrecte referencee
Masque dinterruption et autorisation dinterruption
Type dexception et bits dinterruption en attente
Adresse de linstruction ayant cause lexception

Ces quatre registres sont accedes par des instructions speciales : lwc0, mfc0, mtc0, et swc0.
Apr`es une exception, le registre EPC contient ladresse de linstruction qui sexecutait lorsque linterruption est intervenue. Si linstruction effectuait un acc`es memoire qui a cause linterruption,
le registre BadVAddr contient ladresse de la case memoire referencee. Le registre Status a` la
configuration suivante (dans limplementation de Spim) :

Le masque dinterruption contient un bit pour chacun des 5 niveaux dinterruption materielle et
124

des 3 niveaux dinterruption logicielle. Un bit a` 1 autorise une interruption a` ce niveau, un bit
a` 0 interdit une interruption a` ce niveau. Les 6 bits de poids faibles sont utilises pour empiler
(sur une profondeur de 3 au maximum) les bits kernel/user et interrupt enable. Le bit
kernel/user est a` 0 si le programme e tait en mode superviseur (ou mode noyeau, mode privilegie) lorsque lexception est arrivee et a` 1 si le programme e tait en mode utilisateur. Si le bit
interrupt enable est a` 1, les interruption sont autorisees, si il est a` 0 elles sont empechees.
Lorsquune interruption arrive, ces 6 bits sont decales vers la gauche de 2 bits de mani`ere a` ce que
les bits courants deviennent les bits precedents (les bits vieux sont alors perdus).
Le regitre Cause a` la structure suivante :

Exception
Pending
interrupts
code
Les 5 bits pending interrupt (interruption en attente) correspondent aux 5 niveaux dinterruption. Un bit passe a` 1 lorsquune interruption est arrivee mais na pas encore e te traitee. Les
bits exception code (code dexception) decrivent la cause de lexception avec les codes suivants :
Nombre
0
4
5
6
7
8
9
10
12

nom
INT
ADDRL
ADDRS
IBUS
DBUS
SYSCALL
BKPT
RI
OVF

description
interruption externe
exception derreur dadressage (load ou chargement dinstruction)
exception derreur dadressage (store)
erreur de bus sur chargement dinstruction
erreur de bus sur load ou store
exception dappel syst`eme
exception de breakpoint
exception dinstruction reservee
depassement de capacite arithmetique

Une exception ou une interruption provoque un saut vers une procedure qui sappelle un gestionnaire dinterruption (interrupt handler). Sur le Mips, cette procedure commence systematiquement
a` ladresse 80000080hex (adresse dans lespace dadressage du noyau, pas dans lespace utilisateur). Ce code examine la cause de lexception et appelle le traitement approprie que le syst`eme
dexploitation doit executer. Le syst`eme dexploitation repond a` une interruption soit en terminant le processus qui a cause lexception soit en executant une autre action. Un processus qui
essaye dexecuter une instruction non implementee est tue, en revanche une exception telle quun
defaut de page est une requete dun processus au syst`eme dexploitation pour reclamer le service : ramener une page du disque en memoire. Les autres interruptions sont celles generees par
les composants externes. Le travail consiste alors a` envoyer ou recevoir des donnees de ce composant et a` reprendre le processus interrompu.
Voici un exemple de code assembleur traitant une interruption sur le Mips. Le gestionnaire
dinterruption sauvegarde les registres $a0 et $a1, quil utilisera par la suite pour passer des
arguments. On ne peut pas sauvegarder ces valeurs sur la pile car linterruption peut avoir e te
provoquee par le fait quune mauvaise valeur a e te stockee dans le pointeur de pile. Le gestionnaire sauve donc ces valeurs dans deux emplacements memoire particuliers (save0 et save1).
Si la procedure de traitement de linterruption peut elle meme e tre interrompue, deux emplacements ne sont pas suffisants. Ici, elle ne peut pas e tre interrompue.
.ktext 0x80000080
sw $a0, save0 # Handler is not re-entrant and cannot use
sw $a1, save1 # stack to save $a0, $a1
Le gestionnaire dinterruption charge alors les registres Cause et EPC dans les registres du
CPU (ce sont des registres du co-processeur 0). Le gestionnaire na pas besoin de sauver $k0 et
$k1 car le programme utilisateur nest pas cense utiliser ces registres.
mfc0 $k0, $13

# Move Cause into $k0


125

mfc0 $k1 $14

# Move EPC into $k1

sgt $v0, $k0, 0x44


bgtz $v0, done

#ignore interrupt

mov $a0, $k0


mov $a1, $k1
jal print_excp

# Move Cause into $a0


# Move EPC into $a1
#print exception message

Avant de revenir a` la procedure originale, le gestionnaire restaure les registres $a0 et $a1, puis
il execute linstruction rfe (return from exception) qui restaure le masque dinterruption et les

bits kernel/user dans le registre Status. Le controleur


saute alors a` linstruction suivant
immediatement linstruction qui a provoque linterruption.
done:
lw $a0, save0
lw $a1, save1
addiu $k1, $k1, 4
rfe
jr $k1

save0:
save1:

.kdata
.word 0
.word 0

B.7 Input/Output
Il y a deux mani`eres dacceder a` des organes dentree/sortie (souris, e crans, etc.) : instructions
specialisees ou peripheriques places en memoire (memory mapped). Un peripherique est place
en memoire lorsquil est vu comme un emplacement special de la memoire, le syst`eme oriente
les acc`es a` ces adresses vers le peripherique. Le peripherique interpr`ete ces acc`es comme des
requ`etes. On presente ici un terminal (de type VT100) utilise par le Mips et implemente dans le
simulateur Spim. On ne detaille pas le mecanisme du clavier, uniquement celui du terminal.
Le terminal consiste en deux unites independantes : le transmetteur et le receveur. Le receveur
lit les caract`eres tapes au clavier, le transmetteur e crit les caract`eres sur le terminal. Les caract`eres
tapes au clavier ne sont donc affiches sur le terminal que si le transmetteur demande explicitement
de le faire.
le terminal avec quatre registres mappe en memoire : Le registre de
Un programme controle
controle du receveur est a` ladresse f f f f 0000hex . Seuls deux bits sont utilises dans ce registre, le
bit 0 est appele ready : si il est a` 1 cela veut dire quun caract`ere est arrive depuis le clavier mais
quil na pas e te encore lu par le registre de donnee du receveur. Ce bit ne peut pas e tre e crit par
le programme (les e critures sont ignorees). Le bit passe de 0 a` 1 lorsquun caract`ere est tape au
clavier et passe de 1 a` 0 lorsque le caract`ere est lu par le registre de donnee du receveur.
du receveur est lautorisation dinterruption du clavier (interLe bit 1 du registre de controle
rupt enable). ce bit peut e tre e crit ou lu par le programme. Lorsquil passe a` 1, le terminal l`eve une
interruption de niveau 0 d`es que le bit ready est a` 1. Pour que linterruption soit vue par le processeur, ce niveau dinterruption doit aussi e tre autorise dans le registre Status du co-processeur
0.
Le deuxi`eme registre du composant est le registre de donnee du receveur, il est a` ladresse f f f f 0004hex .
Les 8 bits de poids faible de ce registre contiennent le dernier caract`ere tape au clavier. Ce registre
ne peut quetre lu par le programme et change d`es quun nouveau caract`ere est tape au clavier.
du receveur est
Lorsque ce registre est lu par le programme, le bit ready du registre de controle
remis a` 0.
Le troisi`eme registre du terminal est le registre de controle du transmetteur (adresse f f f f 0008hex ).
Seuls les deux bits de poids faible de ce registre sont utilises. Il se comportent comme les bits du
du receveur. Le bit 0 est appele ready et est en lecture seule. Si il est a` 1, le
registre de controle
transmetteur est pret a` accepter un nouveau caract`ere sur lecran. Si il est a` 0, le transmetteur est
126

occupe a` e crire le caract`ere prec`edent. Le bit 1 est lautorisation dinterruption, lorsquil est a` 1 le
terminal l`eve une interruption d`es que le bit ready est a` 1.
Le dernier registre est le registre de donnee du transmetteur (adresse f f f f 000chex ). Quand une
valeur est e crite, les 8 bits de poids faible sont envoyes a` lecran. Lorsque le registre de donnee
du transmetteur est remis a` 0. Le
du transmetteur est e crit, le bit ready du registre de controle
bit reste a` 0 assez longtemps pour afficher le caract`ere a` lecran, puis le bit ready redevient 1.
Le registre de donnee du transmetteur ne peut e tre e crit que lorsque le bit ready du registre de
du transmetteur est a` 1. Si ce nest pas le cas lecriture est ignoree.
controle
Un vrai ordinateur demande un certain temps pour envoyer un caract`ere sur une ligne serie
qui connecte le terminal au processeur. Le simulateur Spim simule ce temps dexecution lors de
laffichage dun caract`ere sur la console.

127

F IG . 4 Interface X du simulation Spim : simulateur du jeu dinstruction du Mips

B.8 Quelques commandes utiles


Preprocesseur gcc -E ex1.c -o temp.c
Cette commande active le preprocesseur qui effectue nombre de taches simples a` partir du
fichier source. On a ainsi une idee de la complexite du processus genere lorsque lon e crit trois
ligne de C. La commande GNU utilisee ici par gcc est cpp.
Code assembleur gcc -S ex1.c -o ex1.s
Apr`es cette commande, ex1.s contient le code assembleur sous forme lisible (comme sur
celui du processeur de la machine sur lequel la
la figure 3). Lassembleur genere est bien sur
commande est executee.
Compilation+assembleur gcc -c ex1.c -o ex1.o
Produit directement le code objet a` partir du code source. Lassembleur utilise ici par gcc
est as (on peut donc executer de mani`ere e quivalente : gcc -S ex1.c -o ex1.s suivit de
as ex1.s -o ex1.o)
128


Edition
de lien gcc ex1.o -o ex1
Cette commade effectue ledition de lien. La commande GNU utilisee est ld . Elle est difficile
a` utiliser directement car il faut configurer diverses options, cest pourquoi on lutilise a` travers
gcc.
Compilation+assembleur+liens gcc ex1.c -o ex1
Compile, assemble et effectue ledition de lien pour produire un programme executable.
Symboles utilises nm ex1.o
Liste les symboles utilises dans le programme objet ex1.o. Cela est utile pour verifier les
fonctions utilisees ou definies dans une librairie. nm fonctionne aussi sur les codes executables.
Debogage gcc -g ex1.c -o ex1
Gen`ere un executable contenant les informations de debogage permettant dutiliser les debugger ( gdb, ddd etc.) et de suivre lexecution sur le code source.
Objdump Objdump permet de recuperer toutes les informations necessaires sur les fichiers objets ou binaires. Voici les options de cette commande. Les figures 5 et 6 montrent le resultat de
loption objdump -S.
Usage: objdump <option(s)> <file(s)>
Display information from object <file(s)>.
At least one of the following switches must be given:
-a, --archive-headers
Display archive header information
-f, --file-headers
Display the contents of the overall file header
-p, --private-headers
Display object format specific file header contents
-h, --[section-]headers Display the contents of the section headers
-x, --all-headers
Display the contents of all headers
-d, --disassemble
Display assembler contents of executable sections
-D, --disassemble-all
Display assembler contents of all sections
-S, --source
Intermix source code with disassembly
-s, --full-contents
Display the full contents of all sections requested
-g, --debugging
Display debug information in object file
-e, --debugging-tags
Display debug information using ctags style
-G, --stabs
Display (in raw form) any STABS info in the file
-t, --syms
Display the contents of the symbol table(s)
-T, --dynamic-syms
Display the contents of the dynamic symbol table
-r, --reloc
Display the relocation entries in the file
-R, --dynamic-reloc
Display the dynamic relocation entries in the file
-v, --version
Display this programs version number
-i, --info
List object formats and architectures supported
-H, --help
Display this information
man man 3 printf
Explique la fonction C printf et la biblioth`eque ou elle est definie. Indiquer la section de man
(section 3) est necessaire sinon on risque de tomber sur une commande dun shell.
compilation reciblable Le compilateur gcc (ainsi que toutes les commandes ci-dessus) peut
e tre recible pour un tr`es grand nombre darchitectures. Pour cela, il doit e tre recompile en indiquant larchitecture cible (target, voir le fichier config.sub dans les sources de la distribution de
gcc pour la liste des architectures possibles).

129

ex1.o:

file format pe-i386

Disassembly of section .text:


00000000 <.text>:
0:
54
1:
68 65 20 73 75
6:
6d
7:
20 66 72
a:
6f
b:
6d
c:
20 30
e:
20 2e
10:
2e 20 20
13:
20 20
15:
31 30
17:
30 20
19:
69 73 20 25 64 0a 00

push
push
insl
and
outsl
insl
and
and
and
and
xor
xor
imul

%esp
$0x75732065
(%dx),%es:(%edi)
%ah,0x72(%esi)
%ds:(%esi),(%dx)
(%dx),%es:(%edi)
%dh,(%eax)
%ch,(%esi)
%ah,%cs:(%eax)
%ah,(%eax)
%esi,(%eax)
%ah,(%eax)
$0xa6425,0x20(%ebx),%esi

00000020 <_main>:
20:
55
21:
89 e5
23:
83 ec 18
26:
83 e4 f0
29:
b8 00 00 00
2e:
89 45 f4
31:
8b 45 f4
34:
e8 00 00 00
39:
e8 00 00 00
3e:
c7 45 f8 00
45:
c7 45 fc 00
4c:
83 7d fc 64
50:
7e 02
52:
eb 15
54:
8b 45 fc
57:
89 c2
59:
0f af 55 fc
5d:
8d 45 f8
60:
01 10
62:
8d 45 fc
65:
ff 00
67:
eb e3
69:
8b 45 f8
6c:
89 44 24 04
70:
c7 04 24 00
77:
e8 00 00 00
7c:
c9
7d:
c3
7e:
90
7f:
90

push
mov
sub
and
mov
mov
mov
call
call
movl
movl
cmpl
jle
jmp
mov
mov
imul
lea
add
lea
incl
jmp
mov
mov
movl
call
leave
ret
nop
nop

%ebp
%esp,%ebp
$0x18,%esp
$0xfffffff0,%esp
$0x0,%eax
%eax,0xfffffff4(%ebp)
0xfffffff4(%ebp),%eax
39 <_main+0x19>
3e <_main+0x1e>
$0x0,0xfffffff8(%ebp)
$0x0,0xfffffffc(%ebp)
$0x64,0xfffffffc(%ebp)
54 <_main+0x34>
69 <_main+0x49>
0xfffffffc(%ebp),%eax
%eax,%edx
0xfffffffc(%ebp),%edx
0xfffffff8(%ebp),%eax
%edx,(%eax)
0xfffffffc(%ebp),%eax
(%eax)
4c <_main+0x2c>
0xfffffff8(%ebp),%eax
%eax,0x4(%esp)
$0x0,(%esp)
7c <_main+0x5c>

00

00
00
00 00 00
00 00 00

00 00 00
00

F IG . 5 Resultat de la commande gcc ex1.c -o ex1.o ; objdump -S ex1.o executee sur


un Pentium (ex1.c contient le programme de la figure 3 page 120). Le pentium est une architecture
Cisc : toutes les instructions nont pas la meme taille.

130

ex1MIPS.o:

file format elf32-littlemips

Disassembly of section .text:


00000000 <main>:
0:
27bdffe0
4:
afbf001c
8:
afbe0018
c:
03a0f021
10:
afc40020
14:
afc50024
18:
afc00014
1c:
afc00010
20:
8fc20010
24:
00000000
28:
28420065
2c:
14400003
30:
00000000
34:
0800001d
38:
00000000
3c:
8fc30010
40:
8fc20010
44:
00000000
48:
00620018
4c:
00001812
50:
8fc20014
54:
00000000
58:
00431021
5c:
afc20014
60:
8fc20010
64:
00000000
68:
24420001
6c:
08000008
70:
afc20010
74:
3c040000
78:
24840000
7c:
8fc50014
80:
0c000000
84:
00000000
88:
03c0e821
8c:
8fbf001c
90:
8fbe0018
94:
03e00008
98:
27bd0020

addiu
sp,sp,-32
sw ra,28(sp)
sw s8,24(sp)
move
s8,sp
sw a0,32(s8)
sw a1,36(s8)
sw zero,20(s8)
sw zero,16(s8)
lw v0,16(s8)
nop
slti
v0,v0,101
bnez
v0,3c <main+0x3c>
nop
j
74 <main+0x74>
nop
lw v1,16(s8)
lw v0,16(s8)
nop
mult
v1,v0
mflo
v1
lw v0,20(s8)
nop
addu
v0,v0,v1
sw v0,20(s8)
lw v0,16(s8)
nop
addiu
v0,v0,1
j
20 <main+0x20>
sw v0,16(s8)
lui a0,0x0
addiu
a0,a0,0
lw a1,20(s8)
jal 0 <main>
nop
move
sp,s8
lw ra,28(sp)
lw s8,24(sp)
jr ra
addiu
sp,sp,32

F IG . 6 Resultat de la commande gcc ex1.c -o ex1.o ; objdump -S ex1.o recible pour


un MIPS (ex1.c contient le programme de la figure 3 page 120).

131

Vous aimerez peut-être aussi