Académique Documents
Professionnel Documents
Culture Documents
Tanguy Risset
2 mars 2005
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
4
4
5
6
6
7
7
8
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
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
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
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
51
.
.
.
.
.
.
57
57
57
59
61
62
63
65
65
67
70
71
77
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
89
89
91
93
95
95
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.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]
[Muc]
[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.
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
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
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 demander
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
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.
Expressions
Rgulires
Automates
finis
dterministes
Minimization
Construction
de Thompson
Automates
finis
nondterministes
Determinisation
11
s0
S1
s2
s3
s4
S1
b
s2
s3
s7
s6
c
s4
s5
b
s2
s3
S6
s6
s8
c
s4
s9
s5
b
s2
a
s0
s1
s3
s8
S6
s6
c
s4
s5
s9
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
a
n4
a
n0
n1
n5
n2
n8
n3
a
n6
n7
n9
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.
s0
s1
15
a
s1
s1
s1
autres
s1 :
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.
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.
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
Expr
Op
Expr Op nombre
nombre
+
17
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
|
|
|
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.).
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
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
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
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
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.
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
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) =
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
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.
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
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.
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
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.
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
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
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.
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)
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
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
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)
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)
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
/* 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.
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
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)
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
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
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
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
static
Tas
Memoire libre
Pile
0
(petites adresses)
100000
(grandes adresses)
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
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
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
(x < y)
then statement1
else statement2
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
ra , rb
cc1 , rtrue , rf alse
rc , rd
cc2 , rtrue , rf alse
r1 , r 2
cc1
r1
cc2
r2
rx
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
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
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
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
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.
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
Taille
Premier elt
ValueNode
ConsNode
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
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
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.
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
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
69
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).
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
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
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
devient
devient
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),
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 ) =
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 ) =
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 ) =
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
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
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
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
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
Constant folding
Algebraic simplifications
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
81
un tel DAG, chaque noeud ayant plusieurs parents doit representer une expression redondante.
Par exemple :
InstrList
InstrList
InstrList
m
*
z
*
2
82
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.
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
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
(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
(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
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
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) =
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)
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.
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) =
Constant(b) =
ppreds(b)
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
({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.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
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
Branchement redondant
B1
B1
B1
B2
B2
B1
B2
Agregation de blocs
B2
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
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
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,
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
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
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
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
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
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
107
entre dans la trace. Lordonnancement des blocs de base e tendus est en fait un cas
controle
degenere du trace scheduling.
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
@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
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
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.
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)
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
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.
114
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
118
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
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.
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
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.
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
#ignore interrupt
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
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
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:
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
130
ex1MIPS.o:
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
131