Majeure 2
Langages et Compilation
Cours 6


Linéarisation du code
Pile, tas
Code 3 adresses

jean-jacques.levy@inria.fr

9 mars 1998

Les supports de cours sont en

http://www.polytechnique.fr/poly/edu/informatique/
http://www.polytechnique.fr/poly/~levy/
http://http.cs.berkeley.edu/~aiken/cs164temp/
http://www.cs.princeton.edu/~appel/modern/ml/

Livres:

Introduction

Linéarisation du code

Dans une machine, en général $\mbox{CJUMP}$ ne saute qu'à une adresse, l'adresse par défaut est l'instruction suivante.

Les noeuds $\mbox{ESEQ}$ et $\mbox{CALL}$ créent des effets de bords dans le calcul des expressions. On va les sortir des expressions. Idem pour un $\mbox{CALL}$ dans un autre $\mbox{CALL}$.

Le résultat de ces transformations nous donnera une liste d'instructions suivie d'une expression pour chaque fonction.

Montée des noeuds $\mbox{ESEQ}$

On applique les réécritures suivantes:

$\mbox{ESEQ}(s_1,\mbox{ESEQ}(s_2,e)) \rightarrow\mbox{ESEQ}(\mbox{SEQ}(s_1,s_2),e)$

$\mbox{BINOP}(op, \mbox{ESEQ}(s,e_1),e_2) \rightarrow\mbox{ESEQ}(s, \mbox{BINOP}(op,e_1,e_2))$
$\mbox{MEM}(\mbox{ESEQ}(s,e_1)) \rightarrow\mbox{ESEQ}(s,\mbox{MEM}(e_1))$
$\mbox{JUMP}(\mbox{ESEQ}(s,e1),t) \rightarrow\mbox{SEQ}(s, \mbox{JUMP}(e1,t))$
$\mbox{CJUMP}(op,\mbox{ESEQ}(s,e1),e2,l1,l2) \rightarrow$
$\mbox{SEQ}(s, \mbox{CJUMP}(op,e1,l1,l2))$

Si s et e1 commutent,

$\mbox{BINOP}(op,e_1,\mbox{ESEQ}(s,e_2)) \rightarrow\mbox{ESEQ}(s,\mbox{BINOP}(op, e_1, e_2))$
$\mbox{CJUMP}(op,\mbox{ESEQ}(s,e1),e2,l1,l2) \rightarrow$
$\mbox{SEQ}(s, \mbox{CJUMP}(op,e_1,e_2,l1,l2))$

Montée des noeuds $\mbox{ESEQ}$ - bis


Si s et e1 ne commutent pas, on prent t nouveau et:

$\mbox{BINOP}(op,e_1,\mbox{ESEQ}(s,e_2)) \rightarrow$
$\mbox{ESEQ}(\mbox{MOVE}(\mbox{TEMP}t, e_1),$
$\mbox{ESEQ}(s,\mbox{BINOP}(op,\mbox{TEMP}t, e_2)))$
$\mbox{CJUMP}(op,\mbox{ESEQ}(s,e1),e2,l1,l2) \rightarrow$
$\mbox{SEQ}(\mbox{MOVE}(\mbox{TEMP}t, e_1),$
$\mbox{SEQ}(s, \mbox{CJUMP}(op,\mbox{TEMP}t,l1,l2))$

De même, on remonte les $\mbox{CALL}$ en se servant de nouveaux temporaires si nécessaire.

Dorénavant, on n'a plus que des formes canoniques d'expressions d'IC qui n'ont plus de $\mbox{ESEQ}$ ou $\mbox{CALL}$ à l'intérieur de leurs expressions.

Traces et blocs de base

Les expressions sont donc des listes d'instructions suivies d'une expression ne contenant plus d'instruction de séquencement. Les instructions sont aussi des suites d'instructions.

Un programme est une liste d'association entre étiquettes et instructions ou expressions.

Un bloc de base (basic block) est toute suite d'instructions telle que:

- la première instruction est $\mbox{LABEL}$
- la dernière est $\mbox{CJUMP}$ ou $\mbox{JUMP}$
- il n'y a pas d'autres $\mbox{LABEL}$, $\mbox{CJUMP}$ ou $\mbox{JUMP}$

On ne peut qu'entrer par le début et sortir par la fin d'un bloc de base.

Traces

On peut donc diviser tout le code des fonctions en suite de blocs de base. On part du haut et on s'arrête dès qu'on voit un $\mbox{LABEL}$ ou un saut (conditionnel ou pas). Si on se retrouve avec un bloc ne commençant pas par une étiquette, on en rajoute une. Si on se trouve avec un bloc qui ne finit pas par un saut, on rajoute un saut à l'instruction suivante.

Une trace est une suite de blocs de base qui peuvent s'exécuter en séquence. En partant de la première instruction d'une fonction, on peut construire une trace en essayant de mettre en séquence les $\mbox{JUMP}$et leurs étiquettes correspondantes (on pourra ainsi les détruire). Si certains blocs de base ne sont pas couverts, on recommence une trace à partir d'eux.

Sauts conditionnels

Les traces permettent d'implanter le code linéairement en mémoire. Pour les sauts conditionnels, pour beaucoup d'entre eux, la trace aura mis une des 2 étiquettes après le saut. On peut donc changer le sens de la condition pour que ce soit le cas false.

Si ce n'est pas le cas, on peut toujours introduire une nouvelle étiquette intermédiare et faire un saut dessus.

$\mbox{CJUMP}(op, a, b, l_t, l'_f)$
$\mbox{LABEL}l'_f$
$\mbox{JUMP}(\mbox{NAME}l_f)$

Pile


Les blocs sont empilés à l'entrée d'une instruction let ou quand on passe les arguments à une fonction. (Dans la réalité, on limite la création des blocs; par exemple les arguments sont souvents passés dans un bloc semi-ouvert).


\begin{figure}
\expandafter\ifx\csname graph\endcsname\relax \csname newbox\endc...
 ...t 0pt}\kern 3.362in
 }}{\center\mbox{}{\box\graph}\mbox{}\endcenter}\end{figure}

Registres globaux

Toutes les machines modernes se servent de registres globaux (ie dont la durée de vie dépasse le cadre d'une fonction) pour stocker les valeurs suivantes:

sp sommet de pile
fp bloc d'activation courant
rv valeur de retour (d'une procédure)

Le RISC Mips R2000

Les machines Reduced Instruction Set Computer partent du principe que le temps d'une tâche est le produit $C \times T \times I$où:

C = nombre de cycles par instruction,
T = temps par cycle (vitesse de l'horloge),
I = nombre d'intructions dans la tâche.

Le temps mis par une intruction dépend donc de C et T. Dans un Risc, on essaie de faire C=1. Pour y arriver, on utilise:

le pipeline des instructions
architecture simple des load et store
load retardés
branchements retardés .

Le RISC Mips R2000 - bis Les seules opérations ALU n'ont que 2 formes:

    rd = rs op rt
    rd = rs op CONSTANTE(16 bits)

rs, rt, rd sont 32 registres. Toute instruction tient sur 32 bits; 6 bits sont réservés pour désigner l'opération (code-opération).

Les seules instructions load et store font communiquer la mémoire et les registres. (On a toujours r0 = 0).

Le RISC Mips R2000 - pipeline

.




















.

Le RISC Mips R2000 - bis

 # 238                 argc--, argv++;
    lw   $15, 72($sp)
    addu $24, $15, -1
    sw   $24, 72($sp)
    lw   $25, 76($sp)
    addu $8, $25, 4
    sw   $8, 76($sp)

 # 275                 if ((tptr = index(mytty,'y')) != 0)
    la   $4, mytty
    li   $5, 121
    jal  index
    sw   $2, tptr
    lw   $10, tptr
    beq  $10, 0, $44

en mémoire les instructions sont réordonnancées pour permettre les load, store et sauts retardés:

# 238                 argc--, argv++;
0x40020c <main+44>: lw $t7,72($sp)
0x400210 <main+48>: lw $t9,76($sp)
0x400214 <main+52>: addiu $t8,$t7,-1
0x400218 <main+56>: addiu $t0,$t9,4
0x40021c <main+60>: sw $t0,76($sp)
0x400220 <main+64>: sw $t8,72($sp)
0x400224 <main+68>: lw $t1,72($sp)

# 275                 if ((tptr = index(mytty,'y')) != 0)
0x40037c <main+412>: lui $a0,4096
0x400380 <main+416>: addiu $a0,$a0,32272
0x400384 <main+420>: jal 0x405bf0 <index>
0x400388 <main+424>: li $a1,121
0x40038c <main+428>: sw $v0,-30568($gp)
0x400390 <main+432>: lw $t4,-30568($gp)
0x400394 <main+436>: nop 
0x400398 <main+440>: beq $t4,$zero,0x4003b0 <main+464>

Le pico-RISC WL9311-pro

Le code WL9311-pro (Weis-Leroy 93, batiment 11, professionnel) est un code à 3 opérandes: destination, source, source2. La 2ème source peut aussi être une constante immédiate. On note o quand non précisé.

$d \leftarrow s \oplus o$

$\oplus$ est une opération arithmétique, de test logique ou de décalage. Le registre r0 contient toujours la valeur . Le nombre de registres est supposé infini pour l'instant.

Donc $d \leftarrow s + \texttt{r0}$ équivaut à faire $d \leftarrow s$.

Le tableau des instructions WL9311-pro

$\oplus(s,o,d)$ met dans d le résultat de l'opération entre o et s
$\texttt{load}(s,o,d)$ charge le registre d avec le mot à l'adresse s+o
$\texttt{store}(s,o,d)$ range le registre d dans le mot à l'adresse s+o
$\texttt{jmp}(o,r)$ saute à l'adresse o en mettant dans r l'adresse
  suivant l'instruction $\texttt{jmp}$. (Jump and Link du R2000)
$\texttt{braz}(r,a)$ saute à l'adresse a si r = 0,
$\texttt{branz}(r,a)$ saute à l'adresse a si $r \neq 0$,
$\texttt{scall}(n)$ appelle la fonction système n,
$\texttt{stop}$ arrête le programme.
L'interface code.mli du pico-RISC


type registre == int;;

type opérande =
     Reg of registre
   | Imm of int;;

type instruction =
     Op of opération * registre * opérande * registre
   | Jmp of opérande * registre
   | Braz of registre * int
   | Branz of registre * int
   | Scall of int
   | Stop

and opération =
    Load | Store | Add | Mult | Sub | Div
  | And | Or | Xor | Shl | Shr
  | Slt | Sle | Seq;;

value nombre_de_registres: int
  and sp: int
  and ra: int
  and taille_du_mot: int;;

Les instructions sous-forme arborescente

Add correspond aux arbres suivants d'IC:


\begin{figure}
\expandafter\ifx\csname graph\endcsname\relax \csname newbox\endc...
 ...t 0pt}\kern 2.015in
 }}{\center\mbox{}{\box\graph}\mbox{}\endcenter}\end{figure}

Les instructions sous-forme arborescente

load et store correspondent aux arbres suivants d'IC:


\begin{figure}
\expandafter\ifx\csname graph\endcsname\relax \csname newbox\endc...
 ...t 0pt}\kern 2.000in
 }}{\center\mbox{}{\box\graph}\mbox{}\endcenter}\end{figure}



\begin{figure}
\expandafter\ifx\csname graph\endcsname\relax \csname newbox\endc...
 ...t 0pt}\kern 2.769in
 }}{\center\mbox{}{\box\graph}\mbox{}\endcenter}\end{figure}

Sélection des instructions

On doit recouvrir toute expression de IC par des motifs correspondants aux instructions WL9311-pro. On part du sommet de l'expression en trouvant un motif correspondant à une instruction du pico-Risc, puis on descent récursivement dans les sous-arbres non explorés. Ensuite, on part des feuilles pour générer le code en remontant vers la racine.

Remarquons que la mise à plat des instructions par rapport aux expressions fait que les noeuds $\mbox{MEM}$ ne peuvent se trouver qu'au dessus des expressions.

Pour les sauts, on génère le code de l'expression logique, comme pour toute expression. Le saut proprement dit se fait par les instructions $\texttt{braz}$, $\texttt{branz}$.

Sélection des instructions - suite


Plusieurs recouvrements sont possibles pour une même expression. Certains donnent des résultats meilleurs. Si on veut être optimal, il faut une notion de coût attachée à chaque instruction et se lancer dans un peu de programmation dynamique pour tabuler les coûts intermédiaires et arriver au meilleur coût.

Plus important, la sélection des instructions peut prendre du temps, car il faut filtrer toute expression de IC par tous les motifs (correspondant à toutes les instructions machines). Si on sait factoriser le filtrage, on peut gagner beaucoup de temps de compilation (cf Huet-Lévy (79), O'Donnell-Hoffmann (80))

En pratique, prendre le plus gros motif à chaque fois est une bonne heuristique.

Création de blocs d'activation

On crée les nouveaux blocs au sommet de la pile en faisant:

MEM[sp] := fp
fp := sp
sp := sp +/- k


et on met les valeurs dans chacune des entrées du nouveau bloc. Le lien sur le premier mot du bloc permet de libérer le bloc quand on le quitte.

+/- est un décision relevant de l'architecture. Les piles croissent dans le sens décroissant des adresses mémoire (Mips, Vax), mais dans le sens croissant sur HP9000. Attention donc à ce phénomène, car il faut changer + en - dans tous les calculs d'adresse du code intermédiaire pour accéder aux objets dans la pile.

Début et fin des fonctions

A l'appel, on crée un bloc dans la pile, et on y met les arguments, et on fait $\texttt{jmp}(proc, \texttt{ra})$. Le registre ra contient l'adresse de retour.

Le code à l'entrée et à la sortie des fonctions dépend de la machine. A l'entrée, nous sauvons quelques registres, (ra pour l'instant). A la sortie, on restaure ra si nécessaire, on met la valeur dans le registre rv où on met les résultats et on fait un $\texttt{jmp}(ra, 0)$.

Annexes



3/8/1998