ele784-cours8-optimisation

20
Cours # 8 ELE784 - Ordinateurs et programmation système 1 Cours # 8 ELE784 - Ordinateurs et programmation système 1 Cours #8 Optimisation de code ELE-784 Ordinateurs et programmation système Bruno De Kelper Site internet : http://www.ele.etsmtl.ca/academique/ele784/ Cours # 8 ELE784 - Ordinateurs et programmation système 2 Plan d’aujourd’hui 1. Optimisation de la performance du code (chap. 5) 1. Capacités et limitations d’optimisation des compilateurs 2. Exprimer la performance du code 3. Élimination des inefficacités des boucles 4. Réduire les appels de fonctions 5. Élimination des références inutiles à la mémoire 6. Compréhension des processeurs modernes 7. Réduction de la charge des boucles 8. Conversion à l’utilisation de pointeurs 9. Améliorer le parallélisme 10. Résumé des résultats d’optimisation actuels 11. Prédiction des branchements et pénalités 12. Performance des accès à la mémoire 13. Techniques d’amélioration des performances Réf. : Computer Systems – A Programmer’s Perspective, R.E. Bryant, D.R. O’Hallaron, Prentice Hall, chap. 5.

Upload: chfakht

Post on 31-Jan-2016

218 views

Category:

Documents


4 download

TRANSCRIPT

Page 1: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 1

Cours # 8 ELE784 - Ordinateurs et programmation système 1

Cours #8Optimisation de code

ELE-784Ordinateurs et programmation système

Bruno De Kelper

Site internet :http://www.ele.etsmtl.ca/academique/ele784/

Cours # 8 ELE784 - Ordinateurs et programmation système 2

Plan d’aujourd’hui

1. Optimisation de la performance du code (chap. 5)1. Capacités et limitations d’optimisation des compilateurs2. Exprimer la performance du code3. Élimination des inefficacités des boucles4. Réduire les appels de fonctions5. Élimination des références inutiles à la mémoire6. Compréhension des processeurs modernes7. Réduction de la charge des boucles8. Conversion à l’utilisation de pointeurs9. Améliorer le parallélisme10. Résumé des résultats d’optimisation actuels 11. Prédiction des branchements et pénalités12. Performance des accès à la mémoire13. Techniques d’amélioration des performances

Réf. : Computer Systems – A Programmer’s Perspective, R.E. Bryant, D.R. O’Hallaron, Prentice Hall, chap. 5.

Page 2: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 2

Cours # 8 ELE784 - Ordinateurs et programmation système 3

Optimisation de la performance du code

- La performance du code est dictée dans une large mesure par le choix d’algorithmes et des structures de données qui les supportent.

- Mais aussi, grâce à des techniques spéciales, les compilateurs et matériels modernes sont conçus pour tirer le maximum de performance des algorithmes utilisés.

- Malgré ça, les choix faits par le programmeur ont un impact sur la capacitéd’optimisation du compilateur et du matériel.

- Des changements anodins dans la façon d’écrire le code peuvent avoir parfois un impact considérable sur la facilité qu’aura le compilateur a en produire un code performant.

- Ainsi, le programmeur doit aider le compilateur car celui-ci ne peut effectuer que des transformations assez limitées sur le code.

- Les compilateurs utilisent des techniques d’optimisation dites :

Indépendantes de la machine

Dépendantes de la machine

Qui peuvent s’appliquées à n’importe quelle machine

Qui dépendent des particularités de bas niveau de la machine

Cours # 8 ELE784 - Ordinateurs et programmation système 4

Optimisation de la performance du code

- Les processeurs modernes utilisent des techniques sophistiquées pour exécuter le code d’un programme, tels que le parallélisme d’exécution et l’exécution hors séquence.

- Et le programmeur doit comprendre ces principes de fonctionnement pour tirer le maximum de performance de son code.

1.1 - Capacités et limitations d’optimisation des compilateurs

- Les compilateurs modernes exploitent à chaque occasion les opportunités de :- Simplifier les expressions.- Utiliser un même traitement à plusieurs endroits.- Réduire le nombre de fois qu’un traitement est effectué.- Etc.

- Les capacités d’optimisation des compilateurs sont limitées par :

1 – L’exigence qu’ils ne doivent jamais modifier le comportement du code.

2 – Leur compréhension limitée du comportement du code et le contexte dans lequel il sera utilisé.

3 – Le besoin d’effectuer la compilation rapidement.

Page 3: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 3

Cours # 8 ELE784 - Ordinateurs et programmation système 5

Optimisation de la performance du code

1.1 - Capacités et limitations d’optimisation des compilateurs

Par exemple :

void AddXY_1 (int *Xp, int *Yp) {*Xp += *Yp;*Xp += *Yp;

}

void AddXY_2 (int *Xp, int *Yp) {*Xp += 2 * (*Yp);

}

Ces deux fonctions semblent effectuer exactement le même travail.

1er cas :

5A Xp 3 BYp

AddXY_1 AddXY_2

11A 11A

2ième cas :A5

Xp

AddXY_1 AddXY_2

20A 15A

Yp

Donc, lorsque ces deux fonctions reçoivent des pointeurs vers la même variable, elles ne produisent pas le même résultat.

Phénomèned’alias de mémoire"memory aliasing"

Version optimisée ?

Cours # 8 ELE784 - Ordinateurs et programmation système 6

Optimisation de la performance du code

1.1 - Capacités et limitations d’optimisation des compilateurs

Par exemple :

int AddF_1 (int X) {return (f(X) + f(X) + f(X) + f(X));

}

int AddF_2 (int X) {return (4*f(X));

}

Ces deux fonctions semblent effectuer exactement le même travail.

Donc : 5 AddF_1

À cause des "effets secondaires" de f(), ces deux fonctions reçoivent la même valeur en entrée, mais ne produisent pas le même résultat.

int n = 0;

int f (int A) {n++;return (A+n);

}

Mais si : Alors, f() a des "effets secondaires".

30

5 AddF_2 24

- Dans les deux cas, "memory aliasing" et "effets secondaires", ainsi que bien d’autres, le compilateur ne peut pas optimiser sans changer le comportement du code.

Version optimisée ?

effet secondaire

Page 4: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 4

Cours # 8 ELE784 - Ordinateurs et programmation système 7

Optimisation de la performance du code

1.2 - Exprimer la performance du code

- La mesure de performance est un outil essentiel pour identifier les parties de code qui doivent être optimisées ainsi que pour déterminer l’impact des choix d’optimisation.

- La performance du code peut être mesurée de bien des façons, mais de celles-ci, le nombre de cycles par élément (CPE) est particulièrement utile pour mesurer le code répétitif tel que les boucles.

void Sum1 (int n) {for (i = 0; i < n; i++)

c[i] = a[i] + b[i];}

void Sum2 (int n) {for (i = 0; i < n; i += 2) {

c[i] = a[i] + b[i];c[i+1] = a[i+1] + b[i+1];

}}

Par exemple :

0 40 80 120 160 2000

200

400

600

800

80.0 + 4.0*n

83.5 + 3.5*n

Utilise le déroulement partiel de boucle

Le CPE a l’avantage d’être indépendant du matériel

CPE = 3.5

CPE = 4.0

Cours # 8 ELE784 - Ordinateurs et programmation système 8

Optimisation de la performance du code

1.2 - Exprimer la performance du code

void Operation1 (vec_ptr v, data_t *dest) {int i;

*dest = INIT;for (i = 0; i < vec_len(v); i++) {

data_t val;get_element (v, i, &val);*dest = *dest OPER val;

}}

Par exemple : Soit la fonction Operation1() qui accumule les données d’un vecteur selon l’opération OPER.

typedef struct {int len;data_t *data;

} vec_rec, *vec_ptr

#define INIT 0#define OPER +

#define INIT 1#define OPER *

et

ou

143.0160.0

*

33.2541.86

*

Point flottantEntier

33.2531.25Option –O241.4442.06Pas d’optimisation

++Optimisation

Mesure du CPE : - L’option de compilation –O2 améliore la performance d’environ 25 %.

- Les multiplications en point-flottant consomment beaucoup plus de temps (presque 4 fois plus).

Page 5: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 5

Cours # 8 ELE784 - Ordinateurs et programmation système 9

Optimisation de la performance du code

1.3 - Élimination des inefficacités des boucles

- Dans l’exemple précédent, le code exécute la fonction vec_len() à chaque itération, bien que la longueur du vecteur ne change pas.

- Une 1ière optimisation serait donc de déterminer la longueur du vecteur avant la boucle afin d’éviter l’appel trop fréquent à la fonction vec_len().

void Operation2 (vec_ptr v, data_t *dest) {int i;int length = vec_len(v);

*dest = INIT;for (i = 0; i < length; i++) {

data_t val;get_element (v, i, &val);*dest = *dest OPER val;

}}

135.0143.0

*

21.2533.25

*

Point flottantEntier

21.1522.61Operation2()33.2531.25Operation1()

++Optimisation

Mesure du CPE :

- Cette optimisation améliore la performance d’environ 33 %.

- Ce type d’optimisation s’appelle "déplacement de code" (code motion).

- Le compilateur tente d’utiliser le "déplacement de code" , mais à cause d’effet secondaire potentiel, il évite de changer les appels de fonction.

Cours # 8 ELE784 - Ordinateurs et programmation système 10

Optimisation de la performance du code

1.3 - Élimination des inefficacités des boucles

Autre exemple : Les fonctions Minus1() et Minus2() convertissent une chaine de caractères en minuscule.

void Minus1 (char *S) {int i;

for (i = 0; i < strlen(S); i++) {if ((S[i] >= ‘A’) && (S[i] <= ‘Z’))

S[i] -= (‘A’ – ‘a’);}

void Minus2 (char *S) {int i;int length = strlen(S);

for (i = 0; i < length; i++) {if ((S[i] >= ‘A’) && (S[i] <= ‘Z’))

S[i] -= (‘A’ – ‘a’);}

void strlen (const char *S) {int len = 0;

while (*S != ‘/0’) {S++;len++;

}return len;

}

0 0.5 1.0 1.5 2.0 2.5 3.00

40

80

120

160

200

x105

Mesure du CPE :

Minusc

ule1

Minuscule2

- La fonction Minus1a un comportement quadratique (àéviter absolument).

- La fonction Minus2a un comportement linéaire.

Doit trouver la fin de la chaine pour déterminer sa longueur.

Longueur de la chaineLongueur de la chaine

Page 6: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 6

Cours # 8 ELE784 - Ordinateurs et programmation système 11

Optimisation de la performance du code

1.4 - Réduire les appels de fonctions

- Les appels de fonctions induisent une charge supplémentaire et empêchent la plupart des optimisations.

Par exemple :

void Operation3 (vec_ptr v, data_t *dest) {int i;int length = vec_len(v);data_t *data = get_first(v);

*dest = INIT;for (i = 0; i < length; i++)*dest = *dest OPER data[i];

}

- La fonction Operation2 () récupère chaque élément du vecteur àl’aide de la fonction get_element ().

- Mais, en récupérant les éléments par un accès direct au vecteur, comme dans la fonction Operation3 () ci-dessous :

117.0135.0

*

9.0021.25

*

Point flottantEntier

8.006.00Operation3()21.1522.61Operation2()

++Fonction

Mesure du CPE :

- Cette optimisation améliore la performance par un facteur de 3.5

Cours # 8 ELE784 - Ordinateurs et programmation système 12

Optimisation de la performance du code

1.4 - Réduire les appels de fonctions

- Les appels de fonctions induisent une charge supplémentaire et empêchent la plupart des optimisations.

Par exemple :

void Operation3 (vec_ptr v, data_t *dest) {int i;int length = vec_len(v);data_t *data = get_first(v);

*dest = INIT;for (i = 0; i < length; i++)*dest = *dest OPER data[i];

}

- La fonction Operation2 () récupère chaque élément du vecteur àl’aide de la fonction get_element ().

- Mais, en récupérant les éléments par un accès direct au vecteur, comme dans la fonction Operation3 () ci-dessous :

117.0135.0

*

9.0021.25

*

Point flottantEntier

8.006.00Operation3()21.1522.61Operation2()

++Fonction

Mesure du CPE :

- Cette optimisation améliore la performance jusqu’à un facteur de 3.7

Page 7: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 7

Cours # 8 ELE784 - Ordinateurs et programmation système 13

Optimisation de la performance du code

1.5 - Élimination des références inutiles à la mémoire

- L’accès à une variable en mémoire implique que le processeur doit effectuer un cycle de lecture et un cycle d’écriture pour récupérer la donnée et rafraichir la variable modifiée.

- À cause du phénomène de "memory aliasing", le compilateur ne peut pas toujours optimiser ce type d’utilisation d’une variable en lui attribuant un registre.

Par exemple :

void Operation4 (vec_ptr v, data_t *dest) {int i;int length = vec_len(v);data_t *data = get_first(v);data_t x = INIT;

for (i = 0; i < length; i++)x = x OPER data[i];

*dest = x;}

- La fonction Operation3 () accumule le résultat de son calcul dans la variable pointée par dest.Mais

5.00117.0

*

4.009.00

*

Point flottantEntier

3.002.00Operation4()8.006.00Operation3()

++Fonction

Mesure du CPE :

- Cette optimisation améliore la performance jusqu’à un facteur de 3

- Et plus encore pour le cas de la multiplication à point-flottant.

Variable servant a accumuler le résultat

Cours # 8 ELE784 - Ordinateurs et programmation système 14

Optimisation de la performance du code

1.6 - Compréhension des processeurs modernes

- L’optimisation en profondeur du code requière une compréhension en détail des particularités du processeur sur lequel le code sera exécuté.

- Les processeurs modernes incorporent de nombreux mécanismes qui leur permettent d’exécuter le code plus rapidement.

Par exemple : - Utilisation de mémoire cache de données et d’instructions.- Utilisation d’un pipeline d’exécution.- Plusieurs unités fonctionnelles (unités de traitement).- Unités de lecture et d’écriture indépendantes.- Traitement "hors séquence".- Mécanisme de prédiction des branchements.- Mécanisme d’exécution spéculative.

- En fait, l’exécution réelle des instructions par le processeur ne ressemble pas du tout à la séquence exprimée par le code, même dans sa version "machine".

- Le processeur est capable d’exécuter plusieurs instructions en même temps (certains jusqu’à 80 instructions simultanément), tout en assurant que le code se comporte correctement.

Page 8: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 8

Cours # 8 ELE784 - Ordinateurs et programmation système 15

ExecutionExecution

Functionalunits

Instruction controlInstruction control

Integer/branch

FPadd

FPmult/div

Load Store

Instructioncache

Datacache

Fetchcontrol

Instructiondecode

Address

Instructions

Operations

PredictionOK?

DataData

Addr. Addr.

Generalinteger

Operation results

Retirementunit

Registerfile

Registerupdates

Optimisation de la performance du code

1.6 - Compréhension des processeurs modernes

Cours # 8 ELE784 - Ordinateurs et programmation système 16

Optimisation de la performance du code

1.6 - Compréhension des processeurs modernes

- La plupart des unités fonctionnelles du processeur sont équipes d’un pipeline qui leur permet de commencer l’exécution de la prochaine opération avant d’avoir terminé la précédente.

- Le temps d’exécution pour chaque opération est identifié par deux valeurs :Latence :

Nombre de cycle nécessaire pour exécuter l’opération du début jusqu’à la fin.

Temps d’émission :Nombre de cycle nécessaire entre deux opérations indépendantes consécutives.

Par exemple :

13Écriture (en cache)13Lecture (en cache)3838Division point-flottant25Multiplication point-flottant13Addition point-flottant3636Division entière14Multiplication entière11Addition entière

ÉmissionLatenceOpération

Les opérations de division ne sont pas misent en pipeline car elles ne sont pas jugées assez fréquentes.

Page 9: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 9

Cours # 8 ELE784 - Ordinateurs et programmation système 17

Optimisation de la performance du code

1.6 - Compréhension des processeurs modernes

- Pour commencer notre analyse, prenons en exemple la boucle de traitement de la fonction Operation4() (notre meilleur code jusqu’à présent) :

for (i = 0; i < length; i++)x = x OPER data[i];

En supposant que les variables sont stockées dans des registres du processeur (sauf pour data qui se trouve en mémoire-cache).

1 – Lecture de data[i] 2 – Multiplication de x et Reg3 – Incrément i4 – Compare i à length5 – Branche à 1 si plus petit

Reg = Data[i]x = x * Regi = i + 1cond = i - lengthGOTO 1 (cond = <)

Code initial

Détail d’exécution

Notation abrégée

Exemple :

Load

*

++

comp

goto

i

xReg

cond

ii

x

Exécution d’une itération

exécution simultanée

Durée du traitement

cycles

1

2

3

4

5

6

7

Cours # 8 ELE784 - Ordinateurs et programmation système 18

Optimisation de la performance du code

Load

*

++

comp

gotoxReg

cond

i

x

1

2

3

4

5

6

7

Load

++

comp

goto

i

Reg

i

condLoad

++

comp

goto

i

Reg

i

cond

*

x

*

8

9

10

11

- Les multiplications sont effectuées en séquence car elles requièrent le résultat de la précédente.

- Une nouvelle multiplication est commencée à tous les 4 cycles, donc CPE = 4.0

- Le traitement total des 3 itérations utilise 15 cycles.

En supposant des ressources illimitées

Traitement spéculatif

Page 10: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 10

Cours # 8 ELE784 - Ordinateurs et programmation système 19

Optimisation de la performance du code

Load

*

++

comp

gotoxReg

cond

i

x

1

2

3

4

5

6

7

Load

++

comp

goto

i

Reg

i

condLoad

++

comp

goto

i

Reg i

cond

*

x

*

8

9

10

11

- Les unités General Integeret Integer/Branch peuvent commencer une opération àchaque cycle.

- Donc, la 3ième incrémentation doit être décalée.

Avec des ressources limitées

Traitement spéculatif

Cours # 8 ELE784 - Ordinateurs et programmation système 20

Optimisation de la performance du code

1.6 - Compréhension des processeurs modernes

- En général, le processeur est limité par trois types de contraintes :

Dépendance de donnée

Ressources limitées

Prédiction de branchement

Force le retardement de certaines opérations jusqu’à ce que les opérants soient disponibles.

Limite le nombre d’opérations qui peuvent être exécutées en même temps.Limite le degré auquel le processeur peut prédire à l’avance le traitement à effectuer.

- L’optimisation en profondeur du code requière ainsi de tenir compte de ces trois contraintes.

- D’un autre côté, le gain de performance ainsi obtenu dépend des caractéristiques du processeur spécifique sur lequel le code sera exécuté.

Page 11: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 11

Cours # 8 ELE784 - Ordinateurs et programmation système 21

Optimisation de la performance du code

1.7 - Réduction de la charge des boucles

- Il est bien connu que le déroulement, même partiel, des boucles permet de réduire l’impact de la charge supplémentaire induite par la gestion de la boucle.

Par exemple :

void Operation5 (vec_ptr v, data_t *dest) {int length = vec_len(v);data_t *data = get_first(v);data_t x = INIT;int limit = length-2;int i;

for (i = 0; i < limit; i +=3)x = x OPER data[i] OPER data[i+1] OPER data[i+2];

for (; i < length; i++)x = x OPER data[i];

*dest = x;}

- La fonction Operation5 () reproduit la fonction Operation4 (), mais en déroulant la boucle par un facteur 3.

Pour terminer les éléments qui reste.

Pour éviter de déborder du tableau

Cours # 8 ELE784 - Ordinateurs et programmation système 22

Optimisation de la performance du code

Load

+

comp

goto

xReg

cond

i

1

2

3

4

5

6

7

+

comp

goto

i

cond

8

9

+

Load

Reg+

Load

Reg+

Load

xReg

+

Load

Reg+

Load

Reg+

x

i

- Chaque Load prend 3 cycles (latence) et un Load peut démarrer à chaque cycle (temps d’émission).

1.0616

1.258

1.504

1.502Degré de déroulement

1.332.00CPE31

Mesure de CPE

On remarque que le gain n’est pas linéaire.

Page 12: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 12

Cours # 8 ELE784 - Ordinateurs et programmation système 23

Optimisation de la performance du code

1.7 - Réduction de la charge des boucles

- Avec le déroulement de la boucle de la fonction Operation5(), l’effet de l’optimisation se fait sentir de façon plus important lorsque le nombre de données à traiter augmente.

Par exemple :

1.121.311.561.401.562.061024 éléments3.663.913.843.393.574.0231 éléments1.0616

1.258

1.504

1.502Degré de déroulement

1.332.00CPE (idéal)31

Nombre d’éléments

- Le tableau ci-dessus montre le CPE net mesuré sur la fonction Operation5 () au complet, ce qui tient compte des autres parties non analysées précédemment.

- Il est clair que pour de petites quantités de données (31 éléments), l’exécution est dominée par ces autres parties.

- Tandis que le gain pour le cas à 1024 éléments est suffisamment important pour justifier l’augmentation du nombre d’instruction issu du déroulement de la boucle (un degré de 16 produit 142 bytes d’instruction par rapport à 63 bytes sans déroulement).

Cours # 8 ELE784 - Ordinateurs et programmation système 24

Optimisation de la performance du code

1.8 - Conversion à l’utilisation de pointeurs

- L’arithmétique de pointeur est fréquemment utilisée comme alternative plus performante à l’indexation dans les tableaux, mais qu’en est-il réellement ?

Par exemple :

void Operation5p (vec_ptr v, data_t *dest) {data_t *data = get_first(v);data_t *last = data+vec_len(v);data_t x = INIT;int limit = last-2;

for (; data < limit; data +=3)x = x OPER data[0] OPER data[1]

OPER data[2];

for (; data < last; data++)x = x OPER data[0];

*dest = x;}

void Operation4p (vec_ptr v, data_t *dest) {data_t *data = get_first(v);data_t *last = data+vec_len(v);data_t x = INIT;

for (; data < last; data++)x = x OPER (*data);

*dest = x;}

En convertissant les fonctions Operation4() et Operation5() à l’arithmétique de pointeur

5.003.004.001.33Operation5p()5.003.004.001.33Operation5()5.005.00

*

4.004.00

*

Point flottantEntier

3.003.00Operation4p()3.002.00Operation4()

++Fonction

Les résultats sont identiques, sauf un cas qui est pire.

Page 13: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 13

Cours # 8 ELE784 - Ordinateurs et programmation système 25

Optimisation de la performance du code

1.9 - Améliorer le parallélisme

- Jusqu’à présent, notre code est limité par la "latence" des opérations.

- Mais plusieurs unités fonctionnelles du processeur possèdent un pipeline qui leur permet de traiter plusieurs opérations, à différents stages de leur traitement, en même temps.

- Par contre, jusqu’à présent, notre code ne nous permet pas d’en profiter car le résultat du traitement est accumulé dans une seule variable.

- À cause de ça, la prochaine opération ne peut pas commencer avant que le résultat de la précédente ait été obtenu.

- Cette constatation nous indique la prochaine optimisation :

Fractionnement de boucle :

- Certains types de traitement se prêtent à une optimisation qui prend avantage du pipeline des opérations afin de paralléliser le traitement.

- Par exemple, l’addition et la multiplication de nombres entiers étant associative et commutative, une série de ces opérations peut être effectuée par morceaux.

Cours # 8 ELE784 - Ordinateurs et programmation système 26

Optimisation de la performance du code

1.9 - Améliorer le parallélismeFractionnement de boucle :

Sachant que=n=0

Kdata[n]ΠX =

n=0

Ndata[n] Π Π*

k=K

Ndata[k]

void Operation6 (vec_ptr v, data_t *dest) {int length = vec_len(v);int limit = length-1;data_t *data = get_first(v);data_t x0 = INIT, x1 = INIT;int i;

for (i = 0; i < limit; i += 2) {x0 = x0 OPER data[i];x1 = x1 OPER data[i+1];

}for (i; i < length; i++)

x0 = x0 OPER data[i];*dest = x0 OPER x1;

}

Calcul divisé en deux morceaux

Alors, pour une itération

Une nouvelle multiplication commence avant que la précédente termine.

Load

*

++

comp

gotox0Reg0

cond

i

Load

i

Reg1

*

x1

x0x1

i

Page 14: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 14

Cours # 8 ELE784 - Ordinateurs et programmation système 27

Optimisation de la performance du code

1.9 - Améliorer le parallélisme

Fractionnement de boucle :

- Avec cet optimisation, puisque chaque multiplication s’exécute en 4 cycles (latence), deux multiplications sont exécutées à chaque 4 cycles.

- Il n’en va pas de même avec l’addition entière, comme le montre le tableau de CPE ci-dessous, car cette addition s’exécute en un seul cycle.

2.505.00

*

2.004.00

*

Point flottantEntier

2.001.50Operation6()3.001.50Operation4()

++FonctionMesure du CPE : (avec déroulement de

boucle de degré 2)

- Pour les autres opérations, le gain de performance est approximativement de 2.

- La plupart des compilateurs déroulent les boucles, mais peu les fractionnent.

- Notons, par contre, que l’addition et la multiplication point-flottante ne sont pas vraiment associatives à cause des erreurs d’arrondissement de ces calculs.

- Donc, en fait, lorsque les calculs sont effectués dans un ordre différent, le résultat peut être différent.

Cours # 8 ELE784 - Ordinateurs et programmation système 28

Optimisation de la performance du code

1.9 - Améliorer le parallélisme

Fractionnement de registre :

- Le fractionnement de boucle permet de tirer avantage du pipeline des unités fonctionnelles, mais utilise plus de registres internes du processeur.

- En fait, le processeur n’a qu’un nombre limité de registres internes.

- Lorsque le fractionnement de boucle est tel qu’il est à cours de registre, le processeur "fractionne les registres" en stockant temporairement les résultats intermédiaires sur la pile.

- Il en résulte une perte de performance considérable.

Par exemple : Processeur standard = (8 registres entier) + (8 registres point-flottant)

Alors pour la fonction Operation6() :En entiers

Length, idata, Limit

4 registres

2 registres

+X0, X1

Reste

2 registres

En point-flottant

X0, X1 2 registres

Reste6 registresReg0, Reg1 Reg0, Reg1, …

Page 15: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 15

Cours # 8 ELE784 - Ordinateurs et programmation système 29

Optimisation de la performance du code

1.10 - Résumé des résultats d’optimisation actuels

4.452.012.08

4.442.342.36

4.694.124.24

1.191.151.11

Déroule (degré 2) et parallélisme (degré 2)Déroule (degré 4) et parallélisme (degré 2)Déroule (degré 8) et parallélisme (degré 4)

Operation6()

6.326.22

6.326.33

9.019.01

1.511.25

Déroule la boucle (degré 4)Déroule la boucle (degré 16)Operation5()

8.018.019.011.76Accumulation dans variable localeOperation4()13.0113.2612.526.26Accès direct aux donnéesOperation3()32.7328.7332.1819.19vec_len() avant la boucleOperation2()

Option de compilation –O2Pas optimisé

Méthode

32.0253.71

*

36.0547.14

*

Point flottantEntier

37.3725.08Operation1()52.0740.14Operation1()

++Fonction

- Après analyse, il apparaît que les calculs à point-flottant de nos tests utilisaient des données qui produisaient un débordement dont le résultat est l’infini.

- Le processeur prend de 110 à 120 cycles pour effectuer une multiplication avec l’infini, ce qui affecte la performance considérablement.

- Ainsi, la performance dépend aussi des données utilisées et le tableau ci-dessous reproduit les mesures de CPE corrigées.

Cours # 8 ELE784 - Ordinateurs et programmation système 30

Optimisation de la performance du code

1.11 - Prédiction des branchements et pénalités

- Afin de garder les pipelines des unités fonctionnelles le plus occupé possible, le processeur effectue du traitement spéculatif, c’est-à-dire qu’il commence le traitement de plusieurs instructions en avance.

- Cette stratégie fonctionne bien en général; mais face à un branchement, elle exige que le processeur tente de prédire la destination final.

Types de branchement : - Prise de décision (boucle, si…alors, switch)- Branchement inconditionnel (goto)- Appel et retour de fonction- Appel et retour d’interruption

- Lorsque la prédiction est correct, les résultats du traitement spéculatif est conservé, c’est-à-dire que les registres du processeur et la mémoire sont modifiés en conséquence.

- Par contre, lorsque la prédiction s’avère à être inexacte, les résultats du traitement spéculatif doivent être rejeté et les pipelines doivent être réinitialisés avec une nouvelle séquence d’instructions.

- Il en résulte une pénalité assez lourde, en terme de temps d’exécution.

Page 16: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 16

Cours # 8 ELE784 - Ordinateurs et programmation système 31

Optimisation de la performance du code

1.11 - Prédiction des branchements et pénalités

- Souvent, les processeurs se basent sur l’heuristique suivante pour prédire la destination des branchements conditionnels :

- Si la destination du branchement est une adresse plus petite,

alors prend ce branchement.

- Si la destination du branchement est une adresse plus grande,

alors ne prend pas ce branchement.

- Cette heuristique favorise principalement les boucles car leur traitement répétitif amène le processeur aux adresses plus basses et seule la sortie de boucle le fait avancer vers les adresses plus hautes.

- Selon les études, cette heuristique est adéquate dans 65 % des branchements.

- Une autre heuristique utilisée suppose que tous les branchements seront pris, ce qui s’avère être correcte dans 60 % des cas.

- Une mesure de la pénalité des mauvaises prédictions de branchement montre un coût de 14 cycles pour chaque mauvais choix.

- Ainsi, avec 65 % de succès, chaque branchement coûte en moyenne 4.9 cycles.

Cours # 8 ELE784 - Ordinateurs et programmation système 32

Optimisation de la performance du code

1.11 - Prédiction des branchements et pénalités

- Selon les études, les instructions de branchement constituent :- Environ 15 % des instructions des programmes non-numériques.- De 3 % à 12 % des instructions des programmes numériques.

Programme qui fait beaucoup de calculs numériques

- Ainsi, les pénalités des erreurs de prédiction peuvent avoir un impact sur la performance globale du code.

- Hélas, il n’y a que peu de chose qu’un programmeur peut faire pour améliorer la prédiction des branchements, sauf :- Choisir des conditions de branchement

qui ont plus de chance d’être réalisées.

- Réduire le nombre de branchements conditionnels : If (A > 0)

x = A*y;else if (A < 0)

x = y + A;else x = 0;

Cond = ((A < 0) << 1) | ((A > 0) << 0);switch (Cond) {

case 1 : x = y + A;break;

case 2 : x = A*y;break;

default : x = 0;}

Par exemple :

Page 17: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 17

Cours # 8 ELE784 - Ordinateurs et programmation système 33

Optimisation de la performance du code

1.12 - Performance des accès à la mémoire

- Jusqu’à présent, nous avons pris en considération les temps de latence et d’émission des opérations, sauf pour les accès aux données en mémoire.

- Sans rentrer dans des considérations concernant les mouvements entre la mémoire-cache et la mémoire-vive, supposons pour la suite que les données se trouvent toujours en mémoire-cache.

Latence des lectures :

- Les exemples précédents utilisaient uniquement l’indexation pour accéder aux données en mémoire et selon un ordre strictement séquentiel, donc facilement prévisible.

- Dans ce cas, le processeur peut aisément utiliser le traitement spéculatif sur la lecture des données et profiter ainsi du pipeline de l’unité de lecture.

- Lorsque l’emplacement de la prochaine donnée ne peut pas être facilement identifié, comme lors des accès à une liste chainée ou des accès indirects (pointeur), la latence de lecture prend le dessus et réduit la performance.

Cours # 8 ELE784 - Ordinateurs et programmation système 34

Optimisation de la performance du code

1.12 - Performance des accès à la mémoire

Latence des lectures :

Load

++

test

jne

Reg

cond

i1

2

3

4

5

6

7

Load

++i

8

9

10

test

jne

Reg

cond

Load

++i

test

jnecond

Reg

typedef struct Elem {struct Elem *next;int data;

} list_elem, *list_ptr;

int list_len (list_ptr ls) {int len = 0;

for (; ls; ls->next)len++;

return len;}

Par exemple :

Soit la structure suivante :

Et la fonction qui calcule la longueur de la liste chainée :

Chaque lecture dépend de la précédente

CPE = 3.0

Page 18: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 18

Cours # 8 ELE784 - Ordinateurs et programmation système 35

Optimisation de la performance du code

1.12 - Performance des accès à la mémoire

Latence des écritures :

- Comme pour la lecture, l’écriture a une latence de 3 cycles et un temps d’émission d’un cycle, permettant ainsi de démarrer une écriture par cycle.

- De plus, l’écriture est indépendante de la lecture, puisque effectuée par sa propre unité fonctionnelle.

- Par contre, une différence fondamentale entre la lecture et l’écriture est que la 1ière modifie toujours un registre du processeur tandis que l’autre non.

- Aussi, les écritures ne sont jamais dépendantes entre elles, contrairement aux lectures, comme on l’a vu précédemment.

- Ainsi, plusieurs écritures consécutives peuvent aisément être misent en pipeline.

- D’un autre côté, les lectures sont fréquemment dépendantes des écritures puisque seules les lectures peuvent être affectées par le résultat des écritures.

- Souvent les lectures et les écritures ne peuvent être parallélisées entre elles.

Cours # 8 ELE784 - Ordinateurs et programmation système 36

Adresse

Donnée

Load Unit Store UnitAdresse Donnée

Cache-memory

Adresse Donnée Adresse Donnée

Optimisation de la performance du code

1.12 - Performance des accès à la mémoire

Latence des écritures :

- En réalité, l’unité fonctionnelle d’écriture contient 2 tampons (tampon d’adresse et tampon de donnée) qui peuvent tenir plusieurs opérations d’écriture successives.

- Lors d’une lecture, l’unité doit vérifier si une écriture est en attente dans l’unitéd’écriture avant de récupérer la donnée dans la mémoire-cache.

- Si la lecture ne faisait pas cette vérification, elle pourrait lire une donnée en mémoire-cache qui n’a pas encore été mise à jour par une écriture qui a étédéclenchée avant la lecture.

- S’il y a effectivement une écriture en cours sur la case de mémoire-cache visée par la lecture, celle-ci doit attendre la fin de cette écriture.

- L’opération d’écriture ne subit pas ce genre de contrainte de la part de la lecture.

Page 19: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 19

Cours # 8 ELE784 - Ordinateurs et programmation système 37

Optimisation de la performance du code

Par exemple : void Write_Read (int *src, int *dest, int n) {int cnt = n;int val = 0;

while (cnt--) {*dest = val;val = (*src + 1);

}}

Opération de lecture

Opération d’écriture

Storedata

val

1

2

3

4

5

6

7

--

jnc

cntStoreaddr Load

src++

Storedata

val

--

jnc

cnt

condStoreaddr Load

src++

val

cnt

cond

val

Soit la fonction Write_Read qui effectue une écriture suivie d’une lecture.

Cas 1 : *dest ≠ *src

Dans ce cas, la lecture ne dépend pas de l’écriture

CPE = 2.0

Cours # 8 ELE784 - Ordinateurs et programmation système 38

Optimisation de la performance du code

Storedata

val--

jnc

cntStoreaddr

Load

src++

=

Storedata

val

--

jnc

cnt

condStoreaddr

Load

src++

=

val

cnt

cond

val

Cas 2 : *dest = *src Ici, la lecture dépend de l’écriture

1

2

3

4

5

6

7

8

9

10

11

CPE = 6.0

Dans ce cas-ci, la lecture doit attendre

la fin de l’écriture

Page 20: ELE784-Cours8-Optimisation

Cours # 8 ELE784 - Ordinateurs et programmation système 20

Cours # 8 ELE784 - Ordinateurs et programmation système 39

Optimisation de la performance du code

1.13 - Techniques d’amélioration des performances

- Une bonne connaissance du comportement du processeur et du compilateur permet de faire les bons choix lors de la conception et l’optimisation du code.

Conception de haute niveau :

- Choisir des algorithmes et des structures de données adéquates.- Éviter les algorithmes ayant un comportement quadratique.

Principes de codage :

- Éviter les "bloqueurs" d’optimisation du compilateur.- Éviter les appels de fonction excessifs, tels que dans les boucles.- Éliminer les références inutiles à la mémoire, par exemple avec

l’utilisation de variables temporaires et en stockant en mémoire seulement le résultat final.

Optimisation de bas niveau :

- Essayer différentes combinaisons de pointeurs versus indexage.- Réduire la charge des boucles en les déroulant.- Utiliser les pipelines des unités fonctionnelles avec des techniques telles

que le "fractionnement" des boucles.

Cours # 8 ELE784 - Ordinateurs et programmation système 40

Optimisation de la performance du code

1.13 - Techniques d’amélioration des performances

- Les profileurs sont des outils qui permettent d’identifier les parties du code où le principal du traitement est effectué.

- En utilisant ce type d’outil, il est possible de concentrer ses efforts d’optimisation là où ils sont vraiment utiles.

Utilisation de "profileur" :

- La loi d’Amdahl décrit l’effet de l’optimisation d’un morceau de code sur la performance globale du programme.

Loi d’Amdahl :

Tnew = (1-α)Told + (αTold)/k = Told[(1-α) + α/k]

S = Told/Tnew = 1

(1-α) + α/k

Soit Alors

(Loi d’Amdahl)

ToldTnewα

k

où = Temps d’exécution avant optimisation= Temps d’exécution après optimisation= % de code optimisé= facteur d’optimisation obtenu

Cette loi montre qu’un gain de performance élevé sur une petite partie de code ne résulte pas en un gain équivalent sur l’ensemble du code.