ele784-cours8-optimisation
TRANSCRIPT
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.
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.
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
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
où
#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).
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;
}
où
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
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
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.
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.
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
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é.
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.
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.
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
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, …
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.
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 :
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
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.
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
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.