720 likes | 850 Views
Performance des logiciels. Besoins et stratégies (avec des exemples de Steve McConnell « Code Complete ») Vladimir Makarenkov (Université du Québec à Montréal). Nécessité d’avoir des logiciels performants. Applications critiques Applications scientifiques Informatique embarquée
E N D
Performance des logiciels Besoins et stratégies (avec des exemples de Steve McConnell « 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 • Rechercher les portions critiques (« profiling ») • Rechercher 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 • 1re règle d’optimisation • Ne rien faire • 2e règle d’optimisation • Ne rien faire • 3e 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)
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, currentTimeMillis() • 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 peuteffectuer 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 monSin = sin(y); x = (monSin + 1) / (monSin – 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 • Accès séquentiel
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) • À tester sur votre configuration
Arrêter de tester si on connaît la réponse Exemple de recherche inutile: for(i=0; i<taille; i++){ if (entree[i] < 0){ entreeNeg = true; break; } } Utiliser des opérateurs court-circuitants si possible (sinon 2 tests séparés) if ((5 < x) && (x<10)) {…} if (5 < x) if (x < 10) {…} Optimisation des opérations logiques
Optimisation des opérations logiques (2) switch(entree) { case ‘+’: case ‘=‘ : {…} case ‘0’:… case ‘9‘: {…} case ‘,’: case ‘?‘:…:{…} case ‘A’:… case ‘Z‘: {…} … } switch(entree) { case ‘A’:… case ‘Z‘: {…} case ‘,’: case ‘?‘:…:{…} case ‘0’:… case ‘9‘: {…} case ‘+’: case ‘=‘ : {…} … } • Ordonner les tests par leur fréquence
Optimisation des opérations logiques (3) • Ordonner les tests par leur fréquence
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.)
A B 1 1 2 1 2 2 3 0 C Optimisation des opérations logiques (6) 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;} • La catégorie de l’objet est définie selon son appartenance à un ou plusieurs des 3 groupes (voir le schéma à droite)
Optimisation des opérations logiques (7) // définit categoryTable static int categoryTable [2][2][2] = { // !b!c !bc b!c bc 0, 3, 2, 2, // !a 1, 2, 1, 1 // a }; … category = categoryTable[a][b][c]; • Remplacer les expressions compliquées par des tableaux
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 (sumType == SUMTYPE_NET) { netSum = netSum + amount[i]; } else { grossSum = grossSum + amount[i]; } }
Optimisation des boucles (3) if (sumType == SUMTYPE_NET) { for (i = 0; i < count; i++) { netSum = netSum + amount[i]; } } else { for (i = 0; i < count; i++) { grossSum = grossSum + amount[i]; } }
Optimisation des boucles (5) for(i=0; i < nbEmployes; i++){ nomEmploye[i] = “”; } … for(i=0; i < nbEmployes; i++){ salaireEmploye[i] = 0; } for(i=0; i < nbEmployes; i++){ nomEmploye[i] = “”; salaireEmploye[i] = 0; } • Fusion de boucles • Regrouper les portions de code qui travaillent sur le même ensemble d’éléments
Optimisation des boucles (6) • Fusion de boucles
Optimisation des boucles (7) 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; } • Déroulement
Optimisation des boucles (8) • Déroulement