Le sujet proposé a pour but d’évaluer la quantité de travail réellement utile dans l’exécution des programmes. L’idée sous-jacente est que si une fraction importante de l’exécution d’un programme consiste en du travail inutile, il peut être intéressant de chercher un paradigme architectural permettant d’exploiter cette propriété.
GIÁO ÁN DẠY THÊM (KẾ HOẠCH BÀI DẠY BUỔI 2) - TIẾNG ANH 6, 7 GLOBAL SUCCESS (2...
Evaluation de la quantité de travail (in)utile dans l’exécution des programmes
1. Rapport de stage de DEA
´Evaluation de la quantit´e de travail utile
dans l’ex´ecution des programmes
Benjamin Vidal
Responsable de stage : Pierre Michaud
Projet CAPS
2. Sujet de stage
La recherche en architecture de processeur est confront´ee actuellement `a des contraintes qui
rendent de plus en plus difficile l’augmentation des performances des processeurs. Ces contraintes
sont multiples : consommation ´electrique, latence de propagation des signaux sur les connexions,
temps et coˆut de developpement, etc. . . Pour esp´erer trouver d’´eventuelles solutions permettant
d’augmenter les performances de mani`ere significative sur une large gamme d’applications, il faut
trouver de nouveaux paradigmes d’architecture. Pour cela, il faut d’abord avoir une bonne compr´e-hension
du comportement des programmes.
Le sujet propos´e a pour but d’´evaluer la quantit´e de travail r´eellement utile dans l’ex´ecution des
programmes. L’id´ee sous-jacente est que si une fraction importante de l’ex´ecution d’un programme
consiste en du travail inutile, il peut ˆetre int´eressant de chercher un paradigme architectural per-mettant
d’exploiter cette propri´et´e.
Le probl`eme consiste `a donner une d´efinition de l’utilit´e d’un travail. Par exemple, dans la
r´ef´erence [1], un r´esultat interm´ediaire est consid´er´e inutile s’il est ´ecrit dans un registre et est
´ecras´e sans avoir ´et´e utilis´e. Dans la r´ef´erence [5], un store `a une adresse m´emoire est consid´er´e
inutile s’il ´ecrit une valeur ´egale `a la valeur d´ej`a stock´ee `a cette adresse. Nous proposons d’´etudier
une autre d´efinition, selon laquelle une instruction dynamique est consid´er´ee utile si
– Elle produit un r´esultat ´emis en sortie du programme (ex. printf)
– Elle produit un r´esultat utilis´e comme op´erande d’une instruction utile
– C’est un branchement dominant une instruction utile
La partie recherche du stage consiste `a concevoir un algorithme efficace en temps et en m´emoire
permettant d’´evaluer la quantit´e d’instructions dynamiques utiles. La partie mise-en-oeuvre consiste
`a ´ecrire le programme correspondant, et `a l’utiliser pour obtenir des statistiques sur la fraction de
travail utile, et d’autres statistiques, `a d´efinir, permettant de mieux appr´ehender le comportement
des programmes. La mise en oeuvre se fera `a l’aide des outils d´evelopp´es au sein du projet CAPS.
On travaillera sur des traces d’ex´ecution des programmes de la suite SPEC CPU2000.
2
3. Remerciements
Au cours de ce stage au sein de l’´equipe CAPS de l’IRISA, il m’a ´et´e possible de rencontrer
un grand nombre de personnes qui m’ont aid´e `a comprendre le fonctionnement d’un laboratoire de
recherche en informatique et surtout `a acqu´erir le recul n´ecessaire pour mieux appr´ehender le monde
de l’architecture des microprocesseurs. Je voudrais donc remercier Ronan Amicel, Laurent Bertaux,
Fran¸cois Bodin, Henri-Pierre Charles, Assia Djabelkhir, Romain Dolbeau, Antony Fraboulet, Karine
Heydemann, Thierry Lafage, Antoine Monsifrot, Laurent Morin, Gilles Pokam, Olivier Rochecouste,
Andr´e Seznec et ´Eric Toullec.
Je tiens aussi `a remercier Yannos Sazeides (Enseignant `a l’universit´e de Chypre) avec qui j’ai eu
l’occasion d’´echanger des id´ees sur la fa¸con d’´elaborer automatiquement un graphe de d´ependance
de donn´ee `a partir de l’ex´ecution d’un programme.
Et enfin je tiens `a remercier tr`es chaleureusement mon maˆıtre de stage, Pierre Michaud, qui m’a
donn´e la libert´e de travail que j’aurais aim´e trouver tout au long de mon exp´erience universitaire
et professionnelle et m’a permis ainsi de suivre les pistes que je souhaitais. Je tiens ´egalement `a le
remercier pour tous les conseils qu’il a pu me donner concernant le monde de la recherche (publique
ou priv´ee) et de m’avoir fait partager sa vision des choses sur de nombreux sujets.
3
9. Chapitre 1
Bibliographie
1.1 Introduction
Aujourd’hui, pour am´eliorer les performances d’un programme, il ne suffit plus seulement d’ajou-ter
du mat´eriel dans un syst`eme donn´e. Il faut avant tout ´etudier le comportement de ce programme
afin d’adapter au mieux les ajouts qui doivent ˆetre faits au syst`eme. De ce constat, les architectes
des microprocesseurs ont tir´e des id´ees aujourd’hui fondamentales (tels les diff´erents niveaux de
m´emoires caches qui exploitent la propri´et´e de localit´e temporelle et spatiale d’acc`es aux donn´ees
dans les programmes).
En ce sens, certains travaux de recherche s’int´eressent aujourd’hui au probl`eme du travail ef-fectu
´e inutilement par un microprocesseur. Ils mettent en ´evidence une quantit´e non n´egligeable
de travail inutile. Dans cette bibliographie, trois approches principales de travail inutile ont ´et´e
retenues.
1. Une instruction produisant un r´esultat jamais utilis´e par une autre instruction est consid´er´ee
comme inutile (approche de « l’instruction morte » retenue par l’article [1]).
2. Une instruction d’´ecriture est consid´er´ee comme inutile si cette derni`ere ne modifie pas l’´etat
de la m´emoire (i.e. la mˆeme valeur est ´ecrite `a la mˆeme adresse m´emoire) (approche de
« l’´ecriture silencieuse » retenue dans de nombreux articles [5, 3, 7]).
3. Une instruction est consid´er´ee comme utile si elle produit un r´esultat en sortie (affichage d’un
r´esultat par exemple) ou qu’elle est elle mˆeme utile `a une instruction utile (approche retenue
pour le stage).
Apr`es un bref tour d’horizon des travaux d´ej`a effectu´es dans le domaine au niveau des compila-teurs,
chacun des trois aspects d´ecrits ci-dessus du travail inutile sera d´evelopp´e dans un paragraphe
de cette bibliographie. S’en suivra un paragraphe de discussion sur la possibilit´e de mˆeler ces deux
approches pour essayer d’obtenir une coop´eration compilation/ex´ecution dans l’´elimination des
instructions inutiles.
9
10. 1.2 Compilation et travail inutile
1.2.1 Vous avez dit « instructions inutiles » ?
Il peut paraˆıtre surprenant au premier abord d’entendre parler de travail inutile dans un pro-gramme.
En effet, `a partir du moment ou le programmeur demande d’effectuer un travail `a la
machine, (encore que celui-ci ne soit pas infaillible. . .) ce travail doit avoir une utilit´e (au sens
informatique du terme bien entendu. . .). Cependant, au del`a du programmeur, il existe toute une
chaˆıne de m´ecanismes permettant de passer du langage de haut niveau (i.e. langage de program-mation
classique) au code machine ex´ecutable. Ainsi ce programme va passer par toute sortes de
transformations qui vont introduire du travail inutile. De plus, il est possible de trouver, dans la
fa¸con dont sont con¸cus les programmes, du travail inutile (redondance de calculs par exemple).
1.2.2 Instructions statiques inutiles
Pour commencer, il est important de rappeler ce que sont les instructions statiques et les ins-tructions
dynamiques. Une instruction statique est une instruction telle qu’on peut la trouver dans
le code source d’un programme. Une instruction dynamique est une instance d’instruction statique.
A chaque instruction statique peut correspondre plusieurs instructions dynamiques (autant que de
fois o`u l’on ex´ecute cette instruction statique).
Exemple simple :
pour i de 1 à n faire
t[i] := 0;
fpour
Instruction statique : t[i] := 0
Instructions dynamiques associées : t[1] := 0, t[2] := 0, …, t[n] := 0
Dans l’exemple suivant, il est important de noter que si n n’est pas fix´e lors de la compilation, la
seule connaissance du compilateur est l’instruction statique. Il ne pourra donc pas, `a priori se servir
de la valeur de n `a des fins d’optimisation. Supposons maintenant qu’un programme ne soit compos´e
que des instructions de l’exemple et n’affiche aucun r´esultat. Le compilateur peut en d´eduire que
l’ensemble du travail `a effectuer pour ex´ecuter cette boucle est inutile. Cependant, il suffit d’ajouter
une instruction qui utilise t[m] en lecture (m ´etant un param`etre d’entr´ee du programme inconnu
`a la compilation) pour que, potentiellement, l’ensemble du travail de la boucle devienne utile. En
effet, le compilateur ne sachant pas quelle case du tableau t va ˆetre acc´ed´e, il est oblig´e de consid´erer
que l’ensemble de la boucle fournit du travail utile.
Il existe de nombreuses autres mani`eres d’´eliminer du travail inutile lors de la compilation [2, 4]
que nous n’aborderons pas ici car seul l’aspect d´ecrit ci-dessus se rapproche des travaux vis´es dans
cette ´etude.
Dans la suite de cette bibliographie, nous ne nous int´eresserons qu’aux instructions inutiles
dynamiques (i.e. qui ne peuvent pas ˆetre d´etect´ees par le compilateur puisqu’elles d´ependent de
valeurs d’entr´ees du programme non connues au moment de la compilation).
10
11. 1.3 Premi`ere approche :
Instructions inutiles d´etect´ees dynamiquement
1.3.1 Introduction
Les auteurs de l’article [1] se sont aper¸cus que le r´earrangement des instructions fait par les com-pilateurs
lors des phases d’optimisations cr´e´e des instructions inutiles. En effet, comme le montre
leurs r´esultats, une compilation faite sans optimisations montre un niveau faible d’instructions in-utiles
alors qu’une compilation avec un fort niveau d’optimisation montre un taux d’instructions
inutiles relativement ´elev´e (parfois sup´erieur `a 10 %). Cependant, malgr´e ce travail effectu´e inuti-lement,
il est bon de rappeler que globalement, le temps d’ex´ecution de ces programmes diminue
(i.e. on a bien l’effet d´esir´e). La question qui vient alors est :
« Comment conserver ces optimisations tout en r´eduisant le travail inutile qui leur est associ´e ? »
1.3.2 Description du principe de d´etection et d’´elimination des instructions
inutiles
Dans un premier temps, l’important est d’analyser les instructions ex´ecut´ees inutilement afin
de savoir comment les d´etecter. Les auteurs de l’article [1] se sont ainsi aper¸cus que les instructions
dynamiques inutiles ´etaient tr`es souvent des instances d’un nombre r´eduit d’instructions statiques.
Ces instructions statiques sont appel´ees des instructions partiellement inutiles. En marquant ces
instructions particuli`eres comme ´etant propices `a g´en´erer des instructions dynamiques inutiles,
il est possible de ne faire un traitement particulier que sur ces derni`eres afin de savoir si une
instance pr´ecise sera r´eellement inutile. Lors de l’ex´ecution, pour chaque instance d’une instruction
partiellement inutile, une estimation de l’utilit´e de cette instruction dynamique sera faite. De cette
estimation d´ecoulera son ex´ecution ou non. Dans le cas d’une mauvaise pr´ediction, un m´ecanisme
de r´ecup´eration permet de lancer l’ex´ecution de cette instruction au moment ou l’on apprend que
la pr´ediction est ´erron´ee.
1.3.3 Id´ees d’impl´ementation
Les auteurs de l’article [1] ont donn´e quelques id´ees d’impl´ementation qui pourraient ˆetre mises
en oeuvre pour la d´etection de ce type d’instructions. La plus simple consiste `a m´emoriser dans un
cache totalement associatif les instructions statiques ayant d´ej`a g´en´er´e des r´esultats inutiles par le
pass´e. Du fait qu’un faible nombre de ces instructions g´en`erent un grand nombre des instructions
dynamiques inutiles, ce cache permettra de « suspecter » la prochaine instance d’une instruction
statique ayant d´ej`a g´en´er´e des r´esultats inutiles.
Par la suite, lors de la d´etection d’une instruction dynamique « suspect´ee » d’ˆetre inutile, son
ex´ecution sera suspendue jusqu’au « verdict » final permettant de savoir si il ´etait juste de la sus-pecter.
Si tel est le cas, cette instruction ne sera pas ex´ecut´ee, dans le cas contraire, cette instruction
sera ex´ecut´ee ajoutant ainsi un surcoˆut dˆu au retard d’ex´ecution pris par cette instruction. Il est
11
12. donc tr`es important d’avoir une estimation la plus fine possible afin d’´eviter ce genre de cas et afin
d’augmenter le nombre d’instruction inutiles suspect´ees `a juste titre. Pour cela, des optimisations
sont propos´ees : utilisation de l’information de flot de contrˆole et ajout d’un compteur deux bits `a
saturation principalement.
Il est important de noter que ces impl´ementations ne tiennent pas compte des r´esultats calcul´es
qui ne servent qu’`a des instructions inutiles. Autrement dit, cette impl´ementation ne prend pas en
compte le caract`ere transitif que peuvent avoir certaines instructions inutiles.
Instruction
produisant un résultat R
Instruction
utilisant R et produisant R’
Instruction
utilisant R’ et produisant R’’
Si R’’ est un résultat inutile R’ et R auront été produits inutilement
Fig. 1.1 – Mise en ´evidence de l’inutilit´e des instructions ne produisant des r´esultats utilis´es que
par des instructions inutiles
1.3.4 Conclusion sur cette approche
En conclusion, nous pouvons dire que les auteurs de l’article [1] ont mis en ´evidence une quantit´e
non n´egligeable de travail inutile mˆeme si elle reste, aujourd’hui, difficile `a exploiter. En effet, dans
un environnement o`u les ressources sont peu limit´ees, l’efficacit´e de l’impl´ementation d´ecrite ci-dessus
offre des gains en performance n´egligeables. En revanche, dans des conditions de ressources
plus limit´ees, les gains peuvent atteindre 10 % d’am´elioration des performances. De plus le fait
d’ex´ecuter moins d’instructions permet une diminution de la charge des Unit´es Arithm´etiques et
Logiques (UAL) et de la consommation ´electrique relativement importante. D’apr`es les auteurs, un
m´ecanisme mat´eriel diminuant l’impact des instructions inutiles sur la performance et la consom-mation
´electrique permettrait d’appliquer des optimisations de code plus pouss´ees `a la compilation.
12
13. 1.4 Deuxi`eme approche :
´Ecritures silencieuses
1.4.1 Introduction
D’apr`es les auteurs de l’article [5], il existe principalement deux types d’´ecritures silencieuses.
D’une part les mises `a jours de valeurs silencieuses (qui ne changent pas l’´etat de la m´emoire dans
laquelle elles ´ecrivent) et d’autre part les ´ecritures silencieuses stochastiques qui mettent `a jour la
m´emoire de mani`ere pr´evisible. Dans la suite de ce chapitre, nous nous concentrerons sur les mises
`a jour de valeurs silencieuses et parlerons, par abus de langage, d’´ecritures silencieuses pour les
d´esigner.
1.4.2 Le ph´enom`ene d’´ecriture silencieuse
Au vu de la d´efinition de ce qu’est une ´ecriture silencieuse, il parait difficile de croire que ces
instructions puissent avoir un impact n´egatif important sur les performances d’un programme.
Pourtant, les articles sur le sujet montrent que souvent plus de 30 % des ´ecritures sont silencieuses
dans les applications test´ees. En effet, il existe de nombreux cas o`u, lors du parcours des ´el´ements
d’un tableau, les modifications apport´ees par ce parcours ne concernent qu’un petit nombre des
´el´ements de ce tableau.
Exemples simples :
pour i de 1 à n faire
b := (b & t[i]);
fpour
pour i de 1 à n faire
t[i] := (t[i] & b);
fpour
(a) (b) (c)
t étant un tableau de booléens
b étant un booléen
l'opération & étant un "ET" logique
pour i de 1 à n faire
t[i] := t[i] + e(i);
fpour
t étant un tableau d'entiers
e étant une fonction
Dans l’exemple (a), nous pouvons voir que pour chaque case du tableau t dont le bool´een est `a
vrai, l’ex´ecution du corps de la boucle ne produit aucun travail utile (la mˆeme valeur sera r´e-´ecrite
dans b). Dans l’exemple (b), le simple fait que b soit ´egal `a vrai entraˆıne une inutilit´e de l’ensemble
de la boucle. Dans l’exemple (c), lorsque "(i) renvoi z´ero, le corps de la boucle peut-ˆetre consid´er´e
comme inutile. Un autre cas assez fr´equent de travail inutile est celui o`u un tableau est initialis´e
apr`es avoir ´et´e utilis´e une premi`ere fois. Si lors de la premi`ere utilisation de ce tableau, toutes ses
valeurs n’ont pas ´et´e modifi´ees, il est inutile de r´e-initialiser l’ensemble des cases de ce tableau.
Il existe d’autres situations dans lesquelles un grand nombre d’´ecritures silencieuses peuvent ˆetre
observ´ees : lors de l’appel d’un sous-programme, si les registres sauvegard´es n’ont pas ´et´e utilis´es
dans ce sous-programme, leur restauration sera inutile. Ce mˆeme ph´enom`ene peut-ˆetre observ´e lors
de la sauvegarde/restauration de contexte d’un processus par un syst`eme d’exploitation.
13
14. 1.4.3 Les cons´equences de l’´elimination des ´ecritures silencieuses
Au del`a du gain ´evident que provoquerait un m´ecanisme fiable de suppression des ´ecritures
silencieuses, un tel syst`eme permettrait ´egalement de supprimer une certaine quantit´e de travail
assez importante li´ee `a ces instructions. En premier lieu, les informations de contrˆole li´ees `a ces
instructions ne sont plus n´ecessaires (Ex : si une s´erie d’´ecritures silencieuses se trouve dans une
boucle, il est inutile d’ex´ecuter la boucle). De plus, lors de l’ex´ecution d’une instruction de range-ment
en m´emoire, tout un m´ecanisme lourd de rapatriement de la ligne de cache concern´ee vers la
m´emoire est mis en place (´ecriture de la donn´ee dans le cache, marquage de la ligne de cache comme
´etant modifi´ee puis, lors du chargement de nouvelles donn´ees dans cette ligne de cache, ´ecriture
de l’ancienne ligne de cache consid´er´ee comme modifi´ee en m´emoire). De fait, la suppression d’une
´ecriture ´evite d’avoir `a passer par toute ces op´erations d’acc`es `a la m´emoire tr`es coˆuteuses. Comme
expliqu´e dans l’article [3], cette remarque prend encore plus d’importance dans un syst`eme multi-processeur
puisque `a chaque ´ecriture m´emoire est associ´e un message d’invalidation `a destination
des autres processeurs provoquant un d´efaut de cache lors du prochain acc`es `a ces donn´ees. . . Il
est ´egalement important de noter que si certaines ´ecritures ne sont pas effectu´ees, de fait, certaines
d´ependances de donn´ees n’existent plus. De cette fa¸con, le processeur n’est plus oblig´e d’attendre
que ces valeurs soient ´ecrites pour pouvoir les utiliser. Le rendement du pipeline du processeur est
alors am´elior´e.
1.4.4 Id´ees d’impl´ementation
Les auteurs de l’article [5] ont propos´e une impl´ementation basique permettant de supprimer
les ´ecritures silencieuses. Cette impl´ementation consiste `a remplacer toute les op´erations de ran-gement
en m´emoire par trois op´erations : Chargement de l’ancienne valeur pr´esente en m´emoire,
comparaison avec la valeur qui doit y ˆetre ´ecrite et enfin, dans le cas o`u ces deux valeurs ne seraient
pas ´egales, ´ecriture de la nouvelle valeur en m´emoire. Cette m´ethode est sˆure et permet de d´etecter
l’ensemble des ´ecritures silencieuses. De plus, les lectures pouvant ˆetre servies en parall`eles, il peut
ˆetre int´eressant de remplacer les ´ecritures par des lectures suivies de comparaisons. Cependant,
dans la mesure ou le nombre d’´ecritures silencieuses ne repr´esente pas la majorit´e des ´ecritures
m´emoire, les auteurs ont ajout´e une « impl´ementation parfaite » dans laquelle un m´ecanisme per-met
de savoir si une ´ecriture va ˆetre utile et, dans ce cas, n’effectuera que l’´ecriture en m´emoire
sans avoir `a comparer la nouvelle valeur `a la valeur pr´ec´edente.
D’autres id´ees d’impl´ementations apparaissent ´egalement dans l’article [5] comme par exemple
la possibilit´e que la ligne de cache ne soit pas marqu´ee comme modifi´ee lorsqu’elle re¸coit une ´ecriture
silencieuse ´evitant ainsi d’avoir `a propager l’´ecriture en m´emoire centrale (avantage principal de
l’´elimination des ´ecriture silencieuses).
L’impl´ementation retenue pour les simulations faites par les auteurs de [5] est la premi`ere
propos´ee avec pour caract´eristique suppl´ementaire que seules les ´ecritures mises en attente vont
subir une v´erification de leur utilit´e. Ce qui veut dire qu’une ´ecriture survenant `a un moment o`u au
moins un port d’´ecriture de la m´emoire est disponible sera servie avant que la v´erification de son
utilit´e n’ai pu ˆetre faite. De cette fa¸con, les performances des ´ecritures ne sont jamais d´egrad´ees
puisque le m´ecanisme n’agit que sur la file d’attente des ´ecritures afin de la r´eduire.
14
15. 1.4.5 Conclusion sur cette approche
Les auteurs de l’article [5] ont mis en ´evidence une grande quantit´e de travail inutile `a travers les
´ecritures silencieuses. En effet, les proportions d’´ecritures silencieuses obtenues lors des tests sont
parfois tr`es importantes et laissent penser qu’elles pourraient avoir une influence tr`es importante
sur les performances, notamment dans les syst`emes dont la purge des lignes de cache en m´emoire est
un goulet d’´etranglement. Les auteurs mettent ´egalement en avant la r´eduction du trafic sur le bus
d’un syst`eme multiprocesseur `a m´emoire partag´ee qui est souvent un point critique dans ce type de
syst`emes (ce trafic limite le nombre de processeurs sur un mˆeme bus). D’autres travaux ´elargissant le
th`eme de l’´ecriture silencieuse ont ´egalement ´et´e pr´esent´es comme celui sur les ´ecritures silencieuses
temporaires [6] consid´erant que si une valeur en m´emoire est modifi´ee puis remise `a son ancienne
valeur et qu’aucune lecture ne soit intervenue sur la valeur transitoire, elle peut-ˆetre consid´er´ee
comme silencieuse. Ce mod`ele semble bien s’adapter aux cas d´ecrits ci-dessus de sauvegarde et
de restauration de contexte fr´equents (appel de sous-programmes, passage d’un processus `a un
autre. . .).
15
16. 1.5 Troisi`eme approche :
Travail inutile global lors de l’ex´ecution d’un programme
1.5.1 Introduction
Dans cette approche, la probl´ematique est un peu diff´erente de celle vue dans les deux premiers
paragraphes. En effet, le but de ces deux approches ´etait de d´etecter (soit par pr´evision, soit de
mani`ere dynamique) une cat´egorie d’instructions inutiles afin d’´eviter leur ex´ecution « au vol ».
Dans l’approche retenue pour le stage, il s’agit d’abord de regarder quelle est la quantit´e de travail
inutile de fa¸con globale (essayer d’´evaluer l’ensemble du travail fait inutilement par un programme)
afin d’avoir ensuite une id´ee du type de comportement ou d’application ex´ecut´e par un processeur
qui produit le plus de travail inutile. De cette fa¸con, si certains r´esultats montrent une quantit´e non
n´egligeable de travail inutile dans certains types d’applications, il sera ensuite possible d’´etudier
pourquoi ce travail inutile est si important et si il peut ˆetre ´evit´e d’une mani`ere ou d’une autre.
1.5.2 Evaluer l’utilit´e d’une instruction ?
La m´ethode retenue ici pour ´evaluer l’utilit´e d’une instruction est assez simple : Une valeur qui
est affich´ee en sortie d’un programme est consid´er´ee comme un r´esultat utile. Toute instruction
ayant servi `a calculer ce r´esultat est une instruction utile. Ainsi, les instructions utiles `a un r´esultat
peuvent ˆetre repr´esent´ees par un arbre de d´ependance entre ces derni`eres dont la racine est le
r´esultat lui-mˆeme et chaque noeud repr´esente les instructions utiles au calcul de ce r´esultat (aussi
bien les instructions de rangement/r´ecup´eration en m´emoire, de calcul et de branchement). Les
feuilles seront alors les valeurs d’entr´ees (param`etres fix´es lors de la compilation ou de l’ex´ecution)
du programme. En regroupant l’ensemble des arbres ainsi obtenus pour chaque r´esultat en un graphe
orient´e dont les sources sont les r´esultats et les puits sont les valeurs d’entr´ee du programme, il
est possible d’identifier quelles sont les instructions r´eellement utiles au programme. En effet, les
instructions et les valeurs d’entr´ees inutiles au programme n’appartiendront pas `a ce graphe et
seront ainsi mises en ´evidence.
16
17. Exemple simple :
a := lire();
b := VRAI;
c := 0;
si a=0 faire
b := FAUX;
c := 5;
fsi
si b alors écrire(c)
sinon écrire(a)
Exemple de graphe d’exécution si a vaut 0 :
écrire(a)
nécessite b
b := FAUX
branchement
correspondant
Test a=0
nécessite a
nécessite a
a := lire()
Valeur d’entrée
du programme
a := lire()
Valeur d’entrée
du programme
c := 5
c := 0
b := VRAI
Instructions exécutées
inutilement
Exemple de graphe d’exécution si a vaut 1 :
nécessite b nécessite c
Test a=0
nécessite a
a := lire()
Valeur d’entrée
du programme
écrire(c)
c := 0
Valeur d’entrée
du programme
Aucune instruction n’est
exécutée inutilement
b := VRAI
Valeur d’entrée
du programme
Cet exemple permet de mettre en ´evidence le fait que selon les valeurs d’entr´ees du programme,
il peut y avoir du travail inutile ou pas. De plus, il met en lumi`ere (dans le cas o`u a vaut 1) le
fait que le test a=0 correspondant au branchement du « si » doit ˆetre pris en compte comme ´etant
du travail utile puisque de ce branchement vont d´ependre les instructions qui vont suivre (Nous
pouvons dire que ces instructions « exigent » l’ex´ecution de ce branchement et donc du test qui
permet de savoir si ce branchement doit ˆetre pris).
Cette m´ethode d’identification des instructions inutiles semble parfaite (mˆeme si elle ne prend
pas en compte certaines ´ecritures silencieuses). Cependant, elle n´ecessite le d´eroulement complet du
programme afin de savoir si oui ou non une instruction dynamique du programme sera utile pour
un r´esultat final. De fait, cette m´ethode ne peut pas ˆetre utilis´ee directement pour ´eliminer « au
vol » les instructions inutiles. En revanche, elle permet d’exhiber de nombreux cas d’instructions
inutiles que les autres m´ethodes ne d´etectent pas. Par exemple, le cas d’une instruction inutile par
transitivit´e mis en ´evidence figure 1.1 sera d´etect´e par cette m´ethode.
1.5.3 Mise en oeuvre
Comme d´ecrit dans le sujet de stage, la mise en oeuvre de cette approche du travail inutile consis-tera
`a ´elaborer un programme permettant de d´etecter les instructions dynamiques inutiles, d’apr`es
la d´efinition donn´ee ci-dessus, afin de faire des statistiques sur la quantit´e de travail inutile dans un
ensemble de programmes `a tester. Diff´erentes cat´egories de travail inutile pourront ´egalement ˆetre
mises en ´evidence (Ex : chargements inutiles, rangements en m´emoire inutiles, calculs inutiles. . .).
Une fois les tests effectu´es, un travail de regroupement des applications test´ees selon les r´esultats
pourra ˆetre fait afin de d´egager, ´eventuellement, des « motifs » de comportements permettant en-suite
de savoir quelles applications sont le plus concern´ees par quel type de travail inutile. Nous
pouvons imaginer, `a partir de l`a, que des ´ebauches de solutions mat´erielles et/ou logicielles ne soient
17
18. trouv´ees pour r´eduire cette quantit´e de travail inutile. Cependant, l’objet du stage reste celui-ci :
« Concevoir et ´ecrire un programme permettant de calculer la quantit´e de travail inutile dans un
programme particulier apr`es son ex´ecution » .
Dans ce sens, le travail `a effectuer en stage sera, dans un premier temps, de r´efl´echir `a la mani`ere
de d´etecter quelles sont les instructions qui ont ´et´e ex´ecut´ees inutilement lorsque l’ex´ecution d’un
programme sera termin´ee (algorithme de construction puis d’exploration du graphe de d´ependance
des instructions d´ecrit dans cette section). Ces r´esultats devront ensuite ˆetre mis en forme afin de
d´egager des statistiques sur la quantit´e de travail inutile (pourcentage d’instructions inutiles) et
sur la nature de ces instructions (de quel type d’instructions s’agit-il ?). Une fois cet algorithme
impl´ement´e, il sera int´eressant de le tester sur diff´erent type de programme afin de savoir quelle
est la quantit´e de travail r´eellement inutile (d’apr`es la d´efinition donn´ee en introduction de cette
section) dans ces programmes.
1.5.4 Conclusion sur cette approche
En conclusion nous pouvons dire que l’approche retenue pour le stage est une d´emarche scien-tifique
exp´erimentale permettant de savoir quelle est la proportion globale de travail inutile dans
un programme. Si les r´esultats r´ev`elent une grande quantit´e de travail inutile, de nombreuses
ouvertures paraissent possibles : D´etection de ces instructions grˆace `a des compilateurs « intel-ligents
», d´etection de ces instructions « au vol » (approche d´ej`a retenue par [1]), coop´eration
compilateur/mat´eriel ou encore ajout de nouvelles instructions afin de faciliter leur d´etection.
18
19. 1.6 Conclusion
En conclusion, nous pouvons dire que plusieurs mani`eres d’´eliminer le travail inutile ont d´ej`a ´et´e
abord´ees (tant dans le domaine de la compilation qu’en architecture). En effet, lors de la compila-tion,
une certaine quantit´e de travail inutile peut d´ej`a ˆetre supprim´ee (en fonction des informations
que le compilateur peut exploiter). Cependant, nous avons ´egalement vu que certaines optimisa-tions
de ces mˆemes compilateurs g´en`erent des instructions inutiles. De fait, diff´erentes m´ethodes ont
´et´e propos´ees pour ´eliminer ce travail inutile lors de l’ex´ecution (instructions dynamiques inutiles).
Une autre approche int´eressante consistait `a ´eliminer les ´ecritures silencieuses, une autre forme de
travail inutile.
Apr`es ce tour d’horizon global, il est assez difficile de savoir de mani`ere pr´ecise quelle est la
quantit´e de travail inutile effectu´ee par un microprocesseur lors de l’ex´ecution d’un programme.
C’est `a cette question que va tenter de r´epondre le travail `a venir en stage. . .
19
20. Chapitre 2
Compte rendu du stage
2.1 Introduction
2.1.1 Le travail inutile, qu’est ce que c’est ?
Le travail inutile est une notion difficile `a cerner. Il existe diff´erentes approches pour traiter le
probl`eme du travail effectu´e inutilement par un programme. Tout d’abord, le travail inutile peut-
ˆetre de nature statique (d´etectable et supprimable lors de la compilation) ou de nature dynamique
(visible uniquement lors de l’ex´ecution). L’´elimination du travail inutile statique est d´ej`a bien
connue et fait partie int´egrante de toute chaˆıne de compilation optimis´ee digne de ce nom. Ici nous
nous int´eresserons seulement au travail inutile de nature dynamique puisque ce dernier n’est pas
exploit´e par les processeurs ou les langages de programmation actuels.
Une fois le cadre du travail inutile dynamique pos´e, il est n´ecessaire de se donner une d´efinition
pr´ecise du travail inutile afin de pouvoir en ´evaluer la quantit´e lors de l’ex´ecution d’un programme
sur un jeu de donn´ees particulier de fa¸con automatique. Cette d´efinition, dans un premier temps
tr`es large, a ´et´e restreinte pour des raisons d’impl´ementation.
La d´efinition prise comme base de d´epart `a cette ´evaluation ´etait la suivante :
Tout travail qui ne sert, ni directement, ni indirectement, `a produire un r´esultat est jug´e inutile.
De fa¸con plus pr´ecise :
Une instruction dynamique est consid´er´ee comme utile si
- Elle produit un r´esultat ´emis en sortie du programme (ex : affichage `a l’´ecran).
- Elle produit un r´esultat utilis´e comme op´erande d’une instruction utile.
- C’est un branchement dominant une instruction utile.
20
21. Exemple simple :
instruction 1;
si booléen faire
instruction 2;
instruction 3;
fpour
instruction 4;
Dans cet exemple, le branchement conditionnel « domine » les instructions 2 et 3. Si le bool´een
est vrai et que les instructions 2 et 3 sont inutiles, alors on peut consid´erer le branchement comme
inutile.
Note
Par abus de langage, dans la suite de ce document, nous d´esignerons toutes les instructions de
transfert de contrˆole (branchement conditionnels et inconditionnels, sauts, appels de fonctions. . .)
par l’expression « instruction de branchement ».
En regardant cette d´efinition de plus pr`es, un probl`eme se pose dans le cas g´en´eral : Supposons
que l’instruction 2 soit un « store » qui range une valeur `a une adresse m´emoire, que l’instruction
4 soit un « load » et que le bool´een soit `a faux. Dans ce cas, si l’instruction 4 est consid´er´ee
comme utile et si les deux acc`es pointent sur la mˆeme adresse m´emoire, alors le branchement devra
ˆetre consid´er´e comme utile. Cependant, l’adresse de l’acc`es `a la m´emoire qui aurait pu ˆetre fait
par l’instruction 2 n’est pas connue puisque cette instruction n’a pas ´et´e ex´ecut´ee. A cause de ce
type d’acc`es `a la m´emoire (dont l’adresse acc´ed´ee n’est connue qu’`a l’ex´ecution) nous avons du
faire l’hypoth`ese conservatrice suivante : Toutes les instructions de branchements sont consid´er´ees
comme utiles.
Ce qui nous donne la d´efinition suivante :
Une instruction dynamique est consid´er´ee comme utile si
- Elle produit un r´esultat ´emis en sortie du programme (ex : affichage `a l’´ecran).
- Elle produit un r´esultat utilis´e comme op´erande d’une instruction utile.
- C’est un branchement.
Note
Par abus de langage, dans la suite de ce document, nous utiliserons le mot « ressource » pour
d´esigner soit un registre soit un emplacement m´emoire.
Partant de cette nouvelle d´efinition, l’objectif ´etait de construire un graphe de d´ependance de
donn´ee en reliant les instructions lisant une ressource `a la derni`ere instruction ayant ´ecrit dans cette
mˆeme ressource. De cette fa¸con, lorsqu’une ressource apparaˆıt comme utile (lorsque sa valeur est
´ecrite en sortie ou qu’elle est utilis´ee par une instruction de branchement), il devient possible, en
parcourant les arcs de ce graphe, de trouver toutes les instructions qui ont ´et´e utiles pour produire
21
22. ce r´esultat (le r´esultat est un graphe ressemblant `a la figure 2.2 page 25).
2.1.2 Notre protocole de test
A partir de cette d´efinition, nous avons essay´e de mesurer la quantit´e de travail inutile dans des
petits programmes d’exemple, puis, une fois ces exemples valid´es, nous avons test´e notre protocole
pour mesurer le travail inutile sur un programme plus cons´equent et surtout n’ayant pas ´et´e con¸cu
dans le but d’en mesurer la quantit´e de travail inutile. Pour faire nos tests, nous avons choisi
l’utilitaire de compression/d´ecompression de donn´ees gzip.
22
23. 2.2 La m´ethode utilis´ee
2.2.1 L’algorithme
Pour construire notre graphe de d´ependance de donn´ee, nous avons choisi d’instrumenter chaque
instruction assembleur afin de contrˆoler de fa¸con pr´ecise les entr´ees et les sorties de chacune d’elles.
Les entr´ees ´etant les op´erandes d’une instruction (les ressources ´etant lues par l’instruction) et les
sorties ´etant les r´esultats d’une instruction (les ressources ´etant ´ecrites par l’instruction).
Dans le cas g´en´eral, notre impl´ementation de la d´etection de d´ependance de donn´ee peut se
r´esumer par l’algorithme 1.
Algorithme 1: Construction du graphe de d´ependance de donn´ee
Entr´ee : Programme dont on veut ´evaluer la quantit´e de travail inutile.
Donn´ees `a fournir en entr´ee `a ce programme.
Sortie : Graphe de d´ependance de donn´ee dynamique.
1 pour chaque instruction ex´ecut´ee faire
2 Cr´eer une repr´esentation interne de cette instruction;
3 L’ajouter `a la liste des instructions dynamiques ex´ecut´ees;
{Cette repr´esentation interne contient des informations concernant l’instruction : son
num´ero dynamique, le fichier source auquel elle appartient, son num´ero statique dans ce
fichier, son type. . . }
4 pour chaque op´erande de l’instruction {ressource lue} faire
5 Lire dans la table des ressources quelle est la derni`ere instruction qui a ´ecrit dans
cette ressource;
6 Cr´eer un lien de d´ependance {arc dans le graphe} entre l’instruction courante et la
derni`ere instruction ayant ´ecrit dans la ressource en question;
{Associe `a l’op´erande le num´ero de l’instruction dynamique qui l’a produit}
fin
7 pour chaque r´esultat de l’instruction {ressource ´ecrite} faire
8 Ecrire dans la table des ressources que l’instruction courante a modifi´ee l’´etat de
cette ressource;
fin
fin
Une fois cet algorithme ex´ecut´e sur un programme particulier, il est possible de connaˆıtre les
d´ependances directes entre les instructions grˆace aux arcs construits mais aussi les d´ependances
indirectes grˆace aux chemins form´es par des suites d’arcs dans le graphe (le graphe de la figure 2.2
page 25 en est un exemple).
En ajoutant `a l’algorithme pr´ec´edent une condition dans la boucle principale permettant de
parcourir le graphe construit lorsqu’on rencontre une instruction de sortie (affichage), il devient
23
24. possible de connaˆıtre les instructions utiles lors de l’ex´ecution d’un programme (d’apr`es la premi`ere
d´efinition donn´ee ci-dessus) (cf. algorithme 3).
Proc´edure Parcours du graphe (Noeud)
1 si le noeud est marqu´e inutile alors
2 Marquer ce noeud {repr´esentant une instruction} comme ´etant utile;
pour chaque noeud op´erande de ce noeud faire
3 Appeler la proc´edure Parcours du graphe (Noeud op´erande);
fin
fin
Algorithme 3: D´etection des instructions inutiles
Entr´ee : Graphe de d´ependance de donn´ee dynamique.
Sortie : Quantit´e d’instructions dynamiques inutiles.
Localisation de ces instructions dans le code source.
pour chaque noeud du graphe faire
si le noeud repr´esente une instruction de sortie ou de branchement alors
Appeler la proc´edure Parcours du graphe (Noeud);
fin
fin
Numéro d'instruction dynamique
Identificateur d'instruction statique
Type de l'instruction
Nombre d'opérandes
Liste des instructions ayant écrit en dernier dans les opérandes
Fig. 2.1 – La structure de donn´ee d’un noeud du graphe
Un point int´eressant de cet algorithme, dont nous nous sommes rendu compte une fois l’impl´e-mentation
op´erationnelle, est qu’il permet de d´etecter les acc`es fait en lecture `a une zone m´emoire
non initialis´ee pr´ealablement. Par exemple, si un tableau de taille n est d´eclar´e et initialis´e, le fait
de tenter d’acc´eder `a l’adresse de la zone m´emoire n+1, qui n’a donc pas ´etait initialis´ee, provoque
une incoh´erence dans l’algorithme puisqu’il est impossible de trouver la derni`ere instruction ayant
´ecrit dans cette zone m´emoire (ligne 5 dans l’algorithme 1 page pr´ec´edente). De cette fa¸con, il est
possible de trouver, lors de l’ex´ecution, une erreur d’acc`es `a la m´emoire.
24
25. 39
40 38
36
37
35
34
3
14
33
0
13
25
24
32
31
30
29
28
26
27
23
17
16
22
21
20 19
18
15
9
12 11
8
10
7
2
6
5
4
1
Dans ce graphe, les noeuds sont ´etiquet´es par les num´eros dynamiques des instructions (ordre
d’ex´ecution). Les noeuds en gris sont des instructions inutiles alors que les noeuds en noir sont des
instructions utiles.
Les instructions dynamiques 4, 12 et 20 sont issues d’une seule et mˆeme instruction statique de
rangement en m´emoire dont seulement une instance est utile : l’instruction dynamique num´ero 12.
Fig. 2.2 – Exemple de graphe g´en´er´e par l’algorithme 1 & 3
25
26. 2.2.2 L’optimisation
Le gros probl`eme de cette approche est que le graphe devient tr`es rapidement ´enorme, mˆeme
avec des programmes de petite taille. En effet, ´etant donn´e qu’il faut conserver des informations
concernant chaque instruction dynamique jusqu’`a la fin de l’ex´ecution, seule une ex´ecution ayant
un nombre r´eduit d’instructions dynamiques peut ˆetre envisag´ee (aux alentours de 300 000 dans
notre impl´ementation).
Nous avons donc tent´e de r´eduire le graphe au maximum en ´eliminant au cours de l’ex´ecution
les informations qui ne nous ´etaient plus n´ecessaires.
Ce qui est fait. . .
Dans un premier temps, pour r´eduire ce graphe et donc augmenter la taille des programmes
testables, nous avons d´ecid´e de supprimer `a la vol´ee les informations concernant les instructions
« certifi´ees » utiles. Ces informations ´etant le noeud repr´esentant cette instruction et les arcs sor-tant
de celle-ci. Ces informations ne sont plus d’aucune utilit´e une fois que le parcours de l’arbre
dont cette instruction est la racine est effectu´e. Il est alors possible de les supprimer sans perdre
d’information utile `a notre calcul de quantit´e de travail inutile. Cette m´ethode permet, `a mesure
que le programme se d´eroule et envoi des informations en sortie, (affichage. . .) de r´eduire le graphe.
Il est alors d’autant plus r´eduit que la quantit´e de travail utile est importante (sur nos tests, le gain
r´eel en occupation m´emoire est d’un facteur trois `a quatre ce qui permet de tester des programmes
d´epassant le million d’instructions dynamiques sans avoir un temps d’ex´ecution r´edhibitoire).
La courbe de l’occupation m´emoire de l’algorithme au cours du temps devient alors identique
(`a quelques d´etails d’impl´ementation pr`es) `a la courbe repr´esentant la quantit´e de travail inutile
cumul´e (figure 2.9 page 40).
Quelques petites optimisations ont aussi ´etaient apport´ees au programme concernant le temps
d’ex´ecution. Bien que ce facteur ne soit pas le point crucial de notre algorithme, il semblait
int´eressant de s’y pencher pour ´eviter d’avoir des dur´ees de tests trop importantes.
Nous avons par exemple, `a la ligne 1 de la proc´edure Parcours du graphe page 24, supprim´e le
parcours d’une branche lorsque cette derni`ere poss`ede comme racine une instruction utile. En effet,
si tel est le cas, cela signifie que cette branche a d´ej`a ´et´e enti`erement explor´ee et qu’il est inutile de
la parcourir `a nouveau.
Ce qu’il reste `a faire. . .
Dans un second temps, il est int´eressant de voir que si une instruction ´ecrit dans une ou plu-sieurs
ressources, puis que ces ressources sont de nouveau ´ecrites sans ˆetre lues entre temps, les
informations concernant cette instruction inutile ne nous serviront jamais puisque cette instruction
ne sera jamais rendue utile (notion de « valeur morte »). De cette fa¸con, il est possible, ici encore,
26
27. de r´eduire notre graphe en supprimant les noeuds repr´esentant ce type d’instructions ainsi que leurs
arcs sortants.
Une mani`ere simple d’impl´ementer un tel m´ecanisme serait de consid´erer que chaque instruction
inutile est un objet et que les ressources (registres et zones m´emoire) sont des moyens d’acc´eder `a ces
objets. Si une instruction est accessible depuis au moins une ressource, alors il n’est pas possible
de supprimer les informations concernant cette instruction. En revanche, si aucune ressource ne
« r´ef´erence » l’objet, alors cet objet est inaccessible depuis les ressources et le restera jusqu’`a la fin
de l’ex´ecution du programme. Cet objet peut donc ˆetre supprim´e (noeud ainsi que ses arcs sortants).
Cette m´ethode s’apparente `a un syst`eme de ramasse-miettes comme il est souvent mis en place dans
un environnement d’ex´ecution pour lib´erer des zones m´emoires n’´etant plus r´ef´erenc´ees par aucun
pointeur.
2.2.3 Le r´esultat
La figure 2.3 page suivante est un exemple simple permettant de comprendre comment est
construit le graphe de d´ependance de donn´ee `a partir du code assembleur du programme dont on
veut ´evaluer la quantit´e de travail inutile.
Dans la figure 2.3 page suivante, les noeuds sources sont les instruction utiles par hypoth`ese (en
caract`ere gras). Ces instructions sont soit des instructions de sortie (print %valeur dans l’exemple)
soit des instructions de branchement (bne boucle dans l’exemple). Une fois ces instructions jug´ees
comme ´etant utile au programme, nous pouvons appliquer la d´efinition r´ecursive permettant de
trouver toutes les instructions ayant servi `a produire les valeurs utiles `a ces instructions. Ainsi,
dans notre exemple, l’instruction dynamique num´ero 18 (print %valeur) poss`ede comme entr´ee le
registre %valeur. Il est donc n´ecessaire de trouver la derni`ere instruction ayant ´ecrit dans ce registre.
Cette instruction est l’instruction dynamique num´ero 17 (load [@tab+2],%valeur). Ainsi de suite
r´ecursivement, l’instruction dynamique num´ero 17 poss`ede comme entr´ee la seconde case du tableau
rang´e `a l’adresse m´emoire @tab dont la derni`ere ´ecriture a ´et´e faite par l’instruction dynamique
num´ero 8 et ainsi de suite jusqu’`a n’arriver qu’`a des instructions n’ayant aucune entr´ee (copie
d’une constante dans un registre (mov 1,%indice dans l’exemple), instruction d’entr´ee au clavier
par l’utilisateur. . .).
De cette mani`ere, en parcourant le graphe, il est possible d’identifier le travail utile. Les ins-tructions
n’´etant accessible depuis aucune des sources (instructions de sorties ou de branchement)
sont identifi´ees comme ´etant du travail inutile (instructions dynamiques 2, 3, 12 et 13 dans notre
exemple).
Grˆace `a cet exemple, nous avons mis en ´evidence un cas simple comportant peu de travail inutile.
En revanche, il est facile de prendre conscience de l’importance que peut atteindre ce travail inutile
d`es lors que le traitement `a l’int´erieur d’une boucle du type de celle pr´esent´ee dans l’exemple devient
important. En effet, dans notre exemple, seule la multiplication par 10 et le rangement en m´emoire
sont inutiles mais si la valeur `a ranger dans le tableau avait ´et´e un calcul effectu´e par une fonction
comportant 10 000 instructions, les chiffres auraient ´et´e diff´erents. De mˆeme, si la taille du tableau
avait ´et´e de 10 000 cases dont seulement une aurait ´et´e utilis´ee, la quantit´e de travail inutile aurait
´et´e beaucoup plus importante. En revanche si, dans notre exemple, les trois cases du tableau avaient
27
28. 1: mov 1,%indice
boucle:
2: mul %indice,10,%valeur
3: store %valeur,[%indice+@tab-1]
4: add %indice,1,%indice
5: cmp 4,%indice
6: bne boucle
7: load [@tab+2],%valeur
8: print %valeur
Code en assembleur RISC
(code statique)
Dépendances de données permettant d’identifier le travail
utile (arcs du graphe parcourus par l’algorithme).
Dépendance de donnée n’étant pas parcourues par
l'algorithme.
Trace d’exécution des instructions
(dynamique)
1ère itération de la boucle
2ème itération de la boucle
3ème itération de la boucle
Numéro
Dynamique
Numéro
Statique
123456789
10
11
12
13
14
15
16
17
18
123456234562345678
n Numéro d’instruction statique utile par essence
(instructions de sorties ou de branchement).
n Numéro d’instruction statique utile après parcours du
graphe de dépendance (définition récursive).
n Numéro d’instruction dynamique (indique l’ordre
d’éxécution des instructions dans le temps)
n
Numéro d’instruction statique jugés comme étant inutile
d’après la définition (instruction n’appartenant pas au
graphe de dépendance de données).
Compilation
Pour i de 1 à 3 faire
t[i] := i*10;
Finpour
Ecrire (t[2]);
Code source original
Fig. 2.3 – Du code source en langage de haut niveau au graphe de d´ependance de donn´ee dynamique
´et´e affich´ees (instruction print), la quantit´e de travail inutile aurait ´et´e nulle.
Un exemple plus complet montrant dans le d´etail comment l’algorithme a ´et´e impl´ement´e est
en annexe (figure 3.2 page 59).
28
29. 2.3 L’environnement de travail : Les choix de mise en oeuvre
2.3.1 Les Outils
Pour la mise au point de notre programme d’´evaluation de la quantit´e de travail inutile, plusieurs
outils ont ´et´e mis `a contribution :
- Salto : Salto est une biblioth`eque de fonctions permettant d’analyser du code source en
assembleur pour en extraire les informations s´emantiques sous une forme exploitable en C++.
Ces informations peuvent ˆetre de diff´erentes natures : Il est possible de connaˆıtre le d´ecoupage
en blocs de base du code source, les ressources utilis´ees par une instruction pr´ecise (dans notre
cas, ce qui nous int´eresse sont les acc`es `a la m´emoire et aux diff´erents registres). De plus,
une des fonctionnalit´e indispensable `a notre r´ealisation disponible dans Salto est la possibilit´e
d’instrumenter le code source (figure 2.4 page suivante).
- Le compilateur GCC pour processeur Sparc : Compilateur C/C++ gratuit sous licence GNU.
- Le compilateur CC pour processeur Sparc : Compilateur C propri´etaire de Sun disponible
seulement pour la plateforme Sparc.
- Les expressions r´eguli`eres en C.
2.3.2 L’instrumentation
L’instrumentation c’est quoi ?
L’instrumentation d’instruction est un m´ecanisme qui consiste `a ins´erer des instructions suppl´e-mentaires
entre les instructions du code source d’un programme d´ej`a ´etabli afin « d’ausculter » ce
dernier. Les informations qu’il est possible de r´ecup´erer par ce m´ecanisme sont de nature dynamique
puisque les instructions rajout´ees par instrumentation sont ex´ecut´ees autant de fois que le sont les
instructions appartenant au code source d’origine. Prenons comme exemple le cas d’une boucle
dont le corps est ex´ecut´e n fois, alors le code rajout´e par instrumentation du code source d’origine
dans le corps de cette boucle sera lui aussi ex´ecut´e n fois. Ceci permet de savoir de fa¸con pr´ecise
quelles sont les instructions statiques qui ont ´et´e ex´ecut´ees par le processeur s´equentiellement. De
plus, l’instrumentation permet de r´ecup´erer d’autres informations dynamiques comme les adresses
des acc`es `a la m´emoire.
Dans l’exemple de la figure 2.4 page suivante, le code source original est instrument´e afin de
r´ecup´erer la valeur du r´esultat produit par l’instruction `a instrumenter et pour le traiter dans la
fonction fct (tmp ´etant par exemple un registre de d´ebuggage dont le code source original ne fait
jamais usage mais qui peut ˆetre utilis´e par la fonction fct pour effectuer son traitement).
29
30. mov r1,r2
add r2,3,r2
store r2,[a0]
Code source
mov r1,r2
mov r2,tmp
call fct
add r2,3,r2
mov r2,tmp
call fct
store r2,[a0]
load [a0],tmp
call fct
Code source
instrumenté
instrumentation
Fig. 2.4 – Instrumentation de code source en assembleur
Note
Dans l’exemple de la figure 2.4, les instructions sont instrument´ees apr`es leur ex´ecution, ce
qui n’est pas le cas dans notre impl´ementation : l’instrumentation se trouve avant l’ex´ecution de
l’instruction pour des raisons de suivi des branchements (pour pouvoir instrumenter correctement
les branchements, il est n´ecessaire de placer le code d’instrumentation avant ceux-ci).
L’instrumentation dans notre programme
L’instrumentation, dans le cas g´en´eral, permet d’ins´erer des instructions dans un programme. A
partir de ce concept simple, nous avons d´ecid´e d’utiliser l’instrumentation pour ins´erer des appels
de fonctions (´ecrites en C et compil´ees par ailleurs). De fait, les appels de fonctions en assembleur
ne faisant pas de sauvegarde des registres globaux (accessible de n’importe o`u dans le programme).
Il nous a fallu ajouter `a cette instrumentation une sauvegarde de contexte avant l’appel `a cette
fonction puis une restauration apr`es (figure 2.5 page suivante). Mais ce n’est pas tout : Chaque
instruction assembleur ayant un nombre variable d’op´erandes et de r´esultats, il nous a fallu ajouter
une instruction d’appel `a une fonction pour chaque op´erande et pour chaque r´esultat. De plus,
chaque acc`es `a la m´emoire n´ecessitant la r´ecup´eration de l’adresse de cet acc`es, il nous a fallu
r´ecup´erer des informations sur la valeur des registres utilis´es par l’instruction `a instrumenter afin
de savoir quelle ´etait l’adresse de cet acc`es m´emoire.
L’instrumentation d’une instruction dans notre programme d’´evaluation de la quantit´e de travail
inutile peut-ˆetre r´esum´ee en sept phases :
– Sauvegarde : Une phase de sauvegarde du contexte (registres globaux, d´ecalage de la fenˆetre
de registres. . .).
– D´ebut : Une phase de cr´eation de la structure de donn´ee repr´esentant une instruction (fi-gure
2.1 page 24).
– Op´erandes : Une phase permettant de cr´eer les arcs vers les instructions ayant ´ecrit en
dernier dans les ressources op´erandes de l’instruction.
30
31. inst1
inst2
inst3
Code source
sauvegarde
call fct
restauration
inst1
sauvegarde
call fct
restauration
inst2
sauvegarde
call fct
restauration
inst3
Code source
instrumenté
instrumentation
Fig. 2.5 – Principe de l’instrumentation faite par le programme d’´evaluation de la quantit´e de
travail inutile
– Milieu : Une phase, utile seulement pour les instruction « save » et « restore », permettant
de mettre `a jour le niveau de la fenˆetre de registres.
– R´esultats : Une phase permettant de mettre `a jour l’´etat des ressources en fonction des
r´esultats produits par l’instruction.
– Fin : Une phase permettant d’´evaluer l’utilit´e de l’instruction courante. Si cette derni`ere est
utile, on parcour son arbre de d´ependance de donn´ee.
– Restauration : Une phase de restauration du contexte.
2.3.3 Le choix de la Plateforme
Pour choisir notre plateforme de travail, nous avons pris en compte plusieurs param`etres. Nous
avions le choix entre le jeu d’instruction x86 (CISC) et le jeu d’instruction Sparc (RISC). En premier
lieu, l’outil mis `a notre disposition (Salto) semblait plus adapt´e `a un jeu d’instruction r´eduit.
En effet, Salto ´etant bas´e sur la reconnaissance des instructions assembleur par des expressions
r´eguli`eres, il est tr`es difficile de supporter un jeu d’instruction aussi vaste que le x86 d’Intel (CISC).
De fait, le support d’un tel jeu d’instruction par Salto est apparu comme ´etant insuffisant. De plus,
le travail `a effectuer ´etant, entre autre, d’identifier les acc`es `a la m´emoire, un jeu d’instruction
r´eduit avec seulement une instruction pour le chargement et une pour le rangement en m´emoire
nous est apparu plus simple `a manipuler. Cependant, le Sparc poss`ede quelques inconv´enients qui,
nous le verrons plus loin, ne nous ont pas facilit´e la tˆache. Nous avons donc d´ecid´e de travailler
avec un jeu d’instruction RISC pleinement support´e par Salto : le Sparc de Sun.
31
32. 2.3.4 SPARC : Le Meilleur des Mondes ?
Le Sparc est une architecture RISC assez classique ce qui signifie que le nombre d’instructions
est assez r´eduit, qu’elles ont toutes la mˆeme taille (quatre octets pour le Sparc) et que, chose
relativement importante pour notre impl´ementation, les instructions d’acc`es `a la m´emoire sont au
nombre de deux (load pour charger une valeur de la m´emoire vers un registre et store pour ranger
une valeur d’un registre vers la m´emoire). Tout aurait ´et´e parfait si la description du Sparc s’´etait
arrˆet´ee l`a. . .
- Le delay slot est une des particularit´es de l’architecture Sparc. Il s’agit d’ex´ecuter l’instruc-tion
qui suit imm´ediatement une instruction de branchement avant que cette instruction de
branchement ne modifie le flot de contrˆole de l’ex´ecution du programme. Pour r´esumer, l’ins-truction
qui se trouve dans le delay slot d’un branchement (elle se trouve juste apr`es dans le
code source) se comporte comme si elle se trouvait avant le branchement pour ce qui est du
contrˆole mais comme si elle ´etait apr`es pour ce qui est des donn´ees.
- L’annul bit est une autre particularit´e « amusante » du Sparc qui est en relation directe avec le
delay slot. Lorsqu’une instruction se trouve dans le delay slot d’un branchement dont l’annul
bit est activ´e, (be,a : la virgule et le a indique que l’annul bit est activ´e) cette instruction ne
s’ex´ecute que lorsque le branchement est pris. Dans le cas contraire, l’instruction se trouvant
dans le delay slot n’est pas ex´ecut´ee (on dit qu’elle est annul´ee).
- La fenˆetre de registres tournante est une fa¸con de pallier `a la lenteur des acc`es `a la pile lors du
passage de param`etres `a une fonction. En effet, le Sparc se propose de r´esoudre le probl`eme
du passage de param`etres `a une fonction de mani`ere originale au moyen de cette fenˆetre de
registres tournante. Il s’agit de ranger les valeurs que l’on souhaite passer en param`etres `a
la fonction qui va ˆetre appel´ee dans des registres sp´eciaux nomm´es registres de sortie (%o0
`a %o5) qui, une fois la fonction appel´ee, seront renomm´es en registres d’entr´ee (%i0 `a %i5)
(cf. figure 2.6 page suivante). De cette fa¸con, la fonction appel´ee peut se servir de ces valeurs
sans qu’elles n’aient ´et´e recopi´ees dans une quelconque pile (`a l’exception du cas o`u la taille
de la fenˆetre de registres tournante n’est pas suffisante).
Le delay slot
Dans notre impl´ementation, nous avons du prendre en compte cette particularit´e de l’archi-tecture
Sparc. En effet, afin d’instrumenter les instructions se trouvant dans le delay slot d’un
branchement, nous avons du utiliser diff´erentes techniques consistant `a « remonter » notre instru-mentation
de ce type d’instructions avant le branchement. Ceci `a conduit `a pas mal de probl`emes
de coh´erence entre la repr´esentation des instructions cr´e´ees par l’instrumentation et les instructions
r´eellement ex´ecut´ees.
Mais le cas o`u le delay slot nous a pos´e le plus de probl`eme est celui o`u il nous a fallu ajouter des
sauts afin d’´eviter l’ex´ecution de certaines instructions. En effet, dans ce cas, si l’instruction que l’on
veut « sauter » se trouve dans le delay slot d’un branchement, il faut dupliquer cette instruction
des branchement : mettre dans le code source une version normale avec l’instruction qui se trouvait
dans son delay slot dans le code source d’origine et une seconde version du mˆeme branchement
contenant un nop dans son delay slot. De cette fa¸con, selon que l’instruction se trouvant dans le
32
33. delay slot de ce branchement doit ˆetre ex´ecut´ee ou non, le flot de contrˆole est aiguill´ee vers l’une
ou l’autre des deux versions de ce branchement. Il faut ensuite faire converger les deux versions en
un mˆeme point repr´esentant la suite du programme.
La fenˆetre de registres du Sparc
La fenˆetre de registre tournante est une des particularit´e du processeur Sparc. Nous allons en
expliquer rapidement le principe afin d’exposer la mani`ere dont nous avons trait´e cette particularit´e
dans notre programme.
Registres de sortie
%o0 à %o7
Registres locaux
%l0 à %l7
Registres d’entrée
%i0 à %i7
Registres accessibles
au niveau n+1
niveau n
Registres de sortie
%o0 à %o7
Registres locaux
%l0 à %l7
Registres d’entrée
%i0 à %i7
Désigne les mêmes
registres physiques
Registres de sortie
%o0 à %o7
Registres locaux
%l0 à %l7
Registres d’entrée
%i0 à %i7
niveau n+2
Désigne les mêmes
registres physiques
L’instruction « save » permet de passer d’un niveau n `a un niveau n+1 (utilis´e g´en´eralement comme
une sauvegarde de contexte avec en plus la possibilit´e de passer des param`etres d’un contexte `a
l’autre au niveau de la zone de recouvrement de la fenˆetre n et n+1).
L’instruction « restore » permet de passer d’un niveau n `a un niveau n-1 (utilis´e g´en´eralement
comme une restauration de contexte avec en plus la possibilit´e de passer des r´esultats d’un contexte
`a l’autre au niveau de la zone de recouvrement de la fenˆetre n et n-1).
Fig. 2.6 – Le principe de la fenˆetre de registres du Sparc
Lorsque le nombre de registres utilis´es d´epasse le nombre de registres physiques r´eellement
pr´esents dans le processeur, un m´ecanisme invisible pour l’utilisateur utilise une pile en m´emoire
pour sauvegarder la fenˆetre de registres la plus ancienne et r´eutiliser ainsi cette derni`ere comme
une nouvelle fenˆetre vierge. De cette fa¸con, le nombre de registres virtuellement utilisables par
l’utilisateur n’est limit´e que par la taille de la pile et non par la taille du fichier de registres dans le
processeur. Cette fenˆetre est souvent repr´esent´ee, dans les diff´erentes documentations sur le Sparc,
de fa¸con circulaire pour montrer que la plus ancienne fenˆetre et la plus r´ecente peuvent se recouvrir
si le nombre de fenˆetres disponibles dans le fichier de registres est insuffisant pour l’ex´ecution d’un
programme donn´e.
33
34. 2.3.5 S’affranchir de la num´erotation des registres faite par Salto
Les registres du Sparc s’organisent en deux parties. D’une part, des registres dit globaux qui se
comportent de mani`ere classique, et d’autre part une fenˆetre de registres coulissante comme d´ecrit
sur la figure 2.6 page pr´ec´edente. Pour les registres globaux, nous avons utilis´e la num´erotation
propos´ee par Salto qui convenait tout `a fait ´etant donn´e qu’un nom de registre d´esigne toujours le
mˆeme registre physique.
En revanche, pour la fenˆetre de registres coulissante, nous avons du mettre en place notre propre
syst`eme d’identification de registres :
En effet, Salto ´etant un outil qui travaille sur du code assembleur, il lui est impossible d’avoir
acc`es `a des informations concernant l’ex´ecution du programme (les seules informations disponible
au niveau de Salto sont les informations statiques sur le programme). De fait, les informations
que nous donne Salto concernant les acc`es aux registres sont les noms de ces registres. Il lui est
impossible de connaˆıtre le niveau de la fenˆetre de registre en un point donn´e du code et donc de
d´esigner un registre physique de mani`ere unique. Afin d’identifier de mani`ere unique chacun des
registres physiques, il a donc fallu s’affranchir du syst`eme de num´erotation des registres propos´e
par Salto pour le remplacer par un calcul fait de mani`ere dynamique (au cours de l’ex´ecution du
programme) permettant de savoir `a quel registre physique correspondait chaque acc`es `a un registre
appartenant `a la fenˆetre de registre courante.
Pour ce faire, nous avons instrument´e les instructions save qui d´ecalent la fenˆetre de registre
vers le haut et les instructions restore qui d´ecalent la fenˆetre de registre vers le bas (cf. figure 2.6
page pr´ec´edente). De cette fa¸con, il est possible de tenir `a jour une variable globale indiquant le
niveau actuel de la fenˆetre de registre. En utilisant ce niveau comme d´ecalage par rapport `a un
point de r´ef´erence, (la premi`ere fenˆetre disponible lors du lancement du programme) il est possible
de savoir dans quelle fenˆetre de registre seront fait les acc`es aux registres de la fenˆetre courante
indiqu´es par Salto.
Grˆace `a cette m´ethode, il est possible d’identifier de mani`ere unique chacun des registres d’une
fenˆetre pr´ecise mˆeme si celle-ci peut avoir ´et´e sauvegard´ee dans la pile par manque de place dans
le fichier de registres. Les acc`es `a ces registres restent ce qu’ils sont puisque cette op´eration est
transparente pour l’utilisateur du processeur (en l’occurrence notre programme).
2.3.6 Les expressions r´eguli`eres
L’utilisation des expressions r´eguli`eres est particuli`erement utile pour analyser une chaˆıne de
caract`eres ayant un motif fixe et une partie variable. C’est justement le cas d’une instruction
assembleur dont la partie fixe est le mn´emonique de cette instruction et dont les parties variables
sont les arguments.
De cette fa¸con, nous avons utilis´e les expressions r´eguli`eres pour « d´ecouper » les instructions
load et store en plusieurs parties : le mn´emonique d’une part (permettant de connaˆıtre la taille de
l’acc`es `a la m´emoire : octet, mot, double mot ou quadruple mot) et les arguments d’autre part. Une
34
35. fois chaque argument r´ecup´er´e, il est possible d’acc´eder aux valeurs contenues dans les registres
et aux ´eventuelles constantes. Il est donc possible de connaˆıtre de fa¸con exacte `a quelle adresse
m´emoire va acc´eder l’instruction et `a combien d’octets elle va acc´eder.
De plus, les expressions r´eguli`eres nous ont ´et´e utiles pour r´ecup´erer les ´etiquettes des appels
de fonctions afin de savoir si ces fonctions ´etaient d´efinies en local (dans le code source du pro-gramme
´etudi´e) ou si elles appartenaient `a une biblioth`eque externe au programme (stdio en C
par exemple).
2.3.7 La gestion des fonctions
Une fois notre d´efinition impl´ement´ee et test´ee, il reste encore de nombreux probl`emes `a r´esoudre
pour pouvoir confronter cet algorithme au « monde r´eel » (`a des programmes classiques tel que
gzip). En effet, ce mod`ele th´eorique serait parfait si l’ensemble du code source assembleur d’une
application ´etait visible par le programme d’´evaluation. Or les programmes classiques utilisent des
fonctions d´efinies dans des biblioth`eques dont on n’a pas le code source (Un programme en C
travaillant sur des fichiers utilisera par exemple stdio pour lire et ´ecrire dans un fichier). Afin de
r´egler cette difficult´e, nous avons du utiliser l’options de compilation -SO de cc permettant de savoir
quels sont les registres du Sparc et les adresses m´emoires utilis´ees dans la pile comme param`etres
lors de l’appel `a une fonction. Ainsi, il nous a ´et´e possible de rajouter « artificiellement » des arcs
de d´ependance entre l’appel `a une fonction d´efinie dans une biblioth`eque externe (call printf
par exemple) et les param`etres pass´es `a cette fonction. Pour cette raison, cc pour Sparc est apparu
comme ´etant id´eal dans notre protocole de test.
Afin de r´esoudre ce probl`eme, les fonctions appel´ees par le programme ´etudi´e ont ´et´e class´ees
en trois cat´egories, chacune correspondant `a un traitement particulier `a faire pour les prendre en
compte correctement :
Les fonctions « internes »
Les fonctions internes sont les fonctions dont le code source est disponible (peut-ˆetre utilis´e par
notre programme d’´evaluation). Les appels `a ce type de fonctions peuvent donc ˆetre trait´es comme
de simple branchements inconditionnels puisque les arcs du graphe de d´ependance peuvent tr`es bien
relier une instruction appartenant `a cette fonction `a une instruction appartenant au programme
appelant. Le graphe de d´ependance de donn´ee n’est donc pas interrompu par un tel appel de
fonction.
Les fonctions « externes » utilisant des pointeurs
Les fonctions externes sont les fonctions dont le code source n’est pas disponible (code source
d’une biblioth`eque d’entr´ee/sortie par exemple). Dans ces cas l`a, il est n´ecessaire de consid´erer que
les param`etres pass´es `a la fonction sont lus par l’instruction d’appel de la fonction et que le r´esultat
35
36. est ´ecrit par cette instruction.
Cependant, dans le cas g´en´eral, il est n´ecessaire de d´etourner l’appel `a une telle fonction pour
faire ajouter « artificiellement » des arcs de d´ependance de donn´ee pour traiter les donn´ees qui
vont ˆetre lues et ´ecrites `a l’int´erieur de cette fonction. En effet, notre programme est incapable
de connaˆıtre le type des param`etres d’une fonction (si il s’agit d’entiers, de pointeurs. . .) et donc,
de savoir si une fonction d´efinie dans une biblioth`eque dont on n’a pas le code source acc`ede ou
non `a une zone m´emoire dont l’adresse est un de ses param`etres. De plus, mˆeme en sachant qu’un
param`etre est un pointeur, rien ne dit si la fonction dont on n’a pas le code source va y acc´eder
en lecture, en ´ecriture ou pire encore, `a combien d’´el´ements elle va acc´eder ! (Il ne faut pas oublier
qu’en C les tableaux sont implicites et que, par cons´equent, on ne connaˆıt pas leur taille simplement
en connaissant l’adresse de leur premier ´el´ement). Afin de r´egler cette difficult´e, nous avons choisi de
d´etourner les appels `a ces fonctions (au nombre de trente cinq dans gzip) afin de leur faire ex´ecuter
du code permettant de simuler leur comportement en mati`ere d’acc`es `a la m´emoire. Ces fonctions
sont, le plus souvent des fonctions d’entr´ees/sorties (ex : read, write, fflush. . .), des fonctions de
lecture/´ecriture dans des chaˆınes de caract`eres (ex : strcat, strcpy, strcmp. . .) ou des fonctions
de lecture/´ecriture de zones m´emoire en g´en´eral (ex : memset, memcpy, memcmp. . .).
Exemple simple :
char *my_strcpy(char *c1, const char *c2)
{
/* On simule un acc`es en lecture `a une zone m´emoire d´ebutant `a l’adresse
contenue dans le pointeur c2 et de longueur strlen(c2)+1 (taille de la
cha^ıne de caract`ere `a lire avec son terminateur) */
instrumentationEntreeMemoire((int)c2, strlen(c2)+1);
/* On simule un acc`es en ´ecriture `a une zone m´emoire d´ebutant `a l’adresse
contenue dans le pointeur c1 et de longueur strlen(c2)+1 (taille de la
cha^ıne de caract`ere `a copier avec son terminateur) */
instrumentationSortieMemoire((int)c1, strlen(c2)+1);
/* On retourne la valeur retourn´ee par la vraie fonction strcpy */
return strcpy(c1, c2);
}
Les fonctions « externes » n’utilisant pas de pointeurs
Les fonctions externes n’utilisant pas de pointeurs sont des fonctions dites externes d’apr`es la
d´efinition ci-dessus `a la diff´erence qu’elles ne font pas d’acc`es `a la m´emoire `a partir de pointeur.
Elles peuvent donc ˆetre trait´ees simplement en consid´erant que les param`etres pass´es `a la fonction
sont lus et que le r´esultat est ´ecrit.
36
37. 2.4 R´esultats & Analyse
2.4.1 Les chiffres. . .
Une fois l’´evaluation de la quantit´e de travail inutile effectu´ee de cette mani`ere, nous obtenons
les chiffres suivants (cf. figure 2.7).
gzipa gzipb gzipc
O0d 11.03 % 9.53 % 11.03 %
O1 12.05 % 10.73 % 16.44 %
O2 11.40 % 10.28 % 16.17 %
O3 12.77 % 12.10 % 21.83 %
O4 12.66 % 12.55 % 23.35 %
O5 12.66 % 12.55 % 23.35 %
gunzipe gunzipf gunzipg
O0 1.09 % 1.14 % 0.21 %
O1 2.94 % 3.10 % 0.29 %
O2 3.51 % 4.28 % 0.41 %
O3 10.01 % 10.51 % 0.47 %
O4 9.83 % 10.26 % 0.52 %
O5 9.83 % 10.26 % 0.52 %
aCompression d’un fichier texte (RTF) de 2127 octets avec gzip (fort taux de compression (facteur : 1.9))
bCompression d’un fichier image (GIF) de 1704 octets avec gzip (faible taux de compression (facteur : 1.4))
cRe-compression d’un fichier compress´e par gzip de 1506 octets avec gzip (taux de compression nul (facteur : 0.98))
dSeuls ces r´esultats ont ´et´e valid´es avec le programme de v´erification pour des raisons d’impl´ementation
eD´ecompression du fichier texte de 2127 octets
fD´ecompression du fichier image de 1704 octets
gD´ecompression du fichier compress´e deux fois par gzip de 1506 octets
Conditions de test : gzip version 1.2.4 recompil´e avec cc de Sun pour le processeur Sparc version
v8plus. Ces chiffres s’entendent en ne comptant pas les ´eventuelles instructions nop qui se trouvent
dans le delay slot de certaines instructions de branchement.
Fig. 2.7 – Quantit´e d’instructions assembleurs inutiles lors de l’ex´ecution de gzip dans diff´erentes
conditions
2.4.2 Le doute. . .
Introduction
Ces chiffres n’´etant qu’une ´evaluation, rien ne permet de dire avec certitude que ce travail est
effectivement inutile. D’autant plus que l’impl´ementation laisse souvent apparaˆıtre des failles que
l’on n’imagine pas lorsqu’on raisonne de fa¸con abstraite sur les d´ependances de donn´ees (la gestion
des fonctions dont on n’a pas le code source en est un exemple). Afin de valider notre programme
et d’avoir la certitude que le travail inutile ´evalu´e en ´etait bien, nous avons mis au point un second
programme r´e-ex´ecutant exactement le mˆeme programme de test (gzip dans notre cas) sur le mˆeme
jeu de donn´ees (avec le mˆeme fichier en entr´ee) en n’ex´ecutant pas les instructions qui avaient ´et´e
jug´ees comme ´etant inutiles lors de la premi`ere ex´ecution. Ainsi, si la seconde ex´ecution donne
rigoureusement le mˆeme r´esultat que la premi`ere (le mˆeme fichier compress´e dans le cas de gzip),
nous pouvons dire que les deux ex´ecutions sont ´equivalentes et que, par cons´equent, les instructions
qui n’ont pas ´et´e ex´ecut´ees lors de la seconde ex´ecution n’´etaient effectivement pas utiles.
37
38. somewhere :
sub r3,r4,r5
...
mov r1,r2
add r2,3,r2
cmp 0,r2
be somewhere
store r2,(a0)
...
Code source
mov r1,r2
add r2,3,r2
cmp 0,r2
be somewhere
sub r3,r4,r5
...
Trace dynamique de la
première exécution
(détection du travail inutile)
mov r1,r2
add r2,3,r2
cmp 0,r2
be somewhere
store r2,(a0)
...
Trace dynamique de la
deuxième exécution (non
exécution du travail inutile)
L'instruction add est jugée inutile :
elle ne sera donc pas exécutée
lors de la seconde exécution
Fig. 2.8 – Mise en ´evidence d’un probl`eme d’impl´ementation par divergence du flot de contrˆole
Note
Reproduire une ex´ecution `a l’identique ne fonctionne que sur un programme d´eterministe : deux
ex´ecutions successives sur un mˆeme jeu de donn´ee doivent s’ex´ecuter rigoureusement de la mˆeme
mani`ere. De fait, il serait complexe de traiter des programmes utilisant des fonctions de tirages
al´eatoires ou conservant des informations d’une ex´ecution sur l’autre (cache dans un fichier par
exemple). Ce n’est pas le cas de gzip ce qui nous a permis de mener nos tests de fa¸con correcte
sur ce programme.
L’int´erˆet
Un aspect tr`es int´eressant de l’utilisation de ce programme de v´erification est qu’il a permis
d’affiner le programme d’´evaluation de la quantit´e de travail inutile. En effet, en observant les
divergences entre la premi`ere et la seconde ex´ecution (figure 2.8), il a ´et´e possible de trouver les
points faibles du programme d’´evaluation de la quantit´e de travail inutile et de les consolider afin de
rendre les deux ex´ecutions ´equivalentes. Par exemple, lorsque la seconde ex´ecution du programme
de test divergeait de la premi`ere (un branchement pris alors qu’il n’aurait pas du par exemple), cela
signifiait qu’une instruction utile au flot de contrˆole avait ´etait jug´ee comme ´etant inutile `a tort. De
cette mani`ere, il a ´et´e possible de trouver les incorrections du programme d’´evaluation du travail
inutile et surtout de mettre en lumi`ere les lacunes d’une impl´ementation trop « na¨ıve » par rapport
aux appels de fonctions d´efinies dans des biblioth`eques dont le code source n’est pas disponible.
Dans l’exemple de la figure 2.8, le branchement be (branch if equal) est pris lors de la premi`ere
ex´ecution alors qu’il n’est pas pris lors de la seconde, ce qui entraˆıne une incoh´erence entre les deux
38
39. ex´ecutions qui ne sont alors plus ´equivalentes. Ceci se produit en raison d’un mauvais jugement
port´e sur l’instruction add. En effet, celle-ci est utile au bon d´eroulement du programme alors
qu’elle a ´et´e jug´ee comme ne l’´etant pas par le programme de d´etection du travail inutile. Grˆace `a
ce syst`eme, il est facile de corriger les incorrections et impr´ecisions que comporte le programme de
d´etection du travail inutile. Par processus incr´emental, il est alors possible de corriger ces erreurs
une `a une jusqu’`a l’obtention d’un programme qui ne juge inutile que du travail r´eellement inutile
(Ce qui ne prouve pas pour autant qu’il d´etecte tout le travail inutile que peut comporter un
programme).
La M´ethode
Pour pouvoir mettre au point ce deuxi`eme programme, il faut tout d’abord que le programme
d’´evaluation de la quantit´e de travail inutile laisse une trace des instructions inutiles dans un
fichier qui sera utilis´e par le programme de v´erification. Pour la mise au point de ce programme de
v´erification, Salto a, l`a encore, ´et´e sollicit´e afin d’instrumenter chaque instruction. Lorsque cette
instruction est jug´ee inutile (d’apr`es la trace de la premi`ere ex´ecution) alors cette instruction est
saut´ee au moyen d’un jump d’une valeur constante puisque, dans les processeurs Sparc, toutes les
instructions ont une taille de quatre octets (« merci » les jeux d’instructions RISC). De cette fa¸con,
il est assez facile de « sauter » une instruction lorsqu’elle apparaˆıt dans la trace des instructions
inutiles.
Conclusion
Ce programme `a permis, sur des exemples simples, de v´erifier que la quantit´e de travail inutile
´evalu´ee ´etait bien du travail inutile quelque soit le niveau d’optimisation utilis´e et les cas de figure
rencontr´es. Cependant, par manque de temps, nous n’avons r´eussi `a le faire fonctionner que sur gzip
compil´e avec un niveau d’optimisation de 0. N´eanmoins, cette v´erification nous `a permis d’accroˆıtre
la confiance en nos r´esultats (figures 2.7 page 37 et 2.9 page suivante).
2.4.3 La r´epartition du travail inutile
Une fois les informations sur le travail inutile lors de l’ex´ecution d’un programme r´ecup´er´ees,
il est n´ecessaire de les organiser afin de pouvoir analyser d’ou provient ce travail inutile. Dans
un premier temps, nous avons essay´e de voir `a quelles instructions statiques correspondaient nos
instructions dynamiques inutiles. Nous avons trouv´e, sans surprise ´etant donn´e les r´esultats de
l’article [1], que seul un petit nombre d’instructions statiques ´etaient concern´ees. Ce qui signifie
que la plupart des instructions dynamiques inutiles sont concentr´ees sur un nombre d’instructions
statiques r´eduit (de l’ordre de 12.4 % des instructions statiques totales g´en`erent au moins une
instance dynamique inutile). Une fois ces instructions statiques en assembleur identifi´ees, nous
avons cherch´e `a « remonter », lorsque c’´etait possible, au code source en C correspondant afin
de mieux comprendre la raison pour laquelle ce travail est jug´e comme ´etant inutile par notre
d´efinition.
39
40. 108876.0
90730.0
72584.0
54438.0
36292.0
18146.0
0.0
Compression d’un fichier RTF de 2127 octets
Algorithme GZIP compile avec cc et un niveau d’optimisation de 0
0.0 268685.0 537370.0 806055.0 1074740.0
(a) Sans optimisations de compilation
53388.0
44490.0
35592.0
26694.0
17796.0
8898.0
0.0
Compression d’un fichier RTF de 2127 octets
Algorithme GZIP compile avec cc et un niveau d’optimisation de 5
0.0 105695.0 211390.0 317085.0 422780.0
(b) Avec optimisations de compilation
Abscisse : Num´ero d’instruction dynamiques : repr´esente le temps ´ecoul´e en nombre d’instructions
Ordonn´ee : Quantit´e d’instructions inutiles (cumul´ees)
Fig. 2.9 – ´Evolution de la quantit´e de travail inutile en fonction du temps
Travail inutile algorithmique
Introduction En observant les courbes de la figure 2.9 on s’aper¸coit que l’algorithme de gzip,
dans nos conditions de test, se d´ecompose en plusieurs phases g´en´erant chacune des quantit´es de
travail inutile diff´erentes. En premier lieu, il est int´eressant de noter que la phase d’initialisation
comporte une grande proportion de travail inutile (presque 50 % avec un niveau d’optimisation
de 0 et plus de 50 % avec un niveau de 5). Ensuite, vient une courte phase durant laquelle aucun
travail inutile n’est pr´esent (quelque soit le niveau d’optimisation).
Vient ensuite un ensemble de phases que nous appellerons le coeur de l’algorithme durant lequel
on observe une quantit´e de travail inutile moyen non n´egligeable avec un niveau d’optimisation de
0 (de l’ordre de 6,9 %) et plus important encore avec un niveau d’optimisation de 5 (de l’ordre de
9,9 %).
La phase d’initialisation Dans certains cas, le travail inutile semble ˆetre d’origine algorith-mique.
En effet, en observant le code source en C de gzip, il apparaˆıt parfois du travail inutile qu’il
serait simple d’´eviter en modifiant une petite partie du code. De fait, nous pouvons dire que ce
travail inutile est inh´erent `a la fa¸con dont l’algorithme de gzip est impl´ement´e.
De plus, ´etant donn´e une tr`es forte proportion de travail inutile durant la phase d’initialisation
des structures de donn´ees utiles `a l’algorithme de gzip (aux alentours de 50%), il semble raisonnable
de penser qu’une tr`es grande partie de ces structures de donn´ees sont initialis´ees puis jamais utilis´ees
ou r´eutilis´ees pour ˆetre ´ecrites (ce qui g´en`ere des valeurs mortes). Ce type de travail inutile semble
40
41. ˆetre r´eellement inh´erent `a l’algorithme et non du `a une mauvaise impl´ementation de celui-ci.
Exemple simple :
Dans la fonction local void gen codes (tree, max code), on observe que le code source
suivant est inutile la plupart du temps (lors de l’ex´ecution sur un fichier de test) :
for (bits = 1; bits <= MAX_BITS; bits++) {
next_code[bits] = code = (code + bl_count[bits-1]) << 1;
}
L’affectation dans le tableau next code est inutile 52 fois sur 60 dans l’exemple test´e (le nombre
d’it´erations de la boucle est MAX BITS et est ´egal `a 60). Le fait que cette affectation soit inutile un
certain nombre de fois engendre qu’une partie des calculs fait dans la boucle devient inutile. Ce
qui nous donne, pour l’ensemble de la boucle, un nombre de 824 instructions inutiles pour 1440 au
total (soit une proportion de 57 %).
Etant donn´e ces r´esultats, il est int´eressant de se pencher sur le cas de l’initialisation des
structures de donn´ees en g´en´eral. En effet, le premier r´eflexe d’un programmeur, lorsqu’il d´eclare
une structure de donn´ee (tableau, liste. . .) est de l’initialiser pour ´eviter, par la suite, d’y faire
un acc`es en lecture sans y avoir pr´ealablement rang´e une valeur. Or ce r´eflexe de programmation
est probablement ce que nous observons ici ´etant donn´e que les structures de donn´ees de gzip
n’´echappent apparemment pas `a cette r`egle.
Le coeur de l’algorithme Au coeur de l’algorithme, nous observons diff´erents cas d’instructions
dynamiques inutiles. Parfois, nous observons que le travail inutile est du `a l’initialisation de va-riables
locales dont le contenu est, la plupart du temps, r´e-´ecrit avant d’ˆetre lu. Parfois, il s’agit de
param`etres pass´es `a une fonction et qui ne servent que dans certaines conditions. Et enfin, un cas
assez fr´equent ´egalement est celui des variables globales qui sont maintenues `a jour de fa¸con inutile.
En effet, si une telle variable refl`ete une valeur lors du dernier passage dans une certaine fonction,
il est possible que cette fonction soit appel´ee plusieurs fois sans que cette valeur n’ai ´et´e lue entre
temps.
Exemples :
L’affectation prev match = match start ; peut-ˆetre inutile car la seule utilisation de la variable
prev match en lecture est le cas suivant :
if (prev_length >= MIN_MATCH && match_length <= prev_length) {
check_match(strstart-1, prev_match, prev_length);
flush = ct_tally(strstart-1-prev_match, prev_length - MIN_MATCH);
...
}
41
42. Ce qui signifie que lorsque la condition ci-dessus sera fausse, l’affectation de la variable prev match
sera inutile (c’est le cas 78 fois 82 dans notre test). En supposant que le calcul de la valeur de la
variable match start puisse ˆetre coˆuteux, et que cette variable ne soit pas r´e-utilis´ee en lecture
entre temps, on prend conscience de la port´ee que peut avoir le travail inutile.
Note
Dans l’exemple pr´ec´edent, il est int´eressant de noter que le d´eplacement de l’instruction d’affec-tation
prev match = match start ; dans le corps de la conditionnelle aurait suffit `a ´eliminer
ce travail inutile (dans la mesure o`u on ne fait pas d’´ecriture dans match start entre temps).
En effet, la variable prev match n’´etant utilis´ee que dans ce bloc, il est inutile de faire cette
affectation si la condition n’est pas vraie.
Une macro un peu particuli`ere a ´egalement retenu notre attention. Elle se trouve au coeur de
l’algorithme de compression, dans la fonction deflate(). Il s’agit de la macro INSERT STRING qui
ins`ere une chaˆıne de caract`ere dans la liste des chaˆınes de caract`eres qu’utilise gzip pour trouver
les chaˆınes les plus fr´equemment pr´esentes dans le fichier `a compresser.
Voici le code de cette macro apr`es passage du pr´e-processeur :
((ins_h = (((ins_h)<<((15+3-1)/3)) ^
( window[(strstart) + 3-1])) &
((unsigned)(1<<15)-1)),
prev[(strstart) & (0x8000-1)] = hash_head = (prev+0x8000)[ins_h],
(prev+0x8000)[ins_h] = (strstart));
Pour des raisons de lisibilit´e, nous avons r´e-´ecrit ce code :
ins_h = ( ins_h<<5 ^ window[strstart+2] & (unsigned)(1<<15)-1 );
hash_head = (prev+0x8000)[ins_h];
prev[strstart & (0x8000-1)] = hash_head;
(prev+0x8000)[ins_h] = strstart;
Dans cette macro, qui se trouve au coeur de l’algorithme de compression, les deux derni`eres
instructions (remplissage du tableau prev) se trouvent ˆetre tr`es souvent inutile (77 fois sur 82 dans
notre exemple). Ceci tend `a montrer que l’algorithme de compression utilis´e par gzip contient, par
nature, du travail inutile.
Travail inutile introduit par le compilateur. . .
. . . lors des phases d’optimisation de compilation On observe ´egalement que la version
compil´ee avec un niveau d’optimisation de 0 pr´esente une quantit´e globale de travail inutile moins
important (proportionnellement) `a la version compil´ee avec un niveau d’optimisation de 5. De plus,
42
43. l’´ecart entre les deux versions s’accentue dans le coeur de l’algorithme. En effet, durant la phase
d’initialisation, les deux versions se comportent `a peu pr`es de la mˆeme fa¸con (aux alentours de
50 % de travail inutile) alors que dans le coeur de l’ex´ecution, la version non optimis´ee comporte en
moyenne 6.9 % de travail inutile `a comparer aux 9.9 % observ´e dans le cas de la version optimis´ee.
Ce ph´enom`ene avait d´ej`a ´et´e constat´e dans l’article [1] mais uniquement au sujet des valeurs mortes.
. . . du au jeu d’instruction du processeur Cette ´etude n’est absolument pas exhaustive sur
les diverses causes que peut avoir le travail inutile. Cependant, mˆeme si cet aspect n’a pu ˆetre
explor´e pour des raisons de temps, il parait raisonnable de penser qu’une partie du travail inutile
pourrait avoir ´et´e introduit en raison des contraintes impos´ees par le jeu d’instructions utilis´e. En
effet, dans un jeu d’instruction RISC (comme le Sparc) une instruction de haut niveau (en langage
C par exemple) peut ˆetre convertie par le compilateur en une suite tr`es importante d’instructions
comme en une seule. Ceci d´epend de l’´eloignement de cette instruction en langage C par rapport aux
instructions disponibles dans le jeu d’instruction assembleur utilis´e. A contrario, un jeu d’instruction
CISC (comme le x86) aura des instructions assembleur plus proche des instructions en langage de
haut niveau. De cette fa¸con, les proportions d’instructions assembleur inutiles peuvent ne pas ˆetre
identiques aux proportions d’instructions inutiles de haut niveau (en langage C par exemple).
De plus, certaines optimisations de compilation effectuant un r´e-ordonnancement des instructions
assembleur, il est parfois difficile de savoir quel ensemble d’instructions assembleur repr´esente quelle
instruction de haut niveau.
Conclusion
En conclusion, nous pouvons dire que les proportions de travail inutile trouv´ees se rattachent
majoritairement au travail inutile pr´esent dans l’algorithme en langage de haut niveau. De fait, une
piste qui pourrait ˆetre int´eressante pour r´eduire ce travail inutile serait de signaler au programmeur,
lors des premi`eres ex´ecutions d’un prototype de programme, que certaines parties de l’algorithme
g´en`erent une grande quantit´e de travail inutile et que, par cons´equent, une r´e-´ecriture en prenant en
compte cet ´etat de fait pourrait ´eviter ce travail. Il est mˆeme possible d’imaginer un outil proposant
au programmeur une ´ebauche de solution pour l’aider `a restructurer une partie de son code afin
d’´eviter ce travail inutile. Cependant, ce type d’outils ne peut rien pour aider `a ´eliminer le travail
inutile intrins`eque `a l’algorithme.
43
44. 2.5 Conclusion
Cette ´etude est, en premier lieu, une ´etude permettant de comprendre un ph´enom`ene, `a priori,
contre intuitif : Le travail inutile. Pour ce faire, nous nous sommes bas´e sur des r´esultats existants
qui ont d´ej`a ´et´e publi´es et qui montre que le travail inutile existe bel et bien dans des programmes
classiques.
Le but de ce stage ´etait d’´elargir les d´efinitions donn´ees dans ces articles afin d’avoir une
id´ee du travail inutile global qui peut se trouver dans un programme. Cette ´etude, contrairement
`a celles cit´ees ci-contre, n’avait pas pour but de trouver un moyen d’exploiter ce travail inutile
pour en r´eduire l’impact sur le temps d’ex´ecution ou la consommation ´electrique mais seulement
de comprendre ce ph´enom`ene et de savoir pourquoi ce travail inutile est pr´esent (est-ce du au
compilateur ?, au programmeur ?. . .).
En conclusion, nous pouvons dire que cette ´etude `a permis de confirmer l’existence du travail
inutile et de comprendre, en partie, d’o`u il provient.
44
45. Bibliographie
[1] G. Sohi A. Butt. Dynamic dead-instruction detection and elimination. ASPLOS X, October
2002.
[2] Jeffrey D. Ullman Alfred V. Aho, Ravi Sethi. Compilers : Principles, Techniques and Tools.
Addison-Wesley, 1986.
[3] Gordon B. Bell. Characterization of silent stores. Submitted in partial fulfillment of the M.S.
Degree in Electrical and Computer Engineering, May 2001.
[4] F. Bodin. Cours d’optimisation : Transformer pour la performance. Septembre 2002.
[5] M. Lipasti K. Lepak, G. Bell. Silent stores and store value locality. IEEE Transactions on
Computers, 50(11), November 2001.
[6] Mikko H. Lipasti Kevin M. Lepak. Temporally silent stores. ASPLOS X, October 2002.
[7] Kevin M. Lepak. Silent stores for free : Reducing the cost of store verification. Submitted in
partial fulfillment of the M.S. Degree in Electrical and Computer Engineering, December 2000.
[8] Charles N. Fischer Milo M. Martin, Amir Roth. Exploiting dead value information. Proceedings
of Micro-30, December 1997.
45