Optimisation du code : la manière optimale d'optimiser
Publié: 2022-03-11L'optimisation des performances est l'une des plus grandes menaces pour votre code.
Vous pensez peut-être, pas une autre de ces personnes . Je comprends. L'optimisation de toute sorte devrait clairement être une bonne chose, à en juger par son étymologie, donc naturellement, vous voulez être bon dans ce domaine.
Pas seulement pour vous démarquer de la foule en tant que meilleur développeur. Pas seulement pour éviter d'être "Dan" sur The Daily WTF , mais parce que vous pensez que l'optimisation du code est la bonne chose à faire. Vous êtes fier de votre travail.
Le matériel informatique devient de plus en plus rapide et les logiciels plus faciles à créer, mais quelle que soit la chose simple que vous voulez juste pouvoir faire, bon sang prend toujours plus de temps que la précédente. Vous secouez la tête face à ce phénomène (d'ailleurs, connu sous le nom de loi de Wirth) et décidez de renverser cette tendance.
C'est noble de ta part, mais arrête.
Arrête!
Vous courez le plus grand danger de contrecarrer vos propres objectifs, quelle que soit votre expérience en programmation.
Comment? Revenons en arrière.
Tout d'abord, qu'est - ce que l'optimisation de code ?
Souvent, lorsque nous le définissons, nous supposons que nous voulons que le code soit plus performant . Nous disons que l'optimisation du code consiste à écrire ou à réécrire du code afin qu'un programme utilise le moins de mémoire ou d'espace disque possible, minimise son temps CPU ou sa bande passante réseau, ou utilise au mieux les cœurs supplémentaires.
En pratique, nous adoptons parfois par défaut une autre définition : écrire moins de code.
Mais le code préventif que vous écrivez avec cet objectif est encore plus susceptible de devenir une épine dans le pied de quelqu'un. À qui? La prochaine personne malchanceuse qui doit comprendre votre code, qui peut même être vous-même. Et quelqu'un d'intelligent et capable, comme vous, peut éviter l'auto-sabotage : Gardez vos fins nobles mais réévaluez vos moyens, malgré le fait qu'ils semblent incontestablement intuitifs.
L'optimisation du code est donc un terme un peu vague. C'est avant même que nous n'envisagions certaines des autres façons d'optimiser le code, que nous verrons ci-dessous.
Commençons par écouter les conseils des sages alors que nous explorons ensemble les fameuses règles d'optimisation du code de Jackson :
- Ne le faites pas.
- (Réservé aux experts !) Ne le faites pas encore .
1. Ne le faites pas : canaliser le perfectionnisme
Je vais commencer par un exemple extrême plutôt embarrassant d'une époque où, il y a longtemps, je commençais à me mouiller les pieds dans le monde merveilleux de SQL. Le problème était que j'ai alors marché sur le gâteau et je ne voulais plus le manger car il était mouillé et commençait à sentir les pieds.
J'étais juste en train de me mouiller les pieds dans le monde merveilleux de SQL. Le problème était que j'ai alors marché sur le gâteau…
Attendez. Permettez-moi de sortir de cette épave de voiture d'une métaphore que je viens de faire et d'expliquer.
Je faisais de la R&D pour une application intranet, dont j'espérais qu'elle deviendrait un jour un système de gestion complètement intégré pour la petite entreprise où je travaillais. Il suivrait tout pour eux, et contrairement à leur système actuel, il ne perdrait jamais leurs données, car il serait soutenu par un SGBDR, et non par le fichier plat feuilleté maison que d'autres développeurs avaient utilisé. Je voulais tout concevoir le plus intelligemment possible dès le début car j'avais une page blanche. Les idées pour ce système explosaient comme un feu d'artifice dans mon esprit, et j'ai commencé à concevoir des tableaux de contacts et leurs nombreuses variations contextuelles pour un CRM, des modules de comptabilité, d'inventaire, d'achat, de CMS et de gestion de projet, que je serais bientôt en train de dogfooder.
Que tout s'est arrêté, en termes de développement et de performances, à cause de… vous l'avez deviné, l'optimisation.
J'ai vu que les objets (représentés sous forme de lignes de tableau) pouvaient avoir de nombreuses relations différentes entre eux dans le monde réel et que nous pourrions bénéficier du suivi de ces relations : nous conserverions plus d'informations et pourrions éventuellement automatiser l'analyse commerciale partout. Voyant cela comme un problème d'ingénierie, j'ai fait quelque chose qui ressemblait à une optimisation de la flexibilité du système.
À ce stade, il est important de faire attention à votre visage, car je ne serai pas tenu responsable si votre paume vous fait mal. Prêt? J'ai créé deux tables: relationship et une avec une référence de clé étrangère, relationship_type . relationship peut faire référence à deux lignes quelconques n'importe où dans l'ensemble de la base de données et décrire la nature de la relation entre elles.
Oh, mec. Je venais juste d'optimiser tellement cette flexibilité.
Trop, en fait. Maintenant, j'avais un nouveau problème : un type de relationship_type donné n'aurait naturellement aucun sens entre chaque combinaison de lignes donnée. Bien qu'il puisse sembler logique qu'une person ait une relation d' employed by avec une company , cela ne pourrait jamais être sémantiquement équivalent à la relation entre, par exemple, deux document .
D'accord pas de problème. Nous allons simplement ajouter deux colonnes à relationship_type , en spécifiant à quelles tables cette relation pourrait s'appliquer. (Points bonus ici si vous devinez que j'ai pensé à normaliser cela en déplaçant ces deux colonnes vers une nouvelle table faisant référence à relationship_type.id , afin que les relations qui pourraient s'appliquer sémantiquement à plus d'une paire de tables n'aient pas les noms de table dupliqués. Après tout, si j'avais besoin de changer le nom d'une table et que j'oubliais de le mettre à jour dans toutes les lignes applicables, cela pourrait créer un bogue ! Rétrospectivement, au moins les bogues auraient fourni de la nourriture aux araignées habitant mon crâne.)
Heureusement, j'ai perdu connaissance dans une tempête d'indices avant de voyager trop loin sur ce chemin. Quand je me suis réveillé, j'ai réalisé que j'avais réussi, plus ou moins, à réimplémenter les tables internes liées aux clés étrangères du SGBDR au-dessus de lui-même. Normalement, j'apprécie les moments qui se terminent par la proclamation fanfaronne que "je suis tellement méta", mais ce n'était malheureusement pas l'un d'entre eux. Oubliez l'échec de la mise à l'échelle - l'horrible ballonnement de cette conception a rendu le back-end de mon application encore simple, dont la base de données était à peine remplie de données de test, presque inutilisable.
Revenons en arrière une seconde et examinons deux des nombreuses mesures en jeu ici. L'un est la flexibilité, qui avait été mon objectif déclaré. Dans ce cas, mon optimisation, étant de nature architecturale, n'était même pas prématurée :
(Nous y reviendrons plus en détail dans mon article récemment publié, Comment éviter la malédiction de l'optimisation prématurée.) Néanmoins, ma solution a échoué de manière spectaculaire en étant beaucoup trop flexible. L'autre métrique, l'évolutivité, était celle que je n'envisageais même pas encore mais que j'ai réussi à détruire au moins aussi spectaculairement avec des dommages collatéraux.
C'est vrai, "Oh."
Ce fut une leçon puissante pour moi sur la façon dont l'optimisation peut aller complètement de travers. Mon perfectionnisme a complètement implosé : Mon intelligence m'avait amené à produire l'une des solutions les plus objectivement non intelligentes que j'ai jamais faites.
Optimisez vos habitudes, pas votre code
Lorsque vous vous surprenez à avoir tendance à refactoriser avant même d'avoir un prototype fonctionnel et une suite de tests pour prouver son exactitude, réfléchissez à d'autres endroits où vous pouvez canaliser cette impulsion. Sudoku et Mensa sont super, mais peut-être que quelque chose qui profitera directement à votre projet serait mieux :
- Sécurité
- Stabilité d'exécution
- Clarté et style
- Efficacité de codage
- Tester l'efficacité
- Profilage
- Votre boîte à outils/DE
- SEC (Ne vous répétez pas)
Mais attention : l'optimisation de l'un d'entre eux en particulier se fera au détriment des autres. À tout le moins, cela se fait au prix du temps.
C'est ici qu'il est facile de voir à quel point il y a de l'art dans la création de code. Pour chacun des éléments ci-dessus, je peux vous raconter des histoires sur la façon dont trop ou trop peu a été considéré comme le mauvais choix. Qui fait la réflexion ici est également une partie importante du contexte.
Par exemple, concernant DRY : lors d'un travail que j'ai eu, j'ai hérité d'une base de code contenant au moins 80 % d'instructions redondantes, car son auteur ne savait apparemment pas comment et quand écrire une fonction. Les 20 % restants du code étaient auto-similaires, ce qui prêtait à confusion.
J'ai été chargé d'y ajouter quelques fonctionnalités. Une de ces fonctionnalités devrait être répétée dans tout le code à implémenter, et tout code futur devrait être soigneusement copié pour utiliser la nouvelle fonctionnalité.
De toute évidence, il devait être refactorisé uniquement pour ma propre santé mentale (valeur élevée) et pour tous les futurs développeurs. Mais, comme j'étais nouveau dans la base de code, j'ai d'abord écrit des tests pour m'assurer que ma refactorisation n'introduisait aucune régression. En fait, c'est exactement ce qu'ils ont fait : j'ai attrapé deux bogues en cours de route que je n'aurais pas remarqués parmi toutes les sorties de charabia produites par le script.
Au final, j'ai trouvé que j'avais plutôt bien fait. Après le refactoring, j'ai impressionné mon patron d'avoir implémenté ce qui avait été considéré comme une fonctionnalité difficile avec quelques lignes de code simples ; de plus, le code était globalement un ordre de grandeur plus performant. Mais peu de temps après, le même patron m'a dit que j'avais été trop lent et que le projet devait déjà être terminé. Traduction : L'efficacité du codage était une priorité plus élevée.
Attention : l'optimisation d'un [aspect] particulier se fera au détriment des autres. À tout le moins, cela se fait au prix du temps.
Je pense toujours avoir pris le bon cap là-bas, même si l'optimisation du code n'était pas directement appréciée par mon patron à l'époque. Sans le refactoring et les tests, je pense qu'il aurait fallu plus de temps pour être correct, c'est-à-dire que se concentrer sur la vitesse de codage l'aurait en fait contrecarré. (Hé, c'est notre thème !)
Comparez cela avec un travail que j'ai fait sur un petit projet parallèle à moi. Dans le projet, j'essayais un nouveau moteur de template, et je voulais prendre de bonnes habitudes dès le départ, même si essayer le nouveau moteur de template n'était pas l'objectif final du projet.
Dès que j'ai remarqué que quelques blocs que j'avais ajoutés étaient très similaires les uns aux autres, et de plus, chaque bloc nécessitait de se référer trois fois à la même variable, la cloche DRY a sonné dans ma tête, et je me suis mis à trouver le bon façon de faire ce que j'essayais de faire avec ce moteur de modèle.
Il s'est avéré, après quelques heures de débogage infructueux, que ce n'était actuellement pas possible avec le moteur de template de la manière que j'imaginais. Non seulement il n'y avait pas de solution SÈCHE parfaite ; il n'y avait pas de solution sèche du tout !
En essayant d'optimiser cette seule valeur qui m'appartenait, j'ai complètement déraillé mon efficacité de codage et mon bonheur, car ce détour a coûté à mon projet les progrès que j'aurais pu avoir ce jour-là.
Même alors, avais-je entièrement tort ? Parfois, cela vaut la peine d'investir un peu, en particulier dans un nouveau contexte technologique, pour connaître les meilleures pratiques plus tôt que tard. Moins de code à réécrire et de mauvaises habitudes à défaire, n'est-ce pas ?
Non, je pense qu'il était même imprudent de chercher un moyen de réduire la répétition dans mon code, ce qui contraste fortement avec mon attitude dans l'anecdote précédente. La raison en est que le contexte est primordial : j'explorais une nouvelle technologie sur un petit projet de jeu, sans m'installer sur le long terme. Quelques lignes supplémentaires et des répétitions n'auraient fait de mal à personne, mais la perte de concentration m'a blessé, moi et mon projet.
Attendez, alors rechercher les meilleures pratiques peut être une mauvaise habitude ? Parfois. Si mon objectif principal était d'apprendre le nouveau moteur, ou d'apprendre en général, alors cela aurait été du temps bien dépensé : bricoler, trouver les limites, découvrir des fonctionnalités sans rapport et des pièges via la recherche. Mais j'avais oublié que ce n'était pas mon objectif principal, et cela m'a coûté.
C'est un art, comme je l'ai dit. Et le développement de cet art bénéficie du rappel, Ne le fais pas . Cela vous permet au moins de déterminer quelles valeurs sont en jeu pendant que vous travaillez et lesquelles sont les plus importantes pour vous dans votre contexte.
Qu'en est-il de cette deuxième règle ? Quand peut-on réellement optimiser ?
2. Ne le faites pas encore : quelqu'un l'a déjà fait
OK, que ce soit par vous ou par quelqu'un d'autre, vous trouvez que votre architecture est déjà définie, les flux de données ont été pensés et documentés, et il est temps de coder.
Allons encore plus loin avec Ne le faites pas encore : Ne le codez même pas encore .
Cela en soi peut ressembler à une optimisation prématurée, mais c'est une exception importante. Pourquoi? Pour éviter le redoutable syndrome NIHS, ou « non inventé ici », en supposant que vos priorités incluent les performances du code et la réduction du temps de développement. Sinon, si vos objectifs sont entièrement axés sur l'apprentissage, vous pouvez ignorer cette section suivante.
Bien qu'il soit possible que les gens réinventent la roue carrée par pur orgueil, je crois que des gens honnêtes et humbles, comme vous et moi, peuvent faire cette erreur uniquement en ne connaissant pas toutes les options qui s'offrent à nous. Connaître toutes les options de chaque API et outil de votre pile et les suivre au fur et à mesure de leur croissance et de leur évolution représente certainement beaucoup de travail.
Mais, mettre ce temps est ce qui fait de vous un expert et vous empêche d'être la zillionième personne sur CodeSOD à être maudite et moquée pour la piste de dévastation laissée par leur vision fascinante des calculatrices date-heure ou des manipulateurs de chaînes.
(Un bon contrepoint à ce modèle général est l'ancienne API Java Calendar , mais elle a depuis été corrigée.)
Vérifiez votre bibliothèque standard, vérifiez l'écosystème de votre infrastructure, recherchez les FOSS qui résolvent déjà votre problème
Il y a de fortes chances que les concepts que vous traitez aient des noms assez standard et bien connus, donc une recherche rapide sur Internet vous fera gagner beaucoup de temps.
Par exemple, je me préparais récemment à faire une analyse des stratégies d'IA pour un jeu de société. Je me suis réveillé un matin en réalisant que l'analyse que je planifiais pourrait être effectuée de manière plus efficace si j'utilisais simplement un certain concept de combinatoire dont je me souvenais. N'étant pas intéressé à comprendre moi-même l'algorithme de ce concept à ce moment-là, j'étais déjà en avance en connaissant le bon nom à rechercher. Cependant, j'ai découvert qu'après environ 50 minutes de recherche et d'essais de code préliminaire, je n'avais pas réussi à transformer le pseudo-code à moitié fini que j'avais trouvé en une implémentation correcte. (Pouvez-vous croire qu'il existe un article de blog dans lequel l'auteur suppose une sortie d'algorithme incorrecte, implémente l'algorithme de manière incorrecte pour correspondre aux hypothèses, les commentateurs le soulignent, puis des années plus tard, ce n'est toujours pas corrigé ?) À ce stade, mon thé du matin lancé, et j'ai cherché [name of concept] [my programming language] . 30 secondes plus tard, j'avais un code manifestement correct de GitHub et je passais à ce que j'avais réellement voulu faire. Le simple fait d'être précis et d'inclure le langage, au lieu de supposer que je devrais l'implémenter moi-même, signifiait tout.
Il est temps de concevoir votre structure de données et d'implémenter votre algorithme
… Encore une fois, ne jouez pas au golf codé. Prioriser l'exactitude et la clarté dans les projets du monde réel.
OK, vous avez donc regardé, et rien ne résout déjà votre problème dans votre chaîne d'outils, ou sous licence libérale sur le Web. Vous déployez le vôtre.
Aucun problème. Le conseil est simple, dans cet ordre :
- Concevez-le de manière à ce qu'il soit simple à expliquer à un programmeur novice.
- Rédigez un test qui correspond aux attentes produites par cette conception.
- Écrivez votre code de sorte qu'un programmeur novice puisse facilement en glaner la conception.
Simple, mais peut-être difficile à suivre. C'est là que les habitudes de codage et les odeurs de code, l'art, l'artisanat et l'élégance entrent en jeu. Il y a évidemment un aspect technique dans ce que vous faites à ce stade, mais encore une fois, ne jouez pas au golf codé. Prioriser l'exactitude et la clarté dans les projets du monde réel.
Si vous aimez les vidéos, voici celle de quelqu'un qui suit plus ou moins les étapes ci-dessus. Pour ceux qui n'aiment pas la vidéo, je vais résumer : il s'agit d'un test de codage d'algorithme lors d'un entretien d'embauche chez Google. La personne interrogée conçoit d'abord l'algorithme d'une manière facile à communiquer. Avant d'écrire un code, il existe des exemples de la sortie attendue par une conception fonctionnelle. Ensuite, le code suit naturellement.
En ce qui concerne les tests eux-mêmes, je sais que dans certains cercles, le développement piloté par les tests peut être controversé. Je pense qu'une partie de la raison en est que cela peut être exagéré, poursuivi religieusement au point de sacrifier le temps de développement. (Encore une fois, nous nous tirons une balle dans le pied en essayant d'optimiser même une seule variable dès le départ.) Même Kent Beck ne pousse pas le TDD à un tel extrême, et il a inventé la programmation extrême et a écrit le livre sur le TDD. Commencez donc par quelque chose de simple pour vous assurer que votre sortie est correcte. Après tout, vous le feriez de toute façon manuellement après le codage, n'est-ce pas ? (Mes excuses si vous êtes un tel programmeur rockstar que vous n'exécutez même pas votre code après l'avoir écrit pour la première fois. Dans ce cas, vous pourriez peut-être envisager de laisser les futurs responsables de votre code avec un test juste pour que vous sachiez qu'ils ne le feront pas cassez votre implémentation géniale.) Ainsi, au lieu de faire un diff manuel et visuel, avec un test en place, vous laissez déjà l'ordinateur faire ce travail pour vous.
Pendant le processus plutôt mécanique d'implémentation de vos algorithmes et structures de données, évitez de faire des optimisations ligne par ligne, et ne pensez même pas à utiliser un langage externe personnalisé de bas niveau (Assembly si vous codez en C, C si vous codez en Perl, etc.) à ce stade. La raison est simple : si votre algorithme est entièrement remplacé, et vous ne saurez que plus tard dans le processus si cela est nécessaire, vos efforts d'optimisation de bas niveau n'auront finalement aucun effet.
Un exemple ECMAScript
Sur l'excellent site de revue de code communautaire exercism.io, j'ai récemment trouvé un exercice qui suggérait explicitement d'essayer soit d'optimiser pour la déduplication, soit pour plus de clarté. J'ai optimisé pour la déduplication, juste pour montrer à quel point les choses peuvent devenir ridicules si vous poussez trop loin DRY, un état d'esprit de codage par ailleurs bénéfique, comme je l'ai mentionné ci-dessus. Voici à quoi ressemblait mon code :
const zeroPhrase = "No more"; const wallPhrase = " on the wall"; const standardizeNumber = number => { if (number === 0) { return zeroPhrase; } return '' + number; } const bottlePhrase = number => { const possibleS = (number === 1) ? '' : 's'; return standardizeNumber(number) + " bottle" + possibleS + " of beer"; } export default class Beer { static verse(number) { const nextNumber = (number === 0) ? 99 : (number - 1); const thisBottlePhrase = bottlePhrase(number); const nextBottlePhrase = bottlePhrase(nextNumber); let phrase = thisBottlePhrase + wallPhrase + ", " + thisBottlePhrase.toLowerCase() + ".\n"; if (number === 0) { phrase += "Go to the store and buy some more"; } else { const bottleReference = (number === 1) ? "it" : "one"; phrase += "Take " + bottleReference + " down and pass it around"; } return phrase + ", " + nextBottlePhrase.toLowerCase() + wallPhrase + ".\n"; } static sing(start = 99, end = 0) { return Array.from(Array(start - end + 1).keys()).map(offset => { return this.verse(start - offset); }).join('\n'); } } Il n'y a pratiquement aucune duplication de chaînes ! En l'écrivant de cette façon, j'ai implémenté manuellement une forme de compression de texte pour la chanson de la bière (mais uniquement pour la chanson de la bière). Quel était l'avantage, exactement ? Eh bien, disons que vous voulez chanter à propos de boire de la bière dans des canettes au lieu de bouteilles. Je pourrais accomplir cela en changeant une seule instance de bottle en can .
Agréable!
…droit?
Non, car alors tous les tests échouent. OK, c'est facile à résoudre : nous allons simplement effectuer une recherche et remplacer la bottle dans la spécification de test unitaire. Et c'est exactement aussi facile à faire que de le faire au code lui-même en premier lieu et comporte les mêmes risques de casser des choses par inadvertance.
Pendant ce temps, mes variables seront étrangement nommées par la suite, avec des choses comme bottlePhrase n'ayant rien à voir avec les bouteilles . La seule façon d'éviter cela est d'avoir prévu exactement le type de changement qui serait effectué et d'utiliser un terme plus générique comme vessel ou container à la place de bottle dans mes noms de variables.
La sagesse de la pérennité de cette manière est assez discutable. Quelles sont les chances que vous vouliez changer quoi que ce soit ? Et si vous le faites, ce que vous changez fonctionnera-t-il si facilement ? Dans l'exemple de bottlePhrase , que se passe-t-il si vous voulez localiser dans une langue qui a plus de deux formes plurielles ? C'est vrai, refactorisez le temps, et le code peut sembler encore pire par la suite.
Mais lorsque vos besoins changent et que vous n'essayez pas seulement de les anticiper, il est peut-être temps de refactoriser. Ou peut-être pouvez-vous encore le reporter : combien de types de navires ou de localisations allez-vous ajouter, de manière réaliste ? Quoi qu'il en soit, lorsque vous avez besoin d'équilibrer votre déduplication avec clarté, cela vaut la peine de regarder cette démonstration de Katrina Owen.
Revenons à mon horrible exemple : Inutile de dire que les avantages de la déduplication ne se réalisent même pas tant que ça ici. En attendant, combien cela a-t-il coûté ?
En plus de prendre plus de temps à écrire en premier lieu, c'est maintenant un peu moins trivial à lire, déboguer et maintenir. Imaginez le niveau de lisibilité avec une quantité modérée de duplication autorisée. Par exemple, avoir chacune des quatre variantes de versets épelées.
Mais nous n'avons toujours pas optimisé !
Maintenant que votre algorithme est implémenté et que vous avez prouvé que sa sortie est correcte, félicitations ! Vous avez une base !
Enfin, il est temps de... optimiser, n'est-ce pas ? Non, encore Ne le faites pas encore . Il est temps de prendre votre baseline et de faire un joli benchmark . Fixez un seuil pour vos attentes autour de cela et collez-le dans votre suite de tests. Ensuite, si quelque chose ralentit soudainement ce code, même s'il fonctionne toujours, vous le saurez avant qu'il ne sorte.
Retenez toujours l'optimisation jusqu'à ce que vous ayez mis en œuvre toute une partie de l'expérience utilisateur pertinente. Jusque-là, vous ciblez peut-être une partie du code entièrement différente de celle dont vous avez besoin.
Allez terminer votre application (ou composant), si vous ne l'avez pas déjà fait, en définissant toutes vos lignes de base de référence algorithmiques au fur et à mesure.
Une fois cela fait, c'est le moment idéal pour créer et comparer des tests de bout en bout couvrant les scénarios d'utilisation réels les plus courants de votre système.
Peut-être que vous trouverez que tout va bien.
Ou peut-être avez-vous déterminé que, dans son contexte réel, quelque chose est trop lent ou prend trop de mémoire.
OK, maintenant vous pouvez optimiser
Il n'y a qu'une seule façon d'être objectif à ce sujet. Il est temps de sortir des graphiques de flamme et d'autres outils de profilage. Les ingénieurs expérimentés peuvent ou non deviner mieux plus souvent que les novices, mais ce n'est pas la question : la seule façon de savoir avec certitude est de profiler. C'est toujours la première chose à faire dans le processus d'optimisation du code pour les performances.
Vous pouvez profiler au cours d'un test de bout en bout donné pour déterminer ce qui aura vraiment le plus grand impact. (Et plus tard, après le déploiement, la surveillance des modèles d'utilisation est un excellent moyen de rester au fait des aspects de votre système les plus pertinents à mesurer à l'avenir.)
Notez que vous n'essayez pas d'utiliser le profileur dans toute sa profondeur - vous recherchez plus un profilage au niveau de la fonction qu'un profilage au niveau de l'instruction, généralement, car votre objectif à ce stade est uniquement de savoir quel algorithme est le goulot d'étranglement .
Maintenant que vous avez utilisé le profilage pour identifier le goulot d'étranglement de votre système, vous pouvez maintenant tenter d'optimiser, en étant sûr que votre optimisation en vaut la peine. Vous pouvez également prouver l'efficacité (ou l'inefficacité) de votre tentative, grâce aux repères de référence que vous avez réalisés en cours de route.
Techniques générales
Tout d'abord, pensez à rester à haut niveau le plus longtemps possible :
Le saviez-vous? L'ultime astuce d'optimisation universelle, s'applique dans tous les cas :
– Lars Doucet (@larsiusprime) 30 mars 2017
- Dessinez moins de choses
- Mettre à jour moins de choses
Au niveau de l'algorithme global, une technique est la réduction de la force. Dans le cas de la réduction des boucles aux formules, cependant, n'oubliez pas de laisser des commentaires. Tout le monde ne connaît pas ou ne se souvient pas de toutes les formules combinatoires. Soyez également prudent avec votre utilisation des mathématiques : parfois, ce que vous pensez être une réduction de la force ne l'est pas, en fin de compte. Par exemple, supposons que x * (y + z) ait une signification algorithmique claire. Si votre cerveau a été entraîné à un moment donné, pour une raison quelconque, à dissocier automatiquement des termes similaires, vous pourriez être tenté de réécrire cela sous la forme x * y + x * z . D'une part, cela met une barrière entre le lecteur et la signification algorithmique claire qui était là. (Pire encore, c'est maintenant en fait moins efficace en raison de l'opération de multiplication supplémentaire requise. C'est comme si le déroulement de la boucle venait de coller son pantalon.) Dans tous les cas, une note rapide sur vos intentions irait très loin et pourrait même vous aider à voir votre propre erreur avant de la commettre.
Que vous utilisiez une formule ou que vous remplaciez simplement un algorithme basé sur une boucle par un autre algorithme basé sur une boucle, vous êtes prêt à mesurer la différence.
Mais peut-être pouvez-vous obtenir de meilleures performances en modifiant simplement votre structure de données. Renseignez-vous sur la différence de performance entre les différentes opérations que vous devez effectuer sur la structure que vous utilisez et sur les alternatives. Peut-être qu'un hachage semble un peu plus désordonné pour fonctionner dans votre contexte, mais le temps de recherche supérieur en vaut-il la peine par rapport à un tableau ? Ce sont les types de compromis qu'il vous appartient de décider.
Vous remarquerez peut-être que cela se résume à savoir quels algorithmes sont exécutés en votre nom lorsque vous appelez une fonction de commodité. C'est donc vraiment la même chose que la réduction de la force, en fin de compte. Et savoir ce que font les bibliothèques de votre fournisseur dans les coulisses est crucial non seulement pour les performances, mais aussi pour éviter les bogues involontaires.
Micro-optimisations
OK, la fonctionnalité de votre système est terminée, mais d'un point de vue UX, les performances pourraient être affinées un peu plus. En supposant que vous ayez fait tout ce que vous pouviez plus haut, il est temps de considérer les optimisations que nous avons évitées tout le temps jusqu'à présent. Considérez, parce que ce niveau d'optimisation est toujours un compromis contre la clarté et la maintenabilité. Mais vous avez décidé qu'il était temps, alors poursuivez avec le profilage au niveau des instructions, maintenant que vous êtes dans le contexte de l'ensemble du système, là où cela compte vraiment.
Tout comme pour les bibliothèques que vous utilisez, d'innombrables heures d'ingénierie ont été consacrées à votre profit au niveau de votre compilateur ou de votre interpréteur. (Après tout, l'optimisation du compilateur et la génération de code sont des sujets énormes qui leur sont propres). C'est même vrai au niveau du processeur. Essayer d'optimiser le code sans être conscient de ce qui se passe aux niveaux les plus bas, c'est comme penser qu'avoir quatre roues motrices implique que votre véhicule peut aussi s'arrêter plus facilement.
Il est difficile de donner de bons conseils génériques au-delà de cela, car cela dépend vraiment de votre pile technologique et de ce que pointe votre profileur. Mais, parce que vous mesurez, vous êtes déjà dans une excellente position pour demander de l'aide, si des solutions ne se présentent pas de manière organique et intuitive à partir du contexte du problème. (Le sommeil et le temps passé à penser à autre chose peuvent également aider.)
À ce stade, selon le contexte et les exigences de mise à l'échelle, Jeff Atwood suggérerait probablement simplement d'ajouter du matériel, ce qui peut être moins cher que le temps de développement.
Peut-être que vous n'empruntez pas cette voie. Dans ce cas, il peut être utile d'explorer différentes catégories de techniques d'optimisation de code :
- Mise en cache
- Bit hacks et ceux spécifiques aux environnements 64 bits
- Optimisation de la boucle
- Optimisation de la hiérarchie mémoire
Plus précisement:
- Conseils d'optimisation du code en C et C++
- Conseils d'optimisation du code en Java
- Optimisation de l'utilisation du processeur dans .NET
- Mise en cache de la ferme Web ASP.NET
- Réglage de la base de données SQL ou réglage de Microsoft SQL Server en particulier
- Mise à l'échelle du jeu de Scala ! cadre
- Optimisation avancée des performances de WordPress
- Optimisation du code avec prototype JavaScript et chaînes de portée
- Optimiser les performances de React
- Efficacité des animations iOS
- Conseils sur les performances d'Android
Dans tous les cas, j'ai encore quelques choses à ne pas faire :
Ne réutilisez pas une variable à plusieurs fins distinctes. En termes de maintenabilité, c'est comme faire rouler une voiture sans huile. Ce n'est que dans les situations intégrées les plus extrêmes que cela a eu un sens, et même dans ces cas, je dirais que ce n'est plus le cas. C'est le travail du compilateur d'organiser. Faites-le vous-même, puis déplacez une ligne de code et vous avez introduit un bogue. L'illusion d'économiser de la mémoire vaut-elle cela pour vous ?
N'utilisez pas de macros et de fonctions en ligne sans savoir pourquoi. Oui, la surcharge des appels de fonction est un coût. Mais l'éviter rend souvent votre code plus difficile à déboguer, et parfois même le ralentit. Utiliser cette technique partout simplement parce que c'est une bonne idée de temps en temps est un exemple de marteau en or.
Ne déroulez pas les boucles à la main. Encore une fois, cette forme d'optimisation de boucle est presque toujours mieux optimisée par un processus automatisé comme la compilation, et non en sacrifiant la lisibilité de votre code.
L'ironie dans les deux derniers exemples d'optimisation de code est qu'ils peuvent en fait être anti-performants. Bien sûr, puisque vous faites des benchmarks, vous pouvez prouver ou réfuter cela pour votre code particulier. Mais même si vous constatez une amélioration des performances, revenez du côté de l'art et voyez si le gain vaut la perte de lisibilité et de maintenabilité.
It's Yours : Optimisation optimisée de manière optimale
Tenter d'optimiser les performances peut être bénéfique. Le plus souvent, cependant, cela se fait très prématurément, entraîne une litanie d'effets secondaires néfastes et, plus ironiquement, conduit à de moins bonnes performances. J'espère que vous êtes reparti avec une appréciation élargie de l'art et de la science de l'optimisation et, plus important encore, de son contexte.
Je suis heureux si cela nous aide à nous débarrasser de l'idée d'écrire du code parfait dès le départ et d'écrire du code correct à la place. Nous devons nous rappeler d'optimiser de haut en bas, de prouver où se situent les goulots d'étranglement et de mesurer avant et après les avoir résolus. C'est la stratégie optimale optimale pour optimiser l'optimisation. Bonne chance.

