Analyse des durées de vie
Interférences.
Postscript, Luc Maranget Le poly

Place dans la chaîne de compilation


Rappel le code assembleur est presque de l'assembleur : il contient des temporaires.

Le but de l'analyse des durées de vie est de déterminer l'usage des temporaires afin de les remplacer par des registres machine.

Ce type d'analyse se retrouve sous d'autres formes (analyses de flot) pour faire des optimisations poussées (ou agressives)

Le problème
Chaque instruction, Les deux ensembles ne sont pas nécessairement disjoints.

Un temporaire est vivant en en point d'un programme si son contenu en cet endroit est utilisé dans une instruction suivante (au sens large).

Ou encore, si le temporaire est lu par la suite, avant d'être écrit.

La durée de vie d'un temporaire est l'ensemble des points du programme où il est vivant.


Application Deux temporaires qui ont des durées de vie disjointes peuvent être superposés, c'est à dire représenter le même registre.

Exemple
C1
t1 ← 1
  { t1 }
t2 ← 2
  { t1, t2 }
t2t1 + t2
live1live2         
C2
t1 ← 1
  { t1 }
t2t1+2
  { t2 }
t1 ← 3
  { t1, t2 }
t2t2 + t1
live3live4live5


Remarquer t1 n'est pas vivant en sortie de la deuxième instruction de C2.

Définitions
Pour chaque instruction i, on définit : Use (i), Def (i) et Succ (i) sont mis en valeur par notre représentation des instructions de l'assembleur :
type instr =
  | Oper 
of
 string * temp list * temp list * label list option
(* Oper (mnémonique, sources, destinations, sauts) *)
  ...

Définitions (suite)
Les informations de liveness : Selon la définitions informelle « temporaires lus avant d'être écrits ». Les Out (i) :


t |
|
|
i1Succ (i), …, inSucc (in-1),

tUse (in)
k ∈ [1, n-1], tDef (ik)


Et de même, les In (i) :


t |
|
|
i1=i, i2Succ (i1), …, inSucc (in-1),

tUse (in)
k ∈ [1, n-1], tDef (ik)


(Même chose décalé d'un cran vers le haut.)


Note Il s'agit d'une approximation, pourquoi ?

Équations corollaires
In (i) = Use (i) ∪ (Out (i) \ Def (i))          Out  (i) =
 
jSucc (i)
In (j)
Ces deux égalités permettent de calculer Out  et In  par itération. Pour changer, nous allons le prouver.

Pour le moment, dans le cas d'un code en séquence, ces équations permettent de calculer les durées de vie en remontant le sens de l'exécution. Si Succ (i1) = { i2 }, alors
Out (i1) = In (i2) = (Out (i2) \ Def (i2)) ∪ Use (i2)

Exemple, code en séquence
Le calcul procède à l'inverse de l'ordre d'exécution.
C1
  T\ {t1, t2}
t1 ← 1
  (T \ {t2}) ∪ {t1}
t2 ← 2
  (T \ {t2} ∪ { t1, t2 }
t2t1 + t2
  T
        
C2
  T\ {t1, t2}
t1 ← 1
  (T \ {t1, t2}) ∪ {t1}
t2t1+2
  (T \ {t1, t2}) ∪ {t2}
t1 ← 3
  (T \ {t2}) ∪ {t1, t2}
t2t2 + t1
  T

Avec des sauts
On utilise donc le graphe de flot. Il met en valeur :
Dans l'exemple
Exemple, avec des sauts
    li  e, 1
    ble ne, L12
L13:
    li  r, 1
    b L16
L15:
    mul  rrn
    sub  nn, 1
L16:
    bgt n$zero, L15
L17:
    move fr
    b fact_end
L12:
    li  f, 1
fact_end:
    move $v0f
    

Calcul des durées de vie
On définit précisément les séquences de k instructions qui peuvent suivre une instruction i.
Succ0 (i) = { [ ] }          Succk+1  (i) =
 
i1Succ (i)



 
ckSucck (i1))
{ [i1 ; ck] }


On restreint les définitions de Out  et In  en Outn  et Inn 
Outn  (i) =
n
k=0



 
[ck-1 ; ik] ∈ Succk (i)


t |
|
|


tUse (ik)
k' ∈ [1, k-1], tDef (ik')





Calcul des durées de vie, suite
Inn  (i) =
n
k=0



 
ck-1Succk-1 (i)


t |
|
|


tUse (jk)
k' ∈ [1, k-1], tDef (jk')





(avec [j1 ; … ; jk] = [i ; ck-1]).

Ces définitions entraînent :
Calcul des durées de vie, méthode
Les suites Outn  et Inn  ont une formulation par récurrence :
Out0 (i) = Ø          In0 (i) = Ø         Inn+1 (i) = Use (i) ∪ (Outn (i) \ Def (i))          Outn  (i) =
 
jSucc (i)
Inn (j)
Or ces suites sont croissantes et bornées (nombre fini de temporaires) : elles sont donc stationnaires à partir d'un certain rang, leurs limites sont Out  et In . Et on a évidemment :
In (i) = Use (i) ∪ (Out (i) \ Def (i))          Out  (i) =
 
jSucc (i)
In (j)

Durée de vie, définition par point fixe
On définit souvent Out  et In  plus directement.

La suite Out  est le plus petit point fixe de la fonction monotone :
O(i)
 
jSucc (i)
Fj(O(j))
Avec Fi(X) = Use (i) ∪ (X \ Def (i)).

Ou plus synthétiquement, Out  est le plus petit point fixe de la fonction FOut  monotone.
FOut (X) = { i
 
jSucc (i)
Fj(X(j))}

Le Out  du transparent précédent (limite de la suite Outk ) est bien le plus petit des points fixes : { i ↦ Ø} ⊆ T et :
Outk+1 = FOut (Outk ) ⊆ FOut (T) = T

Calcul des Outn 
À priori :
for i=1 to n  do Out (i) <- Ø done ; O(n)
do
  for
 i=1 to n do Out' (i) <- Out (i)  done ; O(n)
  for i=1 to n do
    Out (i) <- (∪jSucc (i) (Out' (j)\Def (j)) ∪ Use (j)) |Succ (i)| × O(n)
  done × nO(n2)
until ∀  i ∈ [1,n], Out (i) = Out '(i) × O(n2) → O(n4)
Coût ? Au plus n2 itération (croissance) cout1cout2cout3cout4


En pratique, le coût n'est pas si elevé
Une borne plus stricte
Reprenons la définition des Outk (i) :
k
l=0



 
[cl-1 ; il] ∈ Succl (i)


t |
|
|


tUse (il)
k' ∈ [1, l-1], tDef (ik')




On considère un code de n instructions au total et on suppose kl > n. La séquence [i ; i1 ; … ; il] comprend alors des sous-sequences (strictes) de la forme [i' ; … ; i']. En les remplaçant par la séquence [i'], on obtient une nouvelle séquence de taille l'+1 ≤ n+1 dont toutes les instructions sont deux à deux distinctes, sauf éventuellement la première et la dernière. Cette séquence prouve tOutl' (i).

Autrement dit, on a prouvé : kn Out (k) ⊆ Outn (i). Soit encore Outn (i) = Outn+1 (i). C'est à dire qu'il y a au plus n+1 itérations de la boucle externe. Le coût est donc en O(n3).

Une envie logique
Calculer les Out k en place dans un tableau.
for i=1 to n do Speed (n) <- Ø done ;
let encore = ref true in
while
 !encore do
  encore := false ;
  for i=1 to n do
    let
 prev = Speed (iin
    Speed (i) <- (∪jSucc (i) (Speed (j)\Def (j))∪ Use (j)) ;
    encore := !encore || (prev <> 
Speed
 (i))
  done
done

Justification de l'envie logique


Intuition On remplace certains Outn (j) par Outn+1 (j) dans la formule
Outn+1  (i) = (
 
jSucc (i)
Use (j) ∪ (Outn (j) \ Def (j)))


Plus précisément
Speed0 (i) = Ø          Speedk+1 (i) =


 
jSucc (i),  j < i
Fj(Speedk+1 (j))





 
jSucc (i),  j > i
Fj(Speedk (j))



De façon synthétique on a :
Speed0 = { i ↦ Ø},         Speedk+1 = FSpeed (Speedk )

Suite de la justification de l'envie logique
Definissons Speed  comme la limite de Speedk  (FSpeed  monotone).

Par monotonie des Fj on a :
Outk ⊆ Speedk 
C'est à dire que le calcul des Speedk  est « plus efficace ».

On a donc aussi :
Out ⊆ Speed 
Si on a peur du « passage à la limite », il suffit de considérer un k0 pour lequel les limites sont atteintes.

Par ailleurs FSpeed (Out ) = Out  et { i ↦ Ø } ⊆ Out , on a donc :
Speedk ⊆ Out 

Calcul en pratique, cas du code en séquence
Il faut ordonner les instructions à l'envers de l'ordre d'exécution :
C Out0  Out1  Out2  Out4  Out5 
t1 ← 1 Ø Ø Ø { t1 } { t1 }
t2 ← 2 Ø Ø { t1, t2 } { t1, t2 } { t1, t2 }
t2t1 + t2 Ø { t1, t2 } { t1, t2 } { t1, t2 } { t1, t2 }
t3t2 * t1 Ø Ø Ø Ø Ø
        
i C Speed0  Speed1  Speed2 
4: t1 ← 1 Ø { t1 } { t1 }
3: t2 ← 2 Ø { t1, t2 } { t1, t2 }
2: t2t1 + t2 Ø { t1, t2 } { t1, t2 }
1: t3t2 * t1 Ø Ø Ø

Cas général
Éviter le plus possible jSucc i et ji.
Ici on a Succ (8) = {7, 12}.

Calcul
i Speed0  Speed1  Speed2  In 
1 Ø Ø Ø { f }
2 Ø {f} {f} { f }
3 Ø {f} {f} Ø
4 Ø Ø Ø Ø
5 Ø {f} {f} {f}
6 Ø {f} {f} { r }
7 Ø {r} {r} { r }
8 Ø { r } { n, r } { n, r }
              
i Speed0  Speed1  Speed2  In 
9 Ø { n, r } { n, r } { n, r }
10 Ø { n, r } { n, r } { n, r }
11 Ø { n, r } { n, r } { n, r }
12 Ø { n, r } { n, r } { n, r }
13 Ø { n, r } { n, r } { n, r }
14 Ø { n, r } { n, r } { n }
15 Ø { n } { n } { n }
16 Ø { n } { n } { e, n }
17 Ø { e, n } { e, n } { n }
Speed1 (8) = { r } ∪ Ø mais Speed2 (8) = { r } ∪ { r, n } (car Speed1 12 = {r, n}).

Pour aller encore plus vite
On passe au graphe des blocs de base. En effet il est inutile d'itérer à l'intérieur des blocs de base. Ils se comportent comme de grosses instructions :
Def  (b) =
n
k=1
Def (ik),          Use (b) =


n
k=1
Use (ik) \


k-1
j=1
Def (ij)





Le graphe de flot des blocs de base a moins de noeuds que celui des instructions. On divise le nombre de calculs à itérer par la longueur moyenne des blocs.

Graphe de flot des blocs de base

Graphe de flot des blocs de base
Ensuite il faut calculer les Out , instruction par instruction dans les blocs et à partir des Out  en fin de bloc.

Interférence
Par définition deux temporaires interfèrent si ils ne peuvent pas partager le même registre. La relation d'interférence est non-réflexive, symétrique et non-nécessairement transitive.

Dans le cas du MIPS, il suffit de parcourir les instructions (ie. le graphe de flot). La seconde règle est une finesse, quel est son intérêt ?

Graphe d'interférence
La relation d'interférence est représentée par un graphe (non-orienté) Ce graphe se calcule facilement comme décrit au transparent précédent.

Exemple du code en séquence
i C Def  Out 
4 t1 ← 1 t1 t1
3 t2 ← 2 t2 t1, t2
2 t2t1 + t2 t2 t1, t2
1 t3t2 * t1 t3  
t1 et t2 interfèrent, en raison par exemple de Def (2) = {t2} et Out (2) = {t1, t2}.

Exemple du code arbitraire
          
i move Def  Out 
1 v0f v0 v0
2     f
3 f ← 1 f f
4      
5     f
6 fr f f
7     r
8     n, r
          
i move Def  Out 
9     n, r
10   n n, r
11   r n, r
12     n, r
13     n, r
14 r ← 1 r n, r
15     n
16     n
17 e ← 1 e n
          
Allocation : e, r et f dans r1, n dans r2.
Les arcs move indiquent de choisir r1 = v0.
Exemple dans le style des TP
Source :
function fact ( n : integer) : integer;
begin
  if
 n <= 1 then
    fact := 1
  else
    fact := n * fact (n-1)
end;
Liveness
fact:                      #  ⇐            # $a0 $s0 $ra
    subu $sp$sp, fact_f  #  ⇐            # $a0 $s0 $ra
    move $112, $ra         # $112$ra   # $a0 $s0 $112
    move $113, $s0         # $113$s0   # $a0 $112 $113
    move $108, $a0         # $108$a0   # $108 $112 $113
    li   $114, 1           $114 ⇐        # $108 $112 $113 $114
    ble  $108, $114, L12   #  ⇐ $108 $114 # $108 $112 $113
L13:                       #  ⇐             # $108 $112 $113
    sub  $a0, $108, 1      $a0$108    # $a0 $108 $112 $113
    jal  fact              $v0 $a0 $ra$a0  # $v0 $108 $112 $113
    move $109, $v0         # $109$v0    # $108 $109 $112 $113
    mul  $107, $108, $109  $107$108 $109 # $107 $112 $113
    b    fact_end          #  ⇐             # $107 $112 $113
L12:                       #  ⇐             # $112 $113
    li   $107, 1           $107 ⇐        # $107 $112 $113
fact_end:                  #  ⇐             # $107 $112 $113
    move $v0, $107         $v0$107    # $v0 $112 $113
    move $ra, $112         $ra$112   # $v0 $113 $115
    move $s0, $113         $s0$113    # $v0 $s0 $115
    addu $sp$sp, fact_f  #  ⇐             # $v0 $s0 $115
    j    $ra               #  ⇐ $v0 $s0 $ra  # 

Interférence
Realisation des graphes orientés
(* Types de sommets et des graphes *)
type 'a node
and 'a t

val
 create : 'a -> 'a t
(* Créer un nouveau graphe, initialement vide *)

val new_node : 'a t -> 'a -> 'a node
(* ajoute un noeud  par effet de bord *)

val new_edge : 'a t -> 'a node -> 'a node -> unit
(* ajoute un arc de n1 vers n2, (si il n'existe pas déjà) *)

val nodes : 'a t -> 'a node list
(* Tous les noeuds, dans l'ordre de leur création *)

val info : 'a t -> 'a node -> 'a
(* Le contenu d'un noeud *)

val succ : 'a t -> 'a node -> 'a node list
(* Les successeurs d'un noeud *)

val iter : 'a t -> ('a node -> unit) -> unit
(* itère un fonction sur les noeuds (ordre de création) *)

val
 debug : out_channel -> (out_channel -> 'a node -> unit) -> 'a t -> unit
(* Affichage pour le debug *)
Les graphes de flot
type flowinfo = {
    instr : Ass.instr ;              (* instruction *)
    def : temp set;  use : temp set; (* détruits et lus *)
    mutable live_in : temp set;      (* sans commentaire *)
    mutable live_out : temp set;
  }

type flowgraph = flowinfo Graph.t
(* Type des graphes de flots décorés des live-in/live-out *)

val flow : Ass.instr list -> flowgraph

(* Fabrication du graphe de flot, décoré par les durées de vie *)

Structure du calcul des graphe de flot
En deux temps : création du graphe, puis calcul des durées de vie.
let flow code =
  let g = mk_graph code in
  fixpoint g ;
  g

Les graphes non-orientés
type ('a, 'b) t
type 'a node
val create : 'a -> ('a, 'b) t
val new_node : ('a, 'b) t -> 'a -> 'a node

val new_edge : ('a, 'b) t -> 'a node -> 'a node -> 'b ->  unit

(*
   « new_edge g n1 n2 l » , ajoute un arc étiqueté par l entre n1 et n2.
   L'arc n'est pas crée lorsque :
    - Il existe déjà,
    - ou n1=n2
*)
Les graphes d'interférence
(* Sortes d'arcs du graphe d'interférence *)
type ilab = Inter | Move_related

(* Contenu des noeud du graphe d'interférence *)
type interference = {
    temp : temp;                  (* un temporaire *)
(* Les champs suivants sont utiles pour l'allocation de registres *)

    mutable color : temp option ;
    ...
  }

type igraph = (interference, ilab) Sgraph.t

val interference :  flowgraph  -> igraph

Ce document a été traduit de LATEX par HEVEA.