Optimisation du Code

Définition et Propriétés

L’optimisation du code est toute méthode de modification du code pour améliorer la qualité et l’efficacité du code. Un programme peut être optimisé de sorte qu’il devienne de plus petite taille, consomme moins de mémoire, s’exécute plus rapidement ou effectue moins d’opérations d’entrée/ sortie.

Les méthodes d’optimisation doivent respecter les exigences de base, c’est-à-dire qu’un programme optimisé doit avoir la même sortie et les mêmes effets secondaires que sa version non optimisée. Cette exigence, cependant, peut être ignorée dans le cas où le bénéfice de l’optimisation est estimé plus important que les conséquences probables d’un changement de comportement du programme.

Types et niveaux d’optimisation

L’optimisation peut être effectuée par des optimiseurs automatiques ou des programmeurs. Un optimiseur est soit un outil logiciel spécialisé, soit une unité intégrée d’un compilateur (le soi-disant compilateur d’optimisation). Les processeurs modernes peuvent également optimiser l’ordre d’exécution des instructions de code.Les optimisations

sont classées en optimisations de haut niveau et de bas niveau. Les optimisations de haut niveau sont généralement effectuées par le programmeur, qui gère des entités abstraites (fonctions, procédures, classes, etc.) et garde à l’esprit le cadre général de la tâche d’optimiser la conception d’un système. Optimisations effectuées au niveau des blocs structurels élémentaires du code source – boucles, branches, etc. – sont généralement aussi appelées optimisations de haut niveau, alors que certains auteurs les classent dans un niveau séparé (“moyen”) (N. Wirth?). Les optimisations de bas niveau sont effectuées au stade où le code source est compilé dans un ensemble d’instructions machine, et c’est à ce stade que l’optimisation automatisée est généralement utilisée. Les programmeurs assembleurs croient cependant qu’aucune machine, aussi parfaite soit-elle, ne peut le faire mieux qu’un programmeur qualifié (pourtant, tout le monde s’accorde à dire qu’un mauvais programmeur fera bien pire qu’un ordinateur).

Ce qu’il faut optimiser

Avec l’optimisation manuelle du code, on est confronté à un autre problème: on n’a pas seulement besoin de savoir exactement comment l’optimisation doit être effectuée, mais aussi quelle partie particulière du programme doit être optimisée. Pour diverses raisons (opérations d’entrée lentes, différence de vitesse de travail d’un opérateur humain et d’un ordinateur, etc.), 90% du temps d’exécution d’un programme est consacré à l’exécution de seulement 10% du code (cette affirmation est plutôt spéculative, avec le principe de Pareto comme motif assez douteux, mais A. Tanenbaum le rend convaincant). Étant donné que l’optimisation prend plus de temps que le temps que vous avez consacré au développement du programme, vous feriez mieux de vous concentrer sur l’optimisation de ces 10% de code critiques plutôt que d’essayer d’optimiser l’ensemble du programme. Ces fragments de code sont connus sous le nom de goulots d’étranglement et peuvent être détectés par des utilitaires spéciaux – des profileurs – qui peuvent mesurer le temps d’exécution des différentes parties du programme.

En pratique, cependant, l’optimisation se fait généralement après l’étape de la programmation “chaotique” (y compris des méthodes telles que “Copier-coller”, “nous verrons plus tard”, “c’est OK de cette façon”), et est donc un mélange d’optimisation en tant que telle, de refactoring et de corrections de bugs: simplification des constructions “queer” comme strlen (path.c_str()), conditions logiques comme (a.x!= 0 & & a.x!= 0), et ainsi de suite. Les profileurs sont de peu d’aide avec ce type d’optimisation. Néanmoins, vous pouvez détecter ces problèmes avec des outils d’analyse statique, c’est-à-dire des outils conçus pour rechercher des erreurs sémantiques, en s’appuyant sur une analyse approfondie du code source. Comme vous pouvez le voir dans l’exemple mentionné ci-dessus avec la condition étrange, un code inefficace peut apparaître à la suite d’erreurs (comme une erreur d’impression dans notre exemple, où a.x!= 0 & & a.y!= 0 devrait être à la place). Un puissant analyseur statique détectera ces fragments de code et attirera votre attention sur eux en produisant des messages d’avertissement.

Bons et mauvais résultats de l’optimisation

En programmation, presque tout doit être traité du point de vue de la rationalité – l’optimisation ne fait pas exception. On croit que le code écrit par un programmeur assembleur inexpérimenté est 3 à 5 fois plus lent que le code généré par le compilateur (Zubkov). Une phrase de Knuth concernant les premières optimisations de bas niveau (telles que les tentatives d’économies sur les opérateurs ou les variables) est largement connue: “L’optimisation prématurée est la racine de tout mal”.

La plupart des programmeurs ne se plaignent pas des optimisations effectuées par l’optimiseur, dont certaines sont conventionnelles et obligatoires. Comme, par exemple, l’optimisation des appels de queue dans les langages fonctionnels (l’appel de queue est un cas particulier de récursivité, qui peut être représenté comme une boucle).

Cependant, il faut comprendre que plusieurs optimisations complexes au niveau du code machine peuvent entraîner un grand ralentissement de la compilation. L’avantage qu’ils vous permettent de gagner peut être beaucoup trop insignifiant par rapport aux optimisations générales de conception de système (Wirth). Il faut également garder à l’esprit que les langues modernes, avec toutes leurs “fioritures” syntaxiques et sémantiques, ont de nombreuses nuances et subtilités, de sorte qu’un programmeur qui ne les connaît pas peut être surpris par un résultat d’optimisation.

Par exemple, prenez C++ et l’optimisation dite de la valeur de retour, lorsque le compilateur évite de copier un objet temporaire renvoyé par une fonction. Parce que le compilateur omet la copie, cette méthode est également appelée “Copy elision”. Donc, le code suivant:

#include <iostream> struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; }}; C f() { return C();} int main() { std::cout << "Hello World!\n"; C obj = f();}

peut avoir plusieurs sorties:

Hello World!A copy was made.A copy was made.Hello World!A copy was made.Hello World!

Aussi étrange que cela puisse paraître, les trois versions sont valides car le standard de langage permet d’omettre les appels d’un constructeur de copie dans de tels cas, même si le constructeur a des effets secondaires (§12.8 Copier des objets de classe, paragraphe 15).

Conclusion

Ainsi, nous devrions toujours envisager d’optimiser le code du programme en utilisant des utilitaires spécialisés dans la mesure du possible, tout en le faisant avec beaucoup de soin et en étant prêt pour la probabilité d’astuces inattendues du compilateur parfois.

PVS-Studio

Un ensemble de diagnostics est implémenté dans l’analyseur statique PVS-Studio qui vous permet de trouver certaines situations où le code peut être optimisé. Cependant, PVS-Studio comme tout autre analyseur statique ne peut pas remplacer les outils de profilage. Seuls les analyseurs de programmes dynamiques sont capables d’identifier les goulots d’étranglement. Les analyseurs statiques ne savent pas ce que les programmes de données d’entrée obtiennent et à quelle fréquence un morceau de code particulier est exécuté. C’est pourquoi nous disons que l’analyseur suggère de mettre en œuvre certaines “micro-optimisations” de code, qui ne garantissent pas les gains de performances.

Malgré l’inconvénient considéré, PVS-Studio analyzer agit comme un bon complément aux outils de profilage. De plus, lorsqu’il s’agit d’avertissements PVS-Studio, liés à l’optimisation, le code devient souvent plus simple et plus court. Cet effet est examiné plus en détail dans l’article “Explorer les microoptimisations en utilisant le Code Tizen comme exemple”.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.