J’ai pu constater que la plupart des développeurs ont en tête le fait que le code qu’ils écrivent doit s’exécuter vite. Je ne sais pas vraiment pourquoi, mais moi aussi, quelque part en moi, dans mon inconscient, j’ai également cette idée. Comme si on allait se faire radier du métier si on écrivait du code qui s’exécute lentement !
Pourtant, rares sont les cas où on nous pose réellement comme contrainte le fait que le logiciel doive fonctionner vite. Souvent, la première chose que les utilisateurs vont apprécier, c’est que le logiciel fonctionne. C’est seulement si le logiciel fonctionne que le temps d’exécution commencera à rentrer en compte.
Mais, rien à faire. Nous avons tous tendance à nous inventer des contraintes sur le temps d’exécution. Et lorsque nous commençons l’écriture du code, ces fausses contraintes peuvent vite devenir une obsession.
L’optimisation locale
La performance à tout prix
En ayant en tête que le code que nous écrivons doit s’exécuter vite, notre esprit se pervertit assez vite, et nous nous efforçons à chaque ligne que nous écrivons de bien penser à l’optimiser pour économiser les moindres instructions processeur ou le moindre octet de mémoire. C’est ce qu’on appelle l’optimisation locale.
Pour des gains peu significatifs
L’optimisation locale va permettre de faire en sorte qu’une petite portion de code donnée s’exécute plus vite, mais cela représente généralement quelques microsecondes de gagnées. Si le code en question est exécuté seulement deux fois par jour, le gain ne sera pas vraiment significatif.
En fait l’optimisation locale peut conduire à des gains très significatifs, mais seulement si elle est faite sur l’ensemble du code, ce qui est humainement impossible.
Optimisation automatique
Aujourd’hui, la plupart des développeurs ne codent plus en assembleur. Ils utilisent des langages de plus haut niveau qui sont compilés ou interprétés. Dans tous les cas, le code n’est pas exécuté tel que nous l’avons écrit, et je dirais même que plus le langage est de haut niveau, moins le code exécuté va ressembler au code original. Les compilateurs et autres interpréteurs sont généreux en terme d’optimisation. Ils sont capables de faire la plupart des optimisations que nous ferions nous, humains, mais également bien d’autres.
Il vont par exemple inliner des fonctions de façon à éviter de multiples appels de fonction qui, répétés un grand nombre de fois, finissent par coûter cher. D’une manière plus générale, ils recherchent des patterns connus dans le code, et y appliquent une optimisation. Dans les langages interprétés, l’optimisation peut être faite directement au runtime. Certains interpréteurs apprennent de l’exécution du code, ils repèrent les portions de code fréquemment utilisées et les optimisent plus que les autres, et se permettent parfois même d’ajouter des sortes de cache pour éviter d’exécuter des portions de code déterministes. C’est d’ailleurs pour cette raison qu’il faut chauffer les applications qui tournent dans un interpréteur pour qu’elles atteignent leur vitesse de croisière.
Il existe quelques optimisations locales que peut faire le développeur et que la machine ne fera pas. En revanche, celles que le développeur fait peuvent être contre-productives car elles complexifient le code. En plus d’entraver sa lisibilité, elles risquent de brouiller les pistes aux mécanismes automatiques d’optimisation et ainsi les empêcher d’opérer pleinement.
L’optimisation globale
Une bonne architecture
S’il y a un levier sur lequel l’humain peut vraiment agir de manière significative sur les performances d’un logiciel en utilisant son intelligence, c’est bien l’architecture du logiciel. Et c’est un point sur lequel aucune optimisation automatique ne peut être effectuée par les machines.
Déjà, ayons toujours à l’esprit qu’une bonne architecture est une architecture simple et basée sur le bon sens.
Des points comme le choix des fondations techniques (bases de données, bibliothèques) peuvent être déterminants pour les performances d’une application. Parfois, on sait d’avance que certains points seront problématiques en terme de performance, et à ce moment-là on peut davantage anticiper en choisissant les algorithmes adéquats et en prévoyant éventuellement la mise en place des caches. Mais attention à le faire seulement quand on sait que c’est nécessaire et à ne pas s’inventer des faux problèmes, notre esprit perverti n’est pas toujours objectif à ce sujet.
Un découpage cohérent en composants et une bonne modélisation des données sont également des points importants, et pas seulement au sujet des performances. D’une manière générale, une bonne conception n’améliorera pas forcément les performances. En revanche, une mauvaise conception peut engendrer des performances désastreuses. Et malheureusement, lorsqu’on se rend compte que les faibles performances d’une application sont dues à son architecture, il est difficile de remettre en cause ses fondements et ça peut coûter très cher.
Incompatible avec le niveau local
Comme on l’a vu, une optimisation locale omniprésente est très nuisible à la lisibilité et à la simplicité du code. Cela implique que nous ne pouvons pas prendre de recul sur le code. On a alors, comme on dit, la tête dans le guidon et c’est une posture très défavorable à la prise de bonnes décisions au sujet de la conception globale.
Mon code est trop lent, que dois-je faire ?
Même avec une bonne conception globale, on peut tomber sur des portions d’un logiciel qui sont plus lentes que ce qu’on souhaiterait. Il ne faut pas paniquer, en s’attaquant au problème on peut souvent espérer de bonnes marges de progression.
Identifier le coupable
La première chose à faire est d’utiliser un profiler, un outil en charge d’analyser le temps d’exécution ou la consommation mémoire d’un code. Ce genre d’outil permet généralement de trouver très rapidement quels sont les portions de code (fonctions par exemple) qui sont les plus longues. Très souvent, on va tomber sur un goulot d’étranglement, une fonction qui représente à elle seule quelque chose comme la moitié du temps total d’exécution d’une portion de code, si bien que si on parvient à l’accélérer drastiquement, on peut diviser par deux la vitesse que met la portion de code à s’exécuter. Plutôt rentable !
Et l’éliminer !
Une fois identifié, le goulot d’étranglement doit être éliminé. Le profiler permet la plupart du temps de vite trouver ce qui est responsable de ce ralentissement. Il peut s’agir d’un problème de conception au niveau local, par exemple une fonction qui est appelée à chaque itération d’une boucle alors que ce n’est pas nécessaire, dans ce cas la correction est simple. Cela peut être aussi une fonction qui est souvent appelée avec les mêmes paramètres, auquel cas la mise en place d’un cache permettra très certainement de régler définitivement le problème. Parfois, le problème peut être dû à un nombre élevé de communications avec des composants tiers (base de données par exemple), et à ce moment-là il faudra essayer de limiter ce nombre d’échanges entre composants.
Très souvent, le problème sera vite réglé. Il sera d’ailleurs plus vite corrigé encore si le code est propre et pas prématurément et localement optimisé, parce qu’il est plus lisible et plus évolutif. En revanche, on peut parfois tomber sur un os, lié par exemple aux fondations du logiciel. Dans de tels cas, la marge de progression est réduite. Il faut alors soit régler le problème de conception, soit aller creuser partout ailleurs pour essayer de compenser, mais dans tous les cas ça sera douloureux.
D’une manière générale, je pense qu’on peut considérer que sur un code bien conçu, bien écrit et sur lequel on n’a jamais cherché à faire des optimisations, on a toujours une marge de progression importante.
Conclusion
L’optimisation prématurée, même si elle est très tentante, est à proscrire. En ne s’en occupant pas, on gagne du temps. Une partie de ce temps peut être investi dans une réelle réflexion sur la conception globale et l’architecture de l’application qui est souvent négligée. On est alors en bonne voie pour rester maître du temps d’exécution de son logiciel !
Mesurons avant d’agir et ne nous inventons pas de faux problèmes !
“Can you fetch me a glass of water please?” *35 mins later* “What are you doing!?”, “I’m building you a scalable drink delivery system”.
— I Am Devloper (@iamdevloper) April 6, 2014
L’image d’en-tête provient de Flickr.
C’est pas la taille qui compte !
En terme de profiling VTune est vraiment excellent
https://software.intel.com/en-us/intel-vtune-amplifier-xe