• Aucun résultat trouvé

1.3 Génie Logiciel, refactorisation et optimisation

1.3.3 Optimisation logicielle

Lo s u il est pas p i o dial de dispose d u p og a e d elopp le plus « proprement » possible, il est souvent possible de réaliser des compromis entre la qualité du code et les performances de l appli atio est le as des appli atio s essita t les eilleu es performances possibles). En effet, la majeure partie des optimisations qui peuvent être réalisées vont avoir tendance à complexifier le code et à le rendre moins facilement compréhe si le et odifia le à l a e i . Il est donc nécessaire de bien mesurer le gain de chaque optimisation de façon à ne conserver que les optimisations significatives.

Pour cela, e a o t du t a ail d opti isatio , il est essai e d ta li p is e t la liste des goulots d t a gle e t de l appli atio . O , pa i tous les outils dispo i les pou le d eloppeu , l i tuitio est généralement pas le plus effi a e. Ai si, da s l ou age « Refactoring: Improving the Design of Existing Code » de Fowler (M. Fowler 1999), Garzaniti appo te ue lo s de l opti isatio d u e appli atio , u e tude de p ofilage a ait o t ue le plus g os goulot d t a gle e t tait la atio de g a des hai es de a a t es jus u à 000 caractères) utilisées pour les entrées-so ties alo s e u au u d eloppeu su le p ojet a ait a a ette h poth se lo s des fle io s su les a es d opti isatio s possi les. Pou et ou e effi a e e t les goulots d t a gle e t, il est donc o ligatoi e d utilise des outils esu a t p is e t les pe fo a es des diff e tes pa ties de l appli atio : des p ofileu s. Lo s ue les fo tio s de l appli atio les plus gourmandes en termes de temps de calcul sont connues, il est possible de réaliser diff e tes fo es d opti isatio .

Élimination des sous-expressions communes

Une première optimisation possible revient à supprimer certaines parties de code dupliquées dans des expressions différentes et à les factoriser dans une nouvelle expression

50 (Cocke 1970). Dans le Code 1-1 pa e e ple, l e p essio pe etta t le al ul de la TVA su un produit coutHT * tauxTVA est utilisé aussi bien pour le calcul du prix total coutTTC que pour le calcul du remboursement sur la TVA remboursementTVA. Il est donc possi le d e t ai e e al ul da s u e a ia le TVA puis d utilise ette a ia le e lieu et place de la sous-expression.

Mise en place, ette te h i ue d opti isatio pe et, lorsque le compilateur est incapable de la mettre en place seul, de diminuer les calculs à effectuer (dans notre exemple, la valeur de la TVA est ai si al ul e u u e seule fois ais aussi pa fois de la ifie le ode les calculs sont plus clairs avec l i t odu tio d une variable intermédiaire). L i o ie t ajeu de ette te h i ue est u elle essite g ale e t l utilisatio de egist es supplémentaires pour stocker les variables intermédiaires. Ceci peut poser de gros problèmes, en particulier sur des architectures où le nombre de registres est très limité telles celles des GPU.

// Avant

coutTTC = coutHT + coutHT * tauxTVA;

remboursementTVA = tauxRemboursmentTVA * coutHT * tauxTVA;

// Après

TVA = coutHT * tauxTVA coutTTC = coutHT + TVA;

remboursementTVA = tauxRemboursmentTVA * TVA;

Code 1-1 : Exemple d'élimination d'une sous-expression commune. Échange de boucles

Une bonne partie des optimisations les plus courantes traitent des boucles car celles-ci

peu e t p te u t s g a d o e de fois les es op atio s. L ha ge de ou les

fait, o e so o l i di ue, pa tie de ces optimisations et traite du cas des boucles i i u es. Ai si, lo s ue deu ou les so t i i u es l u e da s l aut e et u il est possi le, sa s odifie le o po te e t fi al de l appli atio , de les i e se , il est pa fois très avantageux de le faire (Allen et Kennedy 2001). En effet, si cette inversion permet d a de au do es da s u ordre plus proche de celui dans lequel elles sont stockées, les a is es de a he peu e t e d e l op atio eau oup plus apide. Le is ue d u e telle optimisatio est ue l o d e i itiale e t p u peut a oi u plus g a d se s du poi t de vue applicatif. Il peut donc arriver que la compréhension du code soit alors plus difficile.

51 Da s l e e ple du Code 1-2, la dista e à l o igi e est al ul e pou ha ue poi t d u tableau à deux dimensions (les coordonnées des points correspondent à leurs indices dans le tableau). La boucle extérieure parcourt les coordonnées en x et la boucle intérieure les coordonnées en y. Or, le tableau est stocké en mémoire de telle faço ue l o a de d a o d à l i di e : distanceOrigine[ y ][ x ]. Le tableau est donc stocké ligne par ligne : les cellules possédant une même valeur de y se trouvant les unes à la suite des autres, de 0 à maxX - 1. Le parcours choisi ici ne reproduit donc pas ce stockage en pa ou a t tout d a o d toutes les ellules a a t l i di e à puis toutes elles a a t l i di e à , etc. En inversant les deux boucles, on conserve un comportement similaire (la distance calculée sera la même) mais les accès seront faits dans l o d e du stockage en mémoire.

// Avant

for ( int x = 0; x < maxX; ++x ) {

for ( int y = 0; y < maxY; ++y )

{

distanceOrigine[ y ][ x ] = sqrt( x * x + y * y );

} }

// Après

for ( int y = 0; y < maxY; ++y ) {

for ( int x = 0; x < maxX; ++x )

{

distanceOrigine[ y ][ x ] = sqrt( x * x + y * y );

} }

Code 1-2 : Exemple d'échange de boucle. Fragmentation de boucles

La f ag e tatio de ou les s appli ue lo s u u e ou le o tie t, da s le ut de g e u ou plusieurs cas particuliers en début ou en fin de boucle, des instructions supplémentaires. Il est alors parfois possible de supprimer es i st u tio s au p i d u e s pa atio des as pa ti ulie s et des as g au , p i ipale e t l i itialisatio du p e ie l e t ou u traitement particulier sur le dernier élément (Cannings, Thompson et Skolnick 1976). Dans le Code 1-3, l o je tif de e pli u ta leau tabDecale a e le o te u d u p e ie tableau (tabBase) décalé de trois indices est réalisé e affe ta t, pou l i di e i, la valeur de l i di e i + 3. Afin de correctement accéder aux trois premiers éléments du premier ta leau lo s ue l o affe te les t ois de niers éléments du tableau résultat, le al ul d un

52 modulo est nécessaire su l e se le de la ou le. En séparant le cas des sept premiers l e ts de elui des t ois de ie s, il est possi le de se o te te d u e additio i + 3) dans les sept premiers cas et d u e soust a tio i - 7) dans les trois derniers. Si le gain de ette opti isatio est assez ide t ta t do ue le o e d op atio s effe tu es diminue, li o ie t ajeu , la complexification du code, l est uasi e t auta t. En effet, là où il était assez aisé de comprendre que le code donné en exemple correspondait à un décalage de trois éléments dans la première implémentation, ceci est beaucoup plus difficile pour la seconde implémentation et des commentaires seraient nécessaires.

// Avant for ( int i = 0; i < 10; ++i ) { tabDecale[ i ] = tabBase[ ( i + 3 ) % 10 ]; } // Après for ( int i = 0; i < 7; ++i ) { tabDecale[ i ] = tabBase[ i + 3 ]; } for ( int i = 7; i < 10; ++i ) { tabDecale[ i ] = tabBase[ i - 7 ]; }

Code 1-3 : Exemple de fragmentation de boucles. Boucle unswitching

L opti isatio de « boucle unswitching » correspond à la suppression d u a he e t dans une boucle. Pour que cela soit possible, ce branchement ne doit pas évoluer dynamiquement dans la boucle. Il est alors possible de t ai e le branchement de la boucle en dupliquant le code de la boucle dans chaque branche. Cette optimisation permet de passe d un test par itération à un unique test. Le problème majeur de cette optimisation est la duplication du code de la boucle. Si une modification est nécessaire dans ce code, il sera alors obligatoire de la répercuter dans les deux copies.

Le Code 1-4 calcule le coût total toutes taxes comprises (TTC) d u e o a de. Pou ela, u e solutio est de pa ou i tous les p oduits et d ajoute au oût total le coût TTC du p oduit ou a t. Mais l o peut i agi e ue les ta es a ie t selo le t pe de o a de (et non pas selon le type de produit, sans quoi cette optimisation est impossible). On a alors u test au sei de la ou le ui olue pas au ours de celle- i. L opti isatio o siste

53 donc à commencer par ce test et à dupliquer la boucle dans les deux branches du if. Le code des deux boucles perd alors toute présence de tests.

// Avant float coutTotal = 0; for ( int i = 0; i < 10; ++i ) { if ( commandeAEmporter == true ) { coutTotal += 1.05 * coutHT[ i ]; } else { coutTotal += 1.19 * coutHT[ i ]; } } // Après float coutTotal = 0; if ( commandeAEmporter == true ) { for ( int i = 0; i < 10; ++i ) { coutTotal += 1.05 * coutHT[ i ]; } } else { for ( int i = 0; i < 10; ++i ) { coutTotal += 1.19 * coutHT[ i ]; } }

Code 1-4 : Exemple de boucle unswitching. Invariant de boucle

L i a ia t de ou le est u e des opti isatio s les plus si ples à t ou e et à ett e e œu e. C est la aiso pou la uelle les o pilateu s so t t s pe fo a ts pou la ett e automatiquement en place. Mais il est toujours intéressant de savoir de quoi il s agit lo s ue le o pilateu e la d te te pas seul. Le ut est d e t ai e des ou les tous les al uls ui e varient pas au cours de celle- i. E les e t a a t, o di i ue le o e d i st u tio s et cela pe et e pa fois d a l e les a s aux données en conservant la constante dans un registre (Aho, Sethi et Ullman 1986). L i o ie t est, o e ela tait déjà le cas pour l li i atio de sous-expression commune qui est une optimisation très proche de celle-ci, que ce mécanisme peut augmenter le nombre de registres utilisés.

54 Dans le Code 1-5, le al ul du tau de TVA o e ou s e a ie ja ais au œu de la boucle. Il est donc possible de sortir ce calcul de la boucle et de diminuer ainsi le nombre de calculs effectués.

// Avant

for ( int i = 0; i < 10; ++i ) {

coutTotal += coutHT[ i ] +

coutHT[ i ] * tauxTVA * remboursementTVA; }

// Apres

tauxTVANonRembourse = tauxTVA * remboursementTVA;

for ( int i = 0; i < 10; ++i ) {

coutTotal += coutHT[ i ] +

coutHT[ i ] * tauxTVANonRembourse; }

Code 1-5 : Exemple d'invariant de boucle. Fission / fusion de boucles

La fission de boucles (la fusion de boucles est le mécanisme inverse) consiste à séparer dans deu ou les deu o posa tes d u e e ou le. Bien u il soit alors nécessaire de alise deu fois le pa ou s au lieu d u , da s e tai s cas, lorsque le corps de la boucle est important, il est possible que les mécanismes de cache soient plus performants en réalisant les deux boucles séparément (Kennedy et Allen 2001). Da s l e e ple du Code 1-6, deux tableaux (tabA et tabB) sont manipulés dans une même boucle. La fission de la boucle

o siste à dupli ue la ou le pou ue ha u e e s o upe ue d u ta leau.

// Avant for ( int i = 0; i < 10; ++i ) { tabA[ i ] = 1; tabB[ i ] = 2; } // Après for ( int i = 0; i < 10; ++i ) { tabA[ i ] = 1; } for ( int i = 0; i < 10; ++i ) { tabB[ i ] = 2; }

55

Déroulement de boucle

Le déroulement de boucle (unrolling en anglais) consiste à réécrire une boucle entièrement ou en partie de manière séquentielle (Aho, Ullman et Biswas 1977). L o je tif est i i de di i ue le o e d i st u tio s li es au o t ôle de la ou le. Le p i ipal i o ie t est une complexification importante du code avec un éloignement du code de la p o l ati ue tie ai si u u e uta le do t la taille peut sig ifi ati e e t augmenter.

Dans le Code 1-7, qui présente un exemple de déroulement partiel, la boucle initiale parcourt les aleu s du ta leau u e à u e pou i itialise la aleu d u poi teu à NULL. Le déroulement de la boucle est effectué en réalisant à chaque pas dans la boucle trois affectations et en incrémentant de 3 la variable i à chaque itération. Cette exemple montre bien les difficultés de maintenance qui peuvent survenir. En effet, si le tableau évolue vers une taille non multiple de 3, il sera nécessaire de modifier le déroulement en rajoutant une fragmentation de boucle pour gérer les derniers éléments par exemple.

// Avant for ( int i = 0; i < 300; ++i ) { tabPtr[ i ] = NULL; } // Après for ( int i = 0; i < 300; i += 3 ) { tabPtr[ i ] = NULL; tabPtr[ i + 1 ] = NULL; tabPtr[ i + 2 ] = NULL; }

Code 1-7 : Exemple de déroulement de boucle. Métaprogrammation template

La métaprogrammation template tire parti du mécanisme des templates en C++ pour réaliser une partie des instructions lors de la phase de compilation plutôt que lors de l e utio du p og a e. E alua t u e e p essio te plate, il est ai si alo s possi le de réaliser le calcul de constantes de manière à éliminer ces calculs lo s de l e utio ou de réaliser des opérations sur des types afin de faire de la vérification de type par exemple (Touraille, Traoré et Hill 2010). Si cette technique peut permettre de grandement améliorer les te ps d e utio s de l appli atio , elle peut gale e t g a de e t o ple ifie le ode de l appli atio . Pa ailleu s, l e se le des al uls alis s à la o pilatio fo t croître

56 le temps total de compilation qui peut ainsi devenir très significatif et rédhibitoire dans certains cas.

L e e ple le plus lassi ue d utilisatio de la tap og a atio te plate pour le calcul de constantes est elui du al ul d u e fa to ielle oi Code 1-8). De manière classique, la factorielle de n est calculée de manière récursive en retournant 1 si n vaut 0 et n * factorielle( n - 1 ) si o . L utilisatio du a is e de p og a atio te plate i pli ue de de a de au o pilateu d alue u e e p essio à la o pilatio . Plusieurs mécanismes sont possibles parmi lesquels on trouve l aluatio lo s de

l i itialisatio d u u ateu valeurda s ot e as d u e u atio o o e

dans notre cas). Pour différentier les appels selon les valeurs de n, l id e est d a oi u e spécialisation template pour chaque valeur différente. La fonction Factorielle est donc paramétrée par cette valeur n. Dans le cas général (n différent de 0), l u ateu est do initialisé à la valeur de n ultipli pa la aleu de l u ateu pou la lasse Factorielle paramétré par n - 1 : n * Factorielle< n - 1 >::valeur. Lorsque n vaut 0, on spécialise la fonction template pour initialiser valeur à 1. L appel Factorielle< 4 >::valeur alise alo s l e se le des calculs à la compilation.

// Avant

int factorielle( int n )

{ if ( n == 0 ) { return 1; } else { return n * factorielle( n - 1 ); } } // Appel : factorielle( 4 ) ; // Après template < int n > struct Factorielle {

enum { valeur = n * Factorielle< n - 1 >::valeur }; }; template <> struct Factorielle< 0 > { enum { valeur = 1 }; };

57

// Appel :

Factorielle< 4 >::valeur ;

Code 1-8 : Exemple de programmation template Accélération de calcul à base de GP-GPU

A e l o je tif d a lio e les pe fo a es de la PGMS su des a hi es de ta les, l utilisatio de GP-GPU s est apide e t i pos e. Ce i dit, les sp ifi it s de l a hite tu e des GPU i pa te t fo te e t su les thodes d opti isatio s. Ai si, si e taines des optimisations précédentes sont toutes aussi valables sur GPU que sur CPU (déroulement de boucle, boucle unswitching pa e e ple , e tai es peu e t t e o t e p odu ti es. C est le as des opti isatio s essita t l utilisatio de a ia les supplémentaires pour être implémentées (invariant de boucle et élimination des sous-expressions par exemple). En effet, le nombre de registres disponibles pour chaque thread exécuté est très limité. Pour pouvoir tirer parti du nombre maximum de threads ordonnancés en même temps, il est ainsi nécessaire de ne pas utiliser plus de trente-deux registres pour chaque thread. Si ce nombre est d pass , il est alo s p o a le ue les pe fo a es de l appli atio di i ue t. Il a ai si parfois être nécessaire, pour accél e les al uls, de alise l opti isatio i e se de l i a ia t de ou le ou de l li i atio des sous-expressions en dupliquant des calculs afin de diminuer le nombre de variables et par conséquent le nombre de registres utilisés.

Les performances liées à la mémoire du GPU, que ce soit lors de la copie par le CPU ou lors de l a s à u e a ia le par les threads, impactent également fortement sur les opti isatio s à alise . U e i po ta te pa tie du t a ail d opti isatio su GPU a he he à diminue l e se le des oûts li s aux accès mémoire. Pour cela, plusieurs solutions e iste t. Tout d a o d, il est p f a le, auta t que possible, de réaliser les accès à la oi e de faço oales e te. De a i e si plifi e, il s agit de fai e a de les threads à des zones mémoire se succédant (cette notion est reprise plus en détail dans la partie 3.2.1 - Mémoire globale (global memory) . Il est gale e t possi le d utilise les diff e ts types de mémoires disponibles sur un GPU : mémoire constante, mémoire de texture et surtout la mémoire partagée dont les caractéristiques sont différentes de la mémoire globale (ces caractéristiques sont détaillées dans la partie 3.2 - Mémoire des GPU). Enfin, lors de la copie de quantité de données très importante entre la mémoire du CPU et celle du GPU, il est possible de réaliser celle-ci de façon asynchrone afin de pouvoir utiliser le temps

58 de copie pour la réalisation daut es calculs (voir partie 3.3.3 - Utilisation des flux asynchrones (stream)).

Analyse de l’activité

De a i e plus o ple e, l opti isatio d u p o essus peut gale e t t e alis e e t aita t diff e e t l i fo atio . Il est ai si possi le, da s u espa e dis tis , de e pas t aite l e se le des ellules ais u i ue e t elles do t l i fo atio doit olue . L id e est donc de diminuer les calculs en focalisant ces calculs sur les éléments les plus actifs de la simulation. Ce o ept de l a ti it est fo te e t li au app o hes permettant de faire évoluer une simulation. Balci a proposé une classification des processus de simulation en quatre approches distinctes (Balci 1988). Si les deux premières approches sont sans liens a e la otio d a ti it , l app o he a ti it et l app o he e t ois phases se asent sur l a ti it pou fai e olue les si ulatio s.

L app o he a ti it , introduite par Buxton et Laski dans le langage CSL (Buxton et Laski 1962), est fondée su la s utatio d a ti it et est également appelée « approche deux phases ». Elle se décompose fort logiquement en deux phases : la gestion du temps dans la simulation pour la première et la scrutation des activités pour la seconde. Une activité est alors d fi ie d u e pa t pa u e ou plusieu s o ditio s u il est essai e de e pli pou ue ette a ti it soit alide et d aut e pa t pa u e ou plusieu s op atio s à effe tue lorsque celle-ci est valide.

L app o he t ois phases, p opos e pa To he (Tocher 1963), a été conçue pour améliorer les pe fo a es de l app o he p de te. Pou pe ett e de si plifie la s utatio d a ti it , cette nouvelle approche dissocie les activités inconditionnelles des activités conditionnelles. De cette façon, il est possible de traiter séparément les activités inconditionnelles et la s utatio de l a ti it à proprement parler. Le traitement des activités inconditionnelles est ainsi effectué en premier lieu en ne tenant compte que de l ho loge de la si ulatio . Da s u se o d te ps, la s utatio de l a ti it pe et de déterminer quelles activités conditionnelles sont valides à cet instant de la simulation. Si l utilisatio de telles app o hes est pas la solutio à l e se le des p o l es de performance des simulations, son application à des systèmes où la majorité de l a ti it du

59 od le se t ou e da s u e zo e est ei te pe et d ite l aluatio de l e se le du domaine à chaque pas de temps (Coquillard et Hill 1997) (Muzy, Nutaro, et al. 2008).

Documents relatifs