Article de reference

Optimisation du programme

( Learn how and when to remove this message ) En informatique , l'optimisation de programme , l'optimisation de code ou l'optimisation logicielle est le processus de modificatio...

(Learn how and when to remove this message)

En informatique , l'optimisation de programme , l'optimisation de code ou l'optimisation logicielle est le processus de modification d'un système logiciel pour rendre certains aspects de celui-ci plus efficaces ou utiliser moins de ressources. En général, un programme informatique peut être optimisé pour qu'il s'exécute plus rapidement, ou pour le rendre capable de fonctionner avec moins de stockage de mémoire ou d'autres ressources, ou pour consommer moins d'énergie.

Général

Bien que le mot « optimisation » partage la même racine que le mot « optimal », il est rare que le processus d’optimisation produise un système véritablement optimal. Un système peut généralement être rendu optimal non pas en termes absolus, mais uniquement par rapport à une mesure de qualité donnée, qui peut être en contraste avec d’autres mesures possibles. Par conséquent, le système optimisé ne sera généralement optimal que dans une seule application ou pour un seul public. On peut réduire le temps nécessaire à un programme pour effectuer une tâche au prix d’une consommation de mémoire plus importante. Dans une application où l’espace mémoire est limité, on peut délibérément choisir un algorithme plus lent afin d’utiliser moins de mémoire. Souvent, il n’existe pas de conception « universelle » qui fonctionne bien dans tous les cas, de sorte que les ingénieurs font des compromis pour optimiser les attributs les plus intéressants. De plus, l’effort requis pour rendre un logiciel complètement optimal – incapable de toute amélioration supplémentaire – est presque toujours supérieur à ce qui est raisonnable par rapport aux avantages qui en résulteraient ; le processus d’optimisation peut donc être interrompu avant qu’une solution complètement optimale n’ait été atteinte. Heureusement, il arrive souvent que les plus grandes améliorations surviennent tôt dans le processus.

Même pour une mesure de qualité donnée (comme la vitesse d'exécution), la plupart des méthodes d'optimisation ne font qu'améliorer le résultat ; elles n'ont aucune prétention à produire un résultat optimal. La suroptimisation est le processus qui permet de trouver un résultat véritablement optimal.

Niveaux d'optimisation

L’optimisation peut se produire à plusieurs niveaux. En général, les niveaux les plus élevés ont un impact plus important et sont plus difficiles à modifier ultérieurement dans un projet, ce qui nécessite des changements importants ou une réécriture complète s’ils doivent être modifiés. Ainsi, l’optimisation peut généralement se faire par raffinement du plus haut vers le plus bas, les gains initiaux étant plus importants et obtenus avec moins de travail, et les gains ultérieurs étant plus faibles et nécessitant plus de travail. Cependant, dans certains cas, les performances globales dépendent des performances de parties de très bas niveau d’un programme, et de petits changements à un stade tardif ou une prise en compte précoce de détails de bas niveau peuvent avoir un impact démesuré. En général, une certaine attention est accordée à l’efficacité tout au long d’un projet (bien que cela varie considérablement), mais une optimisation majeure est souvent considérée comme un raffinement à effectuer tardivement, voire jamais. Sur les projets de longue durée, il existe généralement des cycles d’optimisation, où l’amélioration d’un domaine révèle des limites dans un autre, et ceux-ci sont généralement réduits lorsque les performances sont acceptables ou que les gains deviennent trop faibles ou coûteux.

Les performances font partie des spécifications d’un programme (un programme excessivement lent n’est pas adapté à son objectif : un jeu vidéo à 60 Hz (images par seconde) est acceptable, mais 6 images par seconde est inacceptablement saccadé). Les performances sont à prendre en compte dès le départ, pour garantir que le système est capable de fournir des performances suffisantes, et les premiers prototypes doivent avoir des performances à peu près acceptables pour que l’on soit sûr que le système final (avec optimisation) atteindra des performances acceptables. On omet parfois cela dans la croyance que l’optimisation peut toujours être effectuée plus tard, ce qui donne lieu à des systèmes prototypes beaucoup trop lents (souvent d’un ordre de grandeur ou plus) et à des systèmes qui sont finalement des échecs parce qu’ils ne peuvent pas atteindre leurs objectifs de performances du point de vue architectural, comme l’ Intel 432 (1981) ; ou des systèmes qui nécessitent des années de travail pour atteindre des performances acceptables, comme Java (1995), qui n’a atteint des performances acceptables qu’avec HotSpot (1999). Le degré de variation des performances entre le prototype et le système de production, ainsi que leur possibilité d’optimisation, peuvent constituer une source importante d’incertitude et de risque.

Niveau de conception

Au plus haut niveau, la conception peut être optimisée pour tirer le meilleur parti des ressources disponibles, compte tenu des objectifs, des contraintes et de l'utilisation/charge prévue. La conception architecturale d'un système affecte considérablement ses performances. Par exemple, un système lié à la latence du réseau (où la latence du réseau est la principale contrainte sur les performances globales) serait optimisé pour minimiser les déplacements réseau, en effectuant idéalement une seule demande (ou aucune demande, comme dans un protocole push ) plutôt que plusieurs allers-retours. Le choix de la conception dépend des objectifs : lors de la conception d'un compilateur , si la compilation rapide est la priorité clé, un compilateur à un seul passage est plus rapide qu'un compilateur à plusieurs passages (en supposant le même travail), mais si la vitesse du code de sortie est l'objectif, un compilateur à plusieurs passages plus lent remplit mieux l'objectif, même s'il prend plus de temps lui-même. Le choix de la plateforme et du langage de programmation se produit à ce niveau, et les changer fréquemment nécessite une réécriture complète, bien qu'un système modulaire puisse permettre la réécriture de seulement certains composants - par exemple, un programme Python peut réécrire des sections critiques en termes de performances en C. Dans un système distribué, le choix de l'architecture ( client-serveur , peer-to-peer , etc.) se produit au niveau de la conception et peut être difficile à modifier, en particulier si tous les composants ne peuvent pas être remplacés de manière synchronisée (par exemple, les anciens clients).

Algorithmes et structures de données

Étant donné une conception globale, un bon choix d' algorithmes et de structures de données efficaces , ainsi qu'une mise en œuvre efficace de ces algorithmes et structures de données viennent ensuite. Après la conception, le choix des algorithmes et des structures de données affecte l'efficacité plus que tout autre aspect du programme. En général, les structures de données sont plus difficiles à modifier que les algorithmes, car une hypothèse de structure de données et ses hypothèses de performance sont utilisées tout au long du programme, bien que cela puisse être minimisé par l'utilisation de types de données abstraits dans les définitions de fonctions et en limitant les définitions de structures de données concrètes à quelques endroits.

Pour les algorithmes, cela consiste principalement à s'assurer que les algorithmes sont constants O(1), logarithmiques O(log n ), linéaires O( n ) ou dans certains cas log-linéaires O( n log n ) dans l'entrée (à la fois dans l'espace et dans le temps). Les algorithmes de complexité quadratique O( n 2 ) ne parviennent pas à évoluer, et même les algorithmes linéaires posent des problèmes s'ils sont appelés à plusieurs reprises, et sont généralement remplacés par des algorithmes constants ou logarithmiques si possible.

Au-delà de l'ordre de croissance asymptotique, les facteurs constants comptent : un algorithme asymptotiquement plus lent peut être plus rapide ou plus petit (parce que plus simple) qu'un algorithme asymptotiquement plus rapide lorsqu'ils sont tous deux confrontés à une entrée de petite taille, ce qui peut être le cas dans la réalité. Souvent, un algorithme hybride fournira les meilleures performances, en raison de ce compromis changeant avec la taille.

Une technique générale pour améliorer les performances consiste à éviter le travail. Un bon exemple est l'utilisation d'un chemin rapide pour les cas courants, améliorant les performances en évitant le travail inutile. Par exemple, utiliser un algorithme de mise en page de texte simple pour le texte latin, et ne passer à un algorithme de mise en page complexe que pour les écritures complexes, telles que le devanagari . Une autre technique importante est la mise en cache, en particulier la mémorisation , qui évite les calculs redondants. En raison de l'importance de la mise en cache, il existe souvent de nombreux niveaux de mise en cache dans un système, ce qui peut entraîner des problèmes d'utilisation de la mémoire et des problèmes d'exactitude dus à des caches obsolètes.

Niveau du code source

Au-delà des algorithmes généraux et de leur implémentation sur une machine abstraite, des choix concrets au niveau du code source peuvent faire une différence significative. Par exemple, sur les premiers compilateurs C, while(1)était plus lent que for(;;)pour une boucle inconditionnelle, car while(1)évalué 1 puis avait un saut conditionnel qui testait si c'était vrai, tandis que for (;;)avait un saut inconditionnel . Certaines optimisations (comme celle-ci) peuvent aujourd'hui être effectuées en optimisant les compilateurs . Cela dépend du langage source, du langage machine cible et du compilateur, et peut être à la fois difficile à comprendre ou à prévoir et évoluer au fil du temps ; c'est un endroit clé où la compréhension des compilateurs et du code machine peut améliorer les performances. Le mouvement du code invariant en boucle et l'optimisation de la valeur de retour sont des exemples d'optimisations qui réduisent le besoin de variables auxiliaires et peuvent même entraîner des performances plus rapides en évitant les optimisations indirectes.

Niveau de construction

Entre le niveau source et le niveau compilation, les directives et les indicateurs de compilation peuvent être utilisés pour ajuster les options de performances dans le code source et le compilateur respectivement, comme l'utilisation de définitions de préprocesseur pour désactiver les fonctionnalités logicielles inutiles, l'optimisation pour des modèles de processeur ou des capacités matérielles spécifiques, ou la prédiction de ramifications , par exemple. Les systèmes de distribution de logiciels basés sur les sources tels que Ports de BSD et Portage de Gentoo peuvent tirer parti de cette forme d'optimisation.

Niveau de compilation

L’utilisation d’un compilateur optimisant tend à garantir que le programme exécutable est optimisé au moins autant que le compilateur peut le prédire.

Niveau d'assemblage

Au niveau le plus bas, l'écriture de code en utilisant un langage assembleur , conçu pour une plate-forme matérielle particulière, peut produire le code le plus efficace et le plus compact si le programmeur tire parti du répertoire complet des instructions machine . De nombreux systèmes d'exploitation utilisés sur des systèmes embarqués ont été traditionnellement écrits en code assembleur pour cette raison. Les programmes (autres que les très petits programmes) sont rarement écrits du début à la fin en assembleur en raison du temps et du coût impliqués. La plupart sont compilés à partir d'un langage de haut niveau vers l'assembleur et optimisés manuellement à partir de là. Lorsque l'efficacité et la taille sont moins importantes, de grandes parties peuvent être écrites dans un langage de haut niveau.

Avec des compilateurs d'optimisation plus modernes et la plus grande complexité des processeurs récents , il est plus difficile d'écrire du code plus efficace que ce que le compilateur génère, et peu de projets ont besoin de cette étape d'optimisation « ultime ».

La plupart des codes écrits aujourd'hui sont destinés à être exécutés sur autant de machines que possible. Par conséquent, les programmeurs et les compilateurs ne tirent pas toujours parti des instructions plus efficaces fournies par les nouveaux processeurs ou des particularités des anciens modèles. De plus, le code assembleur optimisé pour un processeur particulier sans utiliser de telles instructions peut toujours être sous-optimal sur un autre processeur, ce qui nécessite un réglage différent du code.

Généralement, aujourd'hui, plutôt que d'écrire en langage assembleur, les programmeurs utilisent un désassembleur pour analyser la sortie d'un compilateur et modifier le code source de haut niveau afin qu'il puisse être compilé plus efficacement, ou comprendre pourquoi il est inefficace.

Durée d'exécution

Les compilateurs juste-à-temps peuvent produire du code machine personnalisé basé sur des données d'exécution, au prix d'une surcharge de compilation. Cette technique remonte aux premiers moteurs d'expressions régulières et s'est généralisée avec Java HotSpot et V8 pour JavaScript. Dans certains cas, l'optimisation adaptative peut être en mesure d'effectuer une optimisation d'exécution dépassant les capacités des compilateurs statiques en ajustant dynamiquement les paramètres en fonction de l'entrée réelle ou d'autres facteurs.

L'optimisation guidée par profil est une technique d'optimisation de compilation anticipée (AOT) basée sur des profils d'exécution et est similaire à un analogue statique du « cas moyen » de la technique dynamique d'optimisation adaptative.

Le code auto-modifiable peut se modifier en réponse aux conditions d'exécution afin d'optimiser le code ; cela était plus courant dans les programmes en langage assembleur.

Certaines conceptions de processeur peuvent effectuer certaines optimisations au moment de l'exécution. Parmi les exemples, citons l'exécution dans le désordre , l'exécution spéculative , les pipelines d'instructions et les prédicteurs de branchement . Les compilateurs peuvent aider le programme à tirer parti de ces fonctionnalités du processeur, par exemple via la planification des instructions .

Optimisations dépendantes et indépendantes de la plateforme

L'optimisation du code peut également être classée en deux grandes catégories : techniques dépendantes de la plateforme et techniques indépendantes de la plateforme. Alors que ces dernières sont efficaces sur la plupart ou la totalité des plateformes, les techniques dépendantes de la plateforme utilisent des propriétés spécifiques d'une plateforme ou s'appuient sur des paramètres dépendant de la plateforme unique ou même du processeur unique. Il peut donc être nécessaire d'écrire ou de produire différentes versions du même code pour différents processeurs. Par exemple, dans le cas de l'optimisation au niveau de la compilation, les techniques indépendantes de la plateforme sont des techniques génériques (telles que le déroulement de boucle , la réduction des appels de fonction, les routines efficaces en mémoire, la réduction des conditions, etc.), qui ont un impact similaire sur la plupart des architectures de processeur. Un excellent exemple d'optimisation indépendante de la plateforme a été montré avec la boucle for interne, où il a été observé qu'une boucle avec une boucle for interne effectue plus de calculs par unité de temps qu'une boucle sans elle ou qu'une boucle avec une boucle while interne. Généralement, elles servent à réduire la longueur totale du chemin d'instructions nécessaire pour terminer le programme et/ou à réduire l'utilisation totale de la mémoire pendant le processus. D'autre part, les techniques dépendantes de la plate-forme impliquent la planification des instructions, le parallélisme au niveau des instructions , le parallélisme au niveau des données, les techniques d'optimisation du cache (c'est-à-dire les paramètres qui diffèrent selon les différentes plates-formes) et la planification optimale des instructions peut être différente même sur différents processeurs de la même architecture.

Réduction de la force

Les tâches de calcul peuvent être effectuées de différentes manières avec une efficacité variable. Une version plus efficace avec des fonctionnalités équivalentes est connue sous le nom de réduction de force . Par exemple, considérons l' extrait de code C suivant dont l'intention est d'obtenir la somme de tous les entiers de 1 à N :

int i , somme = 0 ; pour ( i = 1 ; i <= N ; ++ i ) { somme += i ; } printf ( "somme : %d " , somme );

Ce code peut (en supposant qu'il n'y ait pas de dépassement arithmétique ) être réécrit à l'aide d'une formule mathématique comme :

int somme = N * ( 1 + N ) / 2 ; printf ( "somme : %d " , somme );

L'optimisation, parfois effectuée automatiquement par un compilateur optimisant, consiste à sélectionner une méthode ( algorithme ) plus efficace en termes de calcul, tout en conservant les mêmes fonctionnalités. Voir Efficacité algorithmique pour une discussion sur certaines de ces techniques. Cependant, une amélioration significative des performances peut souvent être obtenue en supprimant les fonctionnalités superflues.

L'optimisation n'est pas toujours un processus évident ou intuitif. Dans l'exemple ci-dessus, la version « optimisée » pourrait en fait être plus lente que la version d'origine si N était suffisamment petit et si le matériel en question était beaucoup plus rapide pour effectuer des opérations d'addition et de bouclage que des opérations de multiplication et de division.

Compromis

Dans certains cas, l'optimisation repose sur l'utilisation d'algorithmes plus élaborés, faisant appel à des « cas particuliers » et à des « astuces » particulières et effectuant des compromis complexes. Un programme « entièrement optimisé » peut être plus difficile à comprendre et peut donc contenir plus de défauts que les versions non optimisées. Au-delà de l'élimination des anti-modèles évidents, certaines optimisations au niveau du code diminuent la maintenabilité.

L'optimisation se concentre généralement sur l'amélioration d'un ou deux aspects des performances : le temps d'exécution, l'utilisation de la mémoire, l'espace disque, la bande passante, la consommation d'énergie ou une autre ressource. Cela nécessite généralement un compromis, où un facteur est optimisé au détriment des autres. Par exemple, l'augmentation de la taille du cache améliore les performances d'exécution, mais augmente également la consommation de mémoire. D'autres compromis courants incluent la clarté et la concision du code.

Il existe des cas où le programmeur effectuant l'optimisation doit décider d'améliorer le logiciel pour certaines opérations, mais au prix de rendre d'autres opérations moins efficaces. Ces compromis peuvent parfois être de nature non technique, comme lorsqu'un concurrent a publié un résultat de référence qui doit être battu pour améliorer le succès commercial, mais qui s'accompagne peut-être du fardeau de rendre l'utilisation normale du logiciel moins efficace. De tels changements sont parfois appelés en plaisantant des pessimisations .

Goulots d'étranglement

L'optimisation peut consister à identifier un goulot d'étranglement dans un système, c'est-à-dire un composant qui limite les performances. En termes de code, il s'agira souvent d'un point chaud , c'est-à-dire d'une partie critique du code qui est le principal consommateur de la ressource nécessaire, bien qu'il puisse s'agir d'un autre facteur, comme la latence des E/S ou la bande passante du réseau.

En informatique, la consommation des ressources suit souvent une forme de distribution de loi de puissance , et le principe de Pareto peut être appliqué à l'optimisation des ressources en observant que 80 % des ressources sont généralement utilisées par 20 % des opérations. En génie logiciel, il est souvent plus juste de considérer que 90 % du temps d'exécution d'un programme informatique est consacré à l'exécution de 10 % du code (connu sous le nom de loi 90/10 dans ce contexte).

Les algorithmes et structures de données plus complexes fonctionnent bien avec de nombreux éléments, tandis que les algorithmes simples sont plus adaptés aux petites quantités de données : la configuration, le temps d'initialisation et les facteurs constants de l'algorithme plus complexe peuvent l'emporter sur les avantages, et donc un algorithme hybride ou adaptatif peut être plus rapide que n'importe quel algorithme unique. Un profileur de performances peut être utilisé pour affiner les décisions concernant les fonctionnalités qui correspondent à quelles conditions.

Dans certains cas, l'ajout de mémoire supplémentaire peut contribuer à accélérer l'exécution d'un programme. Par exemple, un programme de filtrage lit généralement chaque ligne, puis filtre et affiche cette ligne immédiatement. Cela n'utilise suffisamment de mémoire que pour une seule ligne, mais les performances sont généralement médiocres en raison de la latence de chaque lecture sur le disque. La mise en cache du résultat est tout aussi efficace, bien qu'elle nécessite également une utilisation plus importante de la mémoire.

Quand optimiser

L'optimisation peut réduire la lisibilité et ajouter du code qui n'est utilisé que pour améliorer les performances . Cela peut compliquer les programmes ou les systèmes, les rendant plus difficiles à maintenir et à déboguer. Par conséquent, l'optimisation ou le réglage des performances est souvent effectué à la fin de la phase de développement .

Donald Knuth a fait les deux déclarations suivantes sur l’optimisation :

« Nous devrions oublier les petites économies, disons 97 % du temps : l'optimisation prématurée est la racine de tous les maux. Pourtant, nous ne devrions pas laisser passer les opportunités qui se présentent dans ces 3 % critiques »

(Il a également attribué la citation à Tony Hoare plusieurs années plus tard, bien que cela puisse être une erreur puisque Hoare nie avoir inventé l'expression. )

« Dans les disciplines d'ingénierie établies, une amélioration de 12 %, facilement obtenue, n'est jamais considérée comme marginale et je crois que le même point de vue devrait prévaloir dans l'ingénierie logicielle »

L'expression « optimisation prématurée » est utilisée pour décrire une situation dans laquelle un programmeur laisse des considérations de performances affecter la conception d'un morceau de code. Cela peut entraîner une conception qui n'est pas aussi propre qu'elle aurait pu l'être ou un code incorrect, car le code est compliqué par l'optimisation et le programmeur est distrait par l'optimisation.

Au moment de décider s'il faut optimiser une partie spécifique du programme, la loi d'Amdahl doit toujours être prise en compte : l'impact sur le programme global dépend beaucoup du temps réellement passé dans cette partie spécifique, ce qui n'est pas toujours évident en examinant le code sans analyse des performances .

Une meilleure approche consiste donc à commencer par concevoir, coder à partir de la conception, puis profiler / évaluer le code obtenu pour voir quelles parties doivent être optimisées. Une conception simple et élégante est souvent plus facile à optimiser à ce stade, et le profilage peut révéler des problèmes de performances inattendus qui n'auraient pas été résolus par une optimisation prématurée.

En pratique, il est souvent nécessaire de garder à l’esprit les objectifs de performance lors de la première conception du logiciel, mais le programmeur équilibre les objectifs de conception et d’optimisation.

Les compilateurs et les systèmes d'exploitation modernes sont si efficaces que les gains de performances escomptés ne se matérialisent souvent pas. Par exemple, la mise en cache de données au niveau de l'application qui sont à nouveau mises en cache au niveau du système d'exploitation n'entraîne aucune amélioration de l'exécution. Malgré tout, il est rare que le programmeur supprime les optimisations ratées du code de production. Il est également vrai que les progrès du matériel vont le plus souvent annuler toute amélioration potentielle, mais le code obscurcissant persistera dans le futur bien après que son objectif ait été annulé.

Macros

L'optimisation lors du développement de code à l'aide de macros prend différentes formes dans différents langages.

Dans certains langages procéduraux, tels que C et C++ , les macros sont implémentées à l'aide de la substitution de jetons. De nos jours, les fonctions en ligne peuvent être utilisées comme une alternative de type sûr dans de nombreux cas. Dans les deux cas, le corps de la fonction en ligne peut ensuite subir d'autres optimisations au moment de la compilation par le compilateur, y compris le repliement constant , ce qui peut déplacer certains calculs au moment de la compilation.

Dans de nombreux langages de programmation fonctionnelle , les macros sont implémentées en utilisant la substitution au moment de l'analyse des arbres d'analyse/arbres de syntaxe abstraite, ce qui les rend plus sûres à utiliser. Étant donné que dans de nombreux cas, l'interprétation est utilisée, c'est une façon de garantir que de tels calculs ne sont effectués qu'au moment de l'analyse, et parfois la seule façon.

Lisp est à l'origine de ce type de macro, et ces macros sont souvent appelées « macros de type Lisp ». Un effet similaire peut être obtenu en utilisant la métaprogrammation de modèles en C++ .

Dans les deux cas, le travail est déplacé vers la compilation. La différence entre les macros C d'un côté et les macros de type Lisp et la métaprogrammation de modèles C++ de l'autre côté est que ces derniers outils permettent d'effectuer des calculs arbitraires au moment de la compilation/analyse, tandis que l'expansion des macros C n'effectue aucun calcul et repose sur la capacité de l'optimiseur à l'effectuer. De plus, les macros C ne prennent pas directement en charge la récursivité ou l'itération , elles ne sont donc pas complètes au sens de Turing .

Cependant, comme pour toute optimisation, il est souvent difficile de prédire où ces outils auront le plus d’impact avant la fin d’un projet.

Optimisation automatisée et manuelle

Voir aussi Catégorie :Optimisations du compilateur

L'optimisation peut être automatisée par des compilateurs ou réalisée par des programmeurs. Les gains sont généralement limités pour les optimisations locales et plus importants pour les optimisations globales. En général, l'optimisation la plus puissante consiste à trouver un algorithme supérieur .

L'optimisation d'un système entier est généralement entreprise par des programmeurs, car elle est trop complexe pour les optimiseurs automatisés. Dans cette situation, les programmeurs ou les administrateurs système modifient explicitement le code afin que le système global soit plus performant. Bien que cela puisse produire une meilleure efficacité, cela est beaucoup plus coûteux que les optimisations automatisées. Étant donné que de nombreux paramètres influencent les performances du programme, l'espace d'optimisation du programme est vaste. Les métaheuristiques et l'apprentissage automatique sont utilisés pour traiter la complexité de l'optimisation du programme.

Utilisez un profileur (ou un analyseur de performances ) pour trouver les sections du programme qui consomment le plus de ressources, c'est-à-dire le goulot d' étranglement. Les programmeurs pensent parfois avoir une idée précise de l'endroit où se trouve le goulot d'étranglement, mais l'intuition est souvent erronée. L'optimisation d'un morceau de code sans importance n'aura généralement que peu d'effet sur les performances globales.

Lorsque le goulot d'étranglement est localisé, l'optimisation commence généralement par une refonte de l'algorithme utilisé dans le programme. Le plus souvent, un algorithme particulier peut être spécifiquement adapté à un problème particulier, ce qui donne de meilleures performances qu'un algorithme générique. Par exemple, la tâche de tri d'une énorme liste d'éléments est généralement effectuée avec une routine de tri rapide , qui est l'un des algorithmes génériques les plus efficaces. Mais si certaines caractéristiques des éléments sont exploitables (par exemple, ils sont déjà disposés dans un ordre particulier), une méthode différente peut être utilisée, voire une routine de tri personnalisée.

Une fois que le programmeur est raisonnablement sûr que le meilleur algorithme est sélectionné, l'optimisation du code peut commencer. Les boucles peuvent être déroulées (pour réduire la surcharge de boucle, bien que cela puisse souvent conduire à une vitesse inférieure si cela surcharge le cache du processeur ), des types de données aussi petits que possible peuvent être utilisés, l'arithmétique entière peut être utilisée à la place de la virgule flottante, etc. (Voir l'article sur l'efficacité algorithmique pour ces techniques et d'autres.)

Les goulots d'étranglement des performances peuvent être dus aux limitations du langage plutôt qu'aux algorithmes ou aux structures de données utilisés dans le programme. Parfois, une partie critique du programme peut être réécrite dans un langage de programmation différent qui donne un accès plus direct à la machine sous-jacente. Par exemple, il est courant que les langages de très haut niveau comme Python aient des modules écrits en C pour une plus grande vitesse. Les programmes déjà écrits en C peuvent avoir des modules écrits en assembleur . Les programmes écrits en D peuvent utiliser l' assembleur en ligne .

La réécriture de sections « s'avère payante » dans ces circonstances en raison d'une « règle empirique » générale connue sous le nom de loi 90/10, qui stipule que 90 % du temps est consacré à 10 % du code et seulement 10 % du temps aux 90 % restants. Ainsi, consacrer un effort intellectuel à l'optimisation d'une petite partie du programme peut avoir un effet considérable sur la vitesse globale – si la ou les parties correctes peuvent être localisées.

L'optimisation manuelle a parfois pour effet secondaire de nuire à la lisibilité. Les optimisations de code doivent donc être soigneusement documentées (de préférence à l'aide de commentaires en ligne) et leur effet sur le développement futur doit être évalué.

Le programme qui effectue une optimisation automatique est appelé optimiseur . La plupart des optimiseurs sont intégrés aux compilateurs et fonctionnent pendant la compilation. Les optimiseurs peuvent souvent adapter le code généré à des processeurs spécifiques.

Aujourd'hui, les optimisations automatisées se limitent presque exclusivement à l'optimisation du compilateur . Cependant, comme les optimisations du compilateur se limitent généralement à un ensemble fixe d'optimisations plutôt générales, il existe une demande considérable d'optimiseurs capables d'accepter des descriptions de problèmes et d'optimisations spécifiques au langage, permettant à un ingénieur de spécifier des optimisations personnalisées. Les outils qui acceptent les descriptions d'optimisations sont appelés systèmes de transformation de programmes et commencent à être appliqués à des systèmes logiciels réels tels que C++.

Certains langages de haut niveau ( Eiffel , Esterel ) optimisent leurs programmes en utilisant un langage intermédiaire .

Le calcul en grille ou calcul distribué vise à optimiser l'ensemble du système, en déplaçant les tâches des ordinateurs à forte utilisation vers les ordinateurs avec des temps d'inactivité.

Temps nécessaire à l'optimisation

Parfois, le temps nécessaire pour procéder à l’optimisation peut être un problème.

L'optimisation du code existant n'ajoute généralement pas de nouvelles fonctionnalités et, pire encore, elle peut ajouter de nouveaux bugs dans le code déjà fonctionnel (comme le ferait n'importe quel changement). Étant donné que le code optimisé manuellement peut parfois avoir moins de « lisibilité » que le code non optimisé, l'optimisation peut également avoir un impact sur sa maintenabilité. L'optimisation a un prix et il est important de s'assurer que l'investissement en vaut la peine.

Un optimiseur automatique (ou compilateur optimisant , un programme qui effectue l'optimisation du code) peut lui-même avoir besoin d'être optimisé, soit pour améliorer l'efficacité de ses programmes cibles, soit pour accélérer son propre fonctionnement. Une compilation effectuée avec l'optimisation « activée » prend généralement plus de temps, bien que cela ne pose généralement problème que lorsque les programmes sont assez volumineux.

En particulier, pour les compilateurs juste-à-temps, les performances du composant de compilation au moment de l'exécution , exécuté avec son code cible, sont la clé pour améliorer la vitesse d'exécution globale.

Lectures complémentaires

  • Jon Bentley : Écrire des programmes efficaces , ISBN 0-13-970251-2 .
  • Donald Knuth : L'art de la programmation informatique
  • Comment écrire du code numérique rapidement : une petite introduction
  • « Ce que tout programmeur devrait savoir sur la mémoire » par Ulrich Drepper – explique la structure des sous-systèmes de mémoire modernes et suggère comment les utiliser efficacement
  • « Analyse et optimisation des performances multicœurs Linux en bref », diapositives de présentation de Philip Mucci
  • Optimisation de la programmation par Paul Hsieh
  • Écrire des programmes efficaces (« Les règles de Bentley ») par Jon Bentley
  • « Anti-modèles de performance » de Bart Smaalders
Original text
Rate this translation
Your feedback will be used to help improve Google Translate