Mots clés : hautes performances,
compilation, hiérarchie mémoire, optimisation, transformation
de code .
Résumé :
L'efficacité de l'exécution d'une application tant sur
une machine multiprocesseur que sur un PC ou
une station de travail dépend très fortement de la
structure des programmes. Cette structure est imposée par
le programmeur mais comporte des degrés de liberté que des
techniques logicielles, appelées optimisations de code,
peuvent exploiter pour augmenter la performance des
applications. Nous présentons un rapide aperÇu des
techniques de transformations de code disponibles pour
implémenter un compilateur optimiseur. Ces transformations
peuvent être mises en oeuvre tant au niveau du code source
que du code machine.
L'efficacité des mécanismes matériels pour l'exploitation
de la localité des références mémoire et du parallélisme,
tant au niveau multiprocesseurs qu'instruction (``Instruction
Level Parallelism''), dépend très fortement de la structure
des programmes. Cette structure est imposée par le
programmeur mais comporte des degrés de liberté que des
techniques logicielles, appelées optimisations de code,
peuvent exploiter pour augmenter la performance des
applications. Ces optimisations de code sont fondées sur des
transformations de programmes, qui respectent la sémantique
des codes, mais réorganisent les calculs pour une meilleure
exploitation d'une architecture donnée.
Figure 1: Organisation d'un
compilateur.
|
|
Les transformations de code destinées à l'amélioration de
performances peuvent intervenir à plusieurs étapes dans un
processus de compilation. La figure 1 montre l'organisation
générale d'un compilateur. Des transformations de code
peuvent être effectuées aussi bien au niveau du code source
qu'au niveau du code machine.
Les optimisations effectuées au niveau du code machine
sont principalement les optimisations ``Peephole'', qui
consistent à remplacer des séquences d'instructions par des
séquences plus rapides, et surtout l'application des
techniques d'ordonnancement de code. Cet ordonnancement doit
prendre en compte les caractéristiques fines de
l'architecture telles que le nombre de registres disponibles,
l'usage des ressources des processeurs, etc.
Par exemple, pour l'exploitation du parallélisme
d'instructions au niveau logiciel, les méthodes les plus
simples se restreignent à l'exploitation du parallélisme
entre les instructions d'un même bloc de base
. Cependant, le
nombre limité d'instructions dans un bloc de base réduit
l'efficacité de ce type de techniques. En pratique, surtout
dans le cas des boucles, il faut extraire le parallélisme
entre des instructions de plusieurs blocs de base, par
exemple en utilisant la technique du pipeline logiciel. Cette
technique, fondée sur l'exploitation du parallélisme
disponible entre les instructions d'itérations différentes,
consiste à segmenter le code des boucles d'une manière
similaire à celle utilisée par les pipelines matériels.
Au niveau du code source, les transformations de
programmes utilisent toutes les informations sémantiques
disponibles tant au niveau du contrôle de flot que de l'usage
des variables. A ce niveau, des réorganisations majeures du
code peuvent être effectuées telles que par exemple le
remplacement de l'appel d'une procédure par le corps de
celle-ci ("inlining"). C'est aussi sur le code source que
l'on peut appliquer les techniques de parallélisation
automatique et les méthodes d'optimisation de la localité.
Par exemple, la performance d'une hiérarchie mémoire dépend
très fortement des caractéristiques de localité des accès aux
données effectués par un programme. La prise en compte de la
hiérarchie mémoire par un compilateur consiste à considérer
les trois aspects fondamentaux suivants :
- Détection et estimation de la
localité :
- La détection de la localité est fortement liée au
calcul des dépendances de données. En effet, si une
dépendance existe, alors il y a réutilisation de données.
Le deuxième aspect de cette question est de déterminer la
proportion de références mémoire qui peuvent être évitées
par l'exploitation effective de cette localité.
- Exploitation de la
localité :
- L'exploitation de la localité consiste essentiellement
à déterminer le niveau de la hiérarchie mémoire qui tirera
parti de la localité présente et à adapter la génération de
code en conséquence.
- Optimisation de la
localité :
- Ces transformations de code ont pour but de
restructurer les calculs pour permettre l'exploitation
effective, par un niveau choisi de la hiérarchie, de la
localité présente.
Il existe un nombre très important de transformations du
code source pouvant être utilisées pour améliorer le
comportement de la hiérarchie mémoire sur une application
et/ou la paralléliser [BGS94]. La plupart de ces optimisations
s'appliquent aux boucles. Parmi celles-ci on peut
citer :
- Blocage de boucles :
- Dans le cadre de l'optimisation de la localité, cette
transformation permet de diviser l'espace d'itérations en
pavés de telle sorte que les données réutilisées puissent
être contenues dans un niveau de la hiérarchie
mémoire.
- Distribution de boucle :
- Les instructions d'une boucle sont réparties dans
plusieurs boucles ayant le même espace d'itération que
l'original. Cette transformation est utilisée pour diminuer
la pression sur les registres ou extraire des calculs
parallèles d'une boucle séquentielle.
- Fusion de boucles :
- Les instructions de deux boucles sont fusionnées dans
une seule boucle. Elle est par exemple utilisée pour
améliorer les réutilisations de données.
- Dépliage de boucle :
- Cette transformation consiste à répliquer le corps de
la boucle. Cette transformation, toujours légale, permet de
diminuer le coût de gestion de la boucle et augmente le
parallélisme d'instructions potentiellement exploitable par
les processeurs.
- Strip-mining :
- Le ``strip-mining'' découpe l'espace d'itérations de
boucle en blocs. Il permet d'ajuster la granularité des
opérations dans le cas de la parallélisation ou de la
vectorisation.
Ces transformations sont aujourd'hui relativement bien
comprises individuellement. Le challenge est aujourd'hui de
maîtriser l'interaction de toutes ces transformations et leur
impact sur les performances. D'autres approches s'intéressent
à la compilation dynamique (changement à l'exécution de
binaire) ou à la compilation itérative (boucle de retour dans
la compilation).