Performance des logiciels Besoins et stratgies avec des
Performance des logiciels Besoins et stratégies (avec des exemples de Steve Mc. Connell « Code Complete » ) Vladimir Makarenkov (Université du Québec à Montréal)
Nécessité d’avoir des logiciels performants • Applications critiques • Applications scientifiques • Informatique embarquée • Besoin de temps de réponse très courts • Services distants • Avantage concurrentiel • … La performance doit faire partie des spécifications
Ne pas optimiser le code si ce n’est pas nécessaire • Les méthodes d’optimisation manuelles produisent souvent un code peu maintenable et de faible qualité • La recherche d’optimisation demande des ressources de haut niveau • L’optimisation peut introduire des erreurs difficilement retraçables
Facteurs influençant les performances • Choix des algorithmes • Choix des structures de données • Choix du langage et du compilateur • Choix du matériel • Importance des accès mémoire ou E/S • Qualité du code & expertise du programmeur
Principe de Pareto (80 / 20) • Pareto: « 80% des richesses sont possèdes par 20% de la population » (Peut s’appliquer à toute sorte de domaines) • Knuth: moins de 4% du code compte pour plus de 50% du temps d’exécution • Recher les portions critiques ( « profiling » ) • Recher les structures et types adaptés aux besoins et moins gourmands en ressources • Modulariser l’application – Facilite le profilage et les modifications locales • Utiliser des compilateurs offrant des options d’optimisation • Optimiser les détails du code (code-tuning)
Règles d’optimisation • 1 re règle d’optimisation – Ne rien faire • 2 e règle d’optimisation – Ne rien faire • 3 e règle d’optimisation (pour experts seulement) – Ne rien faire maintenant • Attendre d’avoir une version finale entièrement opérationnelle.
Optimisation par le compilateur • Souvent suffisant pour atteindre les objectifs – Meilleure que l’optimisation manuelle • L’optimisation par le compilateur peut améliorer les performances de plus de 40% – L’optimisation manuelle se limite à 15 - 30% dans le meilleur des cas. • L’optimisation manuelle peut entrer en conflit avec des options du compilateur • Choisir le compilateur en conséquence
Comparaison des performances de certains compilateurs (en secondes) Langage Sans Optimisation par le compilateur Gain C++ (compilateur 1) 2. 21 1. 05 52% C++ (compilateur 2) 2. 78 1. 15 59% C++ (compilateur 3) 2. 43 1. 25 49% C# 1. 55 0% Visual Basic 1. 78 0% Java VM 1 2. 77 0% Java VM 2 1. 39 1. 38 < 1% Java VM 3 2. 63 0%
Mesures de performance • Permettent de circonscrire les portions de code critiques • Il faut des mesures précises • Il faut mesurer ce qui nous intéresse – Attention aux délais dûs aux OS, des programmes en arrière plan, etc. • Utiliser des outils de profilage • Garder les mesures pour les tests subséquents • Refaire les mesures après chaque modification
Outils d’évaluation • Unix cc –p nom_du_fichier -> a. out -> mon. out (voir avec prof) gcc –pg nom_du_fichier -> a. out -> gmon. out (voir avec gprof) • Windows – ANTS Profiler – JProbe • Gestion du temps du langage – Java classe Date, Time, current. Time. Millis() – C clock, time
Quand optimiser ? • Si la performance ne correspond pas aux attentes ou aux spécifications • Si le gain en vaut la peine – Le temps passé à optimiser ne doit pas être supérieur au gain réalisé pendant toute la durée de vie du programme
Quand optimiser ? (2) • Si la performance apporte une plus value importante au logiciel • Quand le code est finalisé et fonctionnel • Après avoir trouvé les points critiques
Analyse de performances : comment ? • Sur un code complètement implémenté et testé • Sur une version « release » (optimisée par le compilateur …) • Avec des données représentatives • Le cycle d’optimisation:
Sources d’inefficacité classiques • Boucles – Sortir les calculs, tests et opérations qui ne dépendent pas des itérations de la boucle for (i = 0; i < image. with()*image. height(); i++){…} vs int longueur = image. with()*image. height() for (i = 0; i < longueur; i++) {…}
Sources d’inefficacité (2) • Boucles – Ordre des boucles imbriquées • Placez la boucle la plus active à l’intérieur for (i = 0; i < 1000; i++) for(j = 0; j < 10; j++) {…} vs for (j = 0; j < 10; j++) { for(i = 0; i < 1000; i++) {…} }
Sources d’inefficacité (3) • Boucles – Dérouler les boucles for (i = 1; i < 4; i++) { a[i] = 0; } vs a[1] = 0; a[2] = 0; a[3] = 0; – Ordre de parcours de tableaux • Profiter de l’antémémoire (i. e. mémoire cache)
Remarques • L’option d’optimisation par le compilateur peut effectuer quelques unes des optimisations citées dans les exemples précédents (et suivants), telles que l’identification des invariants de boucles, déroulement de boucles simples ou l’élimination des sous–expressions communes dans une ligne de code. Cependant, si ces modifications n’altèrent pas la lisibilité du code, elles peuvent être faites manuellement. • L’optimisation de la gestion de l’antémémoire (i. e. mémoire cache) dépens de la manière dont le processeur gère la mémoire. C’est une optimisation de bas niveau étroitement liée au matériel.
Sources d’inefficacité (4) • Test – Utiliser les opérateurs court-circuitants • && et || if ((c >= ‘ 0’) && (c <= ‘ 9’)) {…} – Traiter les cas usuels et fréquents en premier
Sources d’inefficacité (5) • Calculs – Garder le résultat d’un calcul plutôt que de le refaire x = (sin(y) + 1) / (sin(y) – 1); vs mon. Sin = sin(y); x = (mon. Sin + 1) / (mon. Sin – 1);
Sources d’inefficacité (6) • Calculs – Utiliser des opérations moins coûteuses • Additions vs multiplication • Multiplication par l’inverse vs division • … x = pow(y, 2)/2; vs x = (y*y)*0. 5;
Sources d’inefficacité (7) • Calculs – Éviter les calculs inutiles • sqrt(x) < sqrt(y) donne le même résultat que x < y – Faire un prétraitement des données avant de faire une opération coûteuse – Effectuer un précalcul des résultats courants • Ex: tableau de sinus / cosinus pour des valeurs courantes
Sources d’inefficacité (8) • Types de données – Utiliser le type de données approprié – Utiliser le type de données le moins gourmand qui répond aux besoins • char < short < int < long < float < double (attention: pas toujours vrai)
Sources d’inefficacité (9) • Types de données – Éviter les nombres à point flottant si ce n’est pas indispensable • Ex: le calcul de coordonnées à l’écran en float n’a pas de sens, 1. 234567 pixels = 1 pixel – Faire attention à la gestion des chaînes de caractères • Java et C# créent des instances de classes pour chaque modification d’une chaîne et en font la copie • C parcourt la chaîne au complet pour en calculer sa longueur
Sources d’inefficacité (10) • E/S & accès distants – Préférer le travail sur des données en mémoire, éviter les accès disque, réseaux, bases de données… – Utiliser la mémoire cache • Ex: images dans les navigateurs
Sources d’inefficacité (11) • Erreurs et oublis dans le code – Code de débogage oublié – Libération de la mémoire – Indexation des bases de données – Passage par valeur vs par référence • Ex: les tableaux demandent une copie s’ils sont passés par valeur
Comparaison du temps d’accès sur un tableau de 100 éléments (en secondes) • Accès aléatoire Langage Accès fichier disque Accès mémoire Gain C++ 6. 04 0. 000 100% C# 12. 8 0. 010 100% • Accès séquentiel Langage Accès fichier disque Accès mémoire Gain C++ 3. 29 0. 021 99% C# 2. 60 0. 030 99%
Procédure d’optimisation • 1) Développer du code maintenable et facile à comprendre • 2) En cas de problèmes de performance – A. Garder une version fonctionnelle du code – B. Profiler l’exécution du système pour trouver les points critiques – C. Déterminer les sources des problèmes. • Sont-ils dûs à une mauvaise architecture, à de mauvais algorithmes, etc.
Procédure d’optimisation (2) – D. Vérifier si le tuning peur apporter une amélioration, sinon garder le code de l’étape 1 – E. Faire le tuning des zones trouvées en C – F. Mesurer chaque modification individuellement – G. Si la modification n’apporte pas de changements significatifs, revenir au code de l’étape A • 3) Répéter l’étape 2 jusqu’à ce que les exigences initiales soient satisfaites
Attention • Ne pas optimiser les prototypes, les tests ou tout code non finalisé • L’utilisation de structures de données adaptées apportent plus de gains que le tuning • La clarté du code doit primer en premier lieu • Commenter les changements. – Risque de « recorriger » lors d’une relecture – Le tuning produit souvent du code peu clair
Attention (2) • « Le mieux est l’ennemi du bien » – Écrire un programme qui répond aux attentes et n’optimiser que les parties critiques • Attention aux mythes – Les recettes de cuisine, les idées préconçues, les vieilles solutions, etc. ne sont pas adaptées au contexte et sont souvent dépassées par les avancées technologiques. – Ne se fier qu’aux tests en situation avec des données représentatives du problème • Testez, testez et retestez
Techniques d’optimisation Code Tuning
Rappels • L’optimisation (tuning) ne touche que des petites portions du code (points critiques) • Les techniques présentées doivent faire l’objet de tests en situation réelle (compilateur, materiel, etc. ) • L’optimisation ne doit être faite que sur du code final, s’il ne correspond pas aux spécifications et en dernier recours par rapport à d’autres méthodes
Rappels (2) • Chaque modification doit être testée et mesurée individuellement • Toute modification doit être clairement commentée et expliquée
Exemples de gains sur certaines opérations • Sur des int (compilateur C, C++, source Kernighan et Pike 1999, en nanosecondes) Opération i++ i = a + b i = a * b Int 8 12 12 Float N. A 12 11 Double N. A 12 11 i = a / b 114 28 58 • À tester sur votre configuration
Optimisation des opérations logiques • Utiliser des opérateurs court • Arrêter de tester si on -circuitants si possible connaît la réponse (sinon 2 tests séparés) – Exemple de recherche inutile: if ((5 < x) && (x<10)) {…} if (5 < x) if (x < 10) {…} for(i=0; i<taille; i++){ if (entree[i] < 0){ entree. Neg = true; break; } } Langage Sans modifications Code modifié C++ 4. 27 3. 68 Java 4. 85 3. 46 Gain 14% 29%
Optimisation des opérations logiques (2) • Ordonner les tests par leur fréquence switch(entree) { case ‘A’: … case ‘Z‘: {…} case ‘+’: case ‘=‘ : {…} case ‘, ’: case ‘? ‘: …: {…} case ‘ 0’: … case ‘ 9‘: {…} case ‘, ’: case ‘? ‘: …: {…} case ‘+’: case ‘=‘ : {…} case ‘A’: … case ‘Z‘: {…} … … } }
Optimisation des opérations logiques (3) • Ordonner les tests par leur fréquence Cas du Case Sans modifications Code modifié Gain C# 0. 22 0. 26 -18% Java 2. 56 0% Visual Basic 0. 28 0. 26 7% Cas du if Sans modifications Code modifié Gain C# 0. 63 0. 33 48% Java 0. 92 0. 46 50% Visual Basic 1. 36 1. 00 26%
Comparaison du if et du case selon différents langages Langage C# Java Visual Basic case 0. 26 2. 56 0. 26 if else 0. 33 0. 46 1. 00 Gain -27% 82% -284%
Optimisation des opérations logiques (4) • Substituer des expressions logiques compliquées par des tables de valeurs • Utiliser des transformations logiques pour minimiser les opérations (INF 1130)
Optimisation des opérations logiques (5) • Utiliser l’évaluation paresseuse – Évaluer les expressions le plus près possible de leur utilisation – Garder les résultats en mémoire si on doit les utiliser plusieurs fois – Utiliser des langages adaptés (ex: Haskell, Prolog, etc. )
Optimisation des opérations logiques (6) • La catégorie de l’objet est définie selon son appartenance à un ou plusieurs des 3 groupes (voir le schéma à droite) if (( a && !c)||(a && b && c)) { category = 1; } else if (( b && !a)||(a && c && !b)) { category = 2; } else if ((c && !a && !b)) { category = 3; } else { category = 0; } A B 1 1 2 0 2 3 C
Optimisation des opérations logiques (7) • Remplacer les expressions compliquées par des tableaux // définit category. Table static int category. Table [2][2][2] = { // !b!c !bc b!c bc 0, 3, 2, 2, // !a 1, 2, 1, 1 // a }; … category = category. Table[a][b][c];
Optimisation des boucles • Ce sont des sources importantes de gain (ou perte) de performances • Unswitching – Faire les tests qui ne dépendent pas de la boucle à l’extérieur (Attention, donne parfois du mauvais code)
Optimisation des boucles (2) for (i = 0; i < count; i++) { if (sum. Type == SUMTYPE_NET) { net. Sum = net. Sum + amount[i]; } else { gross. Sum = gross. Sum + amount[i]; } }
Optimisation des boucles (3) if (sum. Type == SUMTYPE_NET) { for (i = 0; i < count; i++) { net. Sum = net. Sum + amount[i]; } } else { for (i = 0; i < count; i++) { gross. Sum = gross. Sum + amount[i]; } }
Optimisation des boucles (4) Langage Sans modifications Code modifié C++ 2. 81 2. 27 Java 3. 97 3. 12 Visual Basic 2. 78 2. 77 Gain 19% 21% < 1%
Optimisation des boucles (5) • Fusion de boucles – Regrouper les portions de code qui travaillent sur le même ensemble d’éléments for(i=0; i < nb. Employes; i++){ nom. Employe[i] = “”; } … for(i=0; i < nb. Employes; i++){ salaire. Employe[i] = 0; } for(i=0; i < nb. Employes; i++){ nom. Employe[i] = “”; salaire. Employe[i] = 0; }
Optimisation des boucles (6) • Fusion de boucles Langage Sans modifications Code modifié C++ 3. 68 2. 65 Visual Basic 3. 75 3. 56 PHP 3. 97 2. 42 Gain 28% 5% 39%
Optimisation des boucles (7) • Déroulement i = 0; while (i < count) { a[i] = i; i = i + 1; } i = 0; while (i < count - 2) { a[i] = i; a[i + 1] = i + 1; a[i + 2] = i + 2; i = i + 3; } if ( i <= count - 1) { a[count - 1] = count - 1; } if ( i == count - 2) { a[count - 2] = count - 2; }
Optimisation des boucles (8) • Déroulement Langage Sans modifications Code modifié (count = 100) Gain C++ Java PHP 42% 43% 31% 1. 75 1. 01 5. 33 1. 01 0. 58 3. 70
Optimisation des boucles (9) • Minimiser le travail à l’intérieur des boucles for(i=0; i < nb. Taux; i++){ } taux. Net[i] = taux. Base[i]*taux-> rabais->facteurs-> net; rabais. Quantite = taux->rabais-> facteurs->net; for(i = 0; i<nb. Taux; i++){ } taux. Net[i] = taux. Base[i] * rabais. Quantite;
Optimisation des boucles (10) • Minimiser le travail dans les boucles Langage (nb. Taux = 100) C++ C# Java Sans modifications Code modifié Gain 3. 69 2. 27 4. 13 2. 97 1. 97 2. 35 19% 13% 43%
Optimisation des boucles (11) • Utilisation de sentinelles found = FALSE; i = 0; // Attribuer la sentinelle // Garder la valeur d’origine //test double while((!found) && (i < count)){ initial. Value = item[count]; item[count] = test. Value; } if (item[i] == test. Value){ found = TRUE; } else { i++; } if (found) {…} i = 0; while (item[i] != test. Value){ i++; } // Test si la valeur trouvée if (i < count) {…}
Optimisation des boucles (12) • Utilisation de sentinelles Tableau de 100 entiers Sans modifications Code modifié Gain C# 0. 77 0. 59 23% Java 1. 63 0. 91 44% Visual Basic 1. 34 0. 47 65% Tableau de 100 float Sans modifications Code modifié Gain C# 1. 35 1. 02 24% Java 1. 92 1. 28 33% Visual Basic 1. 75 1. 01 42%
Optimisation des boucles (13) • Mettre la boucle la plus occupée à l’intérieur for (col = 0; col < 100; col++) { for (lgn = 0; lgn < 5; lig ++) { for (col = 0; col < 100; col++) { sum += table[lgn][col]; } } Langage Sans modifications Code modifié Gain C++ 4. 75 3. 19 33% Java 5. 39 3. 56 34% PHP 4. 16 3. 65 12%
Optimisation des boucles (14) • Réduire la difficulté for (i = 0; i < nb. Ventes; i++) { increm. Com = rev*base. Com*rabais; cumul. Com = increm. Com; com[i] = (i+1) * rev * base. Com * rabais; for (i = 0; i < nb. Ventes; i++) { } com[i] = cumul. Com; cumul. Com += increm. Com ; } Langage Sans modifications Code modifié Gain C++ 4. 33 3. 80 12% Visual Basic 3. 54 1. 80 49%
Transformation de types • Int vs Float – Attention: peut varier beaucoup selon le langage Dim x As Single For x = 0 to 99 A(x) = 0 Next Dim i As Integer For i = 0 to 99 A(i) = 0 Next Langage Sans modifications Code modifié C++ 2. 80 0. 801 Visual Basic 6. 84 0. 280 PHP 5. 01 4. 65 Gain 71% 96% 7%
Transformation de types (2) • Utilisation de tableaux de taille réduite for (lgn = 0; lgn < nb. Lgn; lgn++) { for(col = 0; col < nb. Col; col++) { matrix[lgn][col] = 0; } } Langage for (i = 0; i < nb. Lgn * nb. Col; i++) { matrix[i] = 0; } Sans modifications Code modifié Gain C++ 8. 75 7. 82 11% C# 3. 28 2. 99 9% Java 7. 78 4. 14 47% Visual Basic 9. 43 3. 22 66%
Transformation de types (3) • Minimiser les références aux tableaux for (type. Rabais = 0; type. Rabais < nb. Types; type. Rabais++) { for (niv. Rabais = 0; niv. Rabais < nb. Niveaux; niv. Rabais++) { rabais = rabais[type. Rabais]; for (niv. Rabais = 0; niv. Rabais < nb. Niveaux; niv. Rabais++) { taux[niv. Rabais] *= rabais[type. Rabais]; } taux[niv. Rabais] *= rabais ; } } }
Transformation de types (4) • Minimiser les références aux tableaux Langage Sans modifications Code modifié C++ 32. 1 34. 5 C# 18. 3 17. 0 Visual Basic 23. 2 18. 4 Gain -7% 7% 20%
Expressions mathématiques et logiques • Utilisation d’identités algébriques et logiques simplifiées !a & !b 3 opérations !(a | b) 2 opérations pow(x, 3) 1 appel à pow x*x*x pas d’appel de fonction if (sqrt(x) < sqrt(y)) {…} 2 appels à sqrt if (x < y) {…} Même valeur de vérité sans faire appel à la fonction sqrt Langage Sans modifications Code modifié Gain C++ 7. 43 0. 01 99. 9% Visual Basic 4. 59 0. 22 95% Python 4. 21 0. 40 90%
Expressions mathématiques et logiques (2) • Utilisation d’opérations moins coûteuses //polynôme du n-ème ordre valeur = coef[0]; for (puiss = 1; puiss <= ordre; puiss++) { valeur += coef[puiss] * pow(x, puiss); } //polynôme du n-ème ordre valeur = coef[0]; puiss. X = x; for (puiss = 1; puiss <= ordre; puiss++) { valeur += coef[puiss] * puiss. X; puiss. X = puiss. X * x; } Langage Sans modifications Code modifié Visual Basic 6. 26 0. 16 Python 3. 24 2. 60 Gain 97% 20%
Expressions mathématiques et logiques (3) • Initialiser les valeurs à la compilation unsigned int Log 2 (unsigned int x) { return (unsigned int) (log(x) / log(2)); } const double LOG 2 = 0. 69314718; unsigned int Log 2 (unsigned int x) { return (unsigned int) (log(x) / LOG 2)); } Langage C++ Java PHP Sans modifications 9. 66 17. 0 2. 45 Code modifié 5. 97 12. 3 1. 50 Gain 38% 28% 39%
Expressions mathématiques et logiques (4) • Être attentif aux routines système // fonction sans points flottants // (approximation) unsigned int Log 2 (unsigned int x) { return (unsigned int) (log(x) / log(2)); if (x < 2) return 0; if (x < 4) return 1; if (x < 8) return 2; … if (x < 2147483648) return 30; return 31; } } Langage Sans modifications Code modifié Gain C++ 9. 66 0. 66 93% Java 17. 0 0. 88 95% PHP 2. 45 3. 45 -41%
Expressions mathématiques et logiques (5) • Utilisation de constantes du bon type • Précalcul des résultats • Éliminer les expressions communes
Expressions mathématiques et logiques (6) • Calcul préalable d’expressions complexes double Calcul. Mensualites (long emprunt, int mois, double taux. Interet) { return emprunt / (( 1. 0 – Math. pow((1. 0 + (taux. Interet / 12. 0)), -mois)) / (taux. Interet / 12. 0)); }
Expressions mathématiques et logiques (7) • Calcul préalable d’expressions complexes Interet. Mensuel = taux. Interet / 12. 0; double Calcul. Mensualites (long emprunt, int mois, double taux. Interet) { return emprunt / (( 1. 0 – Math. pow((1. 0 + Interet. Mensuel), -mois)) / Interet. Mensuel); }
Routines • Bonne décomposition en sous-routines • Réécrire les routines en ligne (ou des macros) – De nos jours moins efficace. Le compilateur peut le faire directement avec les options d’optimisation. – Exemple: les performances d’un programme de copie d’une chaîne de caractères réécrit en ligne (fonction inline en C++): Langage Routine C++ Java 0. 471 13. 1 Code inline Gain 0. 431 14. 4 8% -10%
Utiliser un langage de bas niveau • Utiliser l’assembleur pour optimiser le code C++ et du C pour optimiser le code écrit dans un langage de haut niveau, etc. • N’utiliser cette approche que si l’on maîtrise le langage de bas niveau et que si les autres solutions n’ont pas apporté les effets escomptés
Utiliser un langage de bas niveau • Approche: – 1) Écrire 100% du code en langage de haut niveau – 2) Tester l’application entièrement et vérifier son exactitude – 3) Si les spécifications de performances ne sont pas atteintes, profiler le programme pour trouver les points critiques (bottlenecks) – 4) Recoder quelques petits morceaux de code en langage de bas niveau
Connaître le langage utilisé • La connaissance des principes internes du langage utilisé permettent d’obtenir de bonnes performances par l’utilisation des instructions appropriées – Java et C# créent un nouvel objet et font une copie pour toute modification dans un String. S’il y a beaucoup de modifications de texte, utiliser les classes appropriées (String. Builder ou similaires) – En C, strlen() parcours toute la chaîne pour calculer sa longueur – Vérifier si le passage par défaut des paramètres se fait par référence ou par valeur, forcer le choix approprié
- Slides: 71