JavaScript peut sembler très éloigné du matériel sur lequel il s'exécute, mais penser au bas niveau peut toujours être utile dans des cas limités.
Un article récent de Kafeel Ahmad sur l'optimisation des boucles a détaillé un certain nombre de techniques d'amélioration des performances des boucles. Cet article m'a fait réfléchir sur le sujet.
Juste pour mettre cela de côté, c'est une technique que très peu de personnes auront besoin d'envisager dans le développement Web. De plus, se concentrer trop tôt sur l’optimisation peut rendre le code plus difficile à écrire et beaucoup plus difficile à maintenir. Jeter un coup d'œil aux techniques de bas niveau peut nous donner un aperçu de nos outils et du travail en général, même si nous ne pouvons pas appliquer ces connaissances directement.
Le déroulement d'une boucle duplique essentiellement la logique à l'intérieur d'une boucle, vous effectuez donc plusieurs opérations au cours de chaque, eh bien, boucle. Dans des cas spécifiques, rendre le code dans la boucle plus long peut le rendre plus rapide.
En effectuant intentionnellement certaines opérations en groupes plutôt qu'une par une, l'ordinateur peut être en mesure de fonctionner plus efficacement.
Prenons un exemple très simple : additionner des valeurs dans un tableau.
// 1-to-1 looping const simpleSum = (data) => { let sum = 0; for(let i=0; i < data.length; i += 1) { sum += data[i]; } return sum; }; const parallelSum = (data) => { let sum1 = 0; let sum2 = 0; for(let i=0; i < data.length; i += 2) { sum1 += data[i]; sum2 += data[i + 1]; } return sum1 + sum2; };
Cela peut paraître très étrange au début. Nous gérons davantage de variables et effectuons des opérations supplémentaires qui ne se produisent pas dans l'exemple simple. Comment cela peut-il être plus rapide ?!
J'ai effectué des comparaisons sur une variété de tailles de données et plusieurs exécutions, ainsi que des tests séquentiels ou entrelacés. Les performances de parallelSum variaient, mais étaient presque toujours meilleures, à l'exception de quelques résultats étranges pour de très petites tailles de données. J'ai testé cela en utilisant RunJS, qui est construit sur le moteur V8 de Chrome.
Différentes tailles de données ont donné très grossièrement ces résultats :
Ensuite, j'ai créé un JSPerf avec 1 million d'enregistrements à essayer sur différents navigateurs. Essayez-le vous-même !
Chrome a exécuté parallelSum deux fois plus vite que simpleSum, comme prévu lors des tests RunJS.
Safari était presque identique à Chrome, à la fois en pourcentage et en opérations par seconde.
Firefox sur le même système fonctionnait presque de la même manière pour simpleSum, mais parallelSum n'était qu'environ 15 % plus rapide, pas deux fois plus rapide.
Cette variante m'a poussé à chercher plus d'informations. Bien que cela n'ait rien de définitif, j'ai trouvé un commentaire StackOverflow de 2016 discutant de certains problèmes du moteur JS avec le déroulement des boucles. C'est un aperçu intéressant de la façon dont les moteurs et les optimisations peuvent affecter le code d'une manière inattendue.
J'ai également essayé une troisième version, qui ajoutait deux valeurs en une seule opération pour voir s'il y avait une différence notable entre une variable et deux.
const parallelSum = (data) => { let sum = 0 for(let i=0; i < data.length; i += 2) { sum += data[i] + data[i + 1]; } return sum; };
Réponse courte : Non. Les deux versions "parallèles" se trouvaient dans la marge d'erreur indiquée l'une de l'autre.
Bien que JavaScript soit monothread, les interpréteurs, les compilateurs et le matériel en dessous peuvent effectuer des optimisations pour nous lorsque certaines conditions sont remplies.
Dans l'exemple simple, l'opération a besoin de la valeur i pour savoir quelles données récupérer, et elle a besoin de la dernière valeur de sum pour être mise à jour. Étant donné que ces deux éléments changent à chaque boucle, l’ordinateur doit attendre que la boucle soit terminée pour obtenir plus de données. Bien que cela puisse nous sembler évident ce que i += 1 fera, l'ordinateur comprend généralement "la valeur va changer, revenez plus tard", il a donc du mal à optimiser.
Nos versions parallèles chargent plusieurs entrées de données pour chaque valeur de i. Nous dépendons toujours de la somme pour chaque boucle, mais nous pouvons charger et traiter deux fois plus de données par cycle. Mais cela ne veut pas dire qu'il fonctionne deux fois plus vite.
Pour comprendre pourquoi le déroulement de boucle fonctionne, examinons le fonctionnement de bas niveau d'un ordinateur. Les processeurs dotés d'architectures super-scalaires peuvent disposer de plusieurs pipelines pour effectuer des opérations simultanées. Ils peuvent prendre en charge une exécution dans le désordre afin que les opérations qui ne dépendent pas les unes des autres puissent avoir lieu le plus rapidement possible. Pour certaines opérations, SIMD peut effectuer une action sur plusieurs éléments de données à la fois. Au-delà de cela, nous commençons à nous lancer dans la mise en cache, la récupération de données et la prédiction de branchement...
Mais ceci est un article JavaScript ! Nous n'allons pas si loin. Si vous souhaitez en savoir plus sur les architectures de processeurs, Anandtech propose d'excellentes plongées approfondies.
Le déroulement d’une boucle n’est pas magique. Il existe des limites et des rendements décroissants qui apparaissent en raison de la taille du programme ou des données, de la complexité des opérations, de l'architecture informatique, etc. Mais nous n'avons testé qu'une ou deux opérations, et les ordinateurs modernes prennent souvent en charge quatre threads ou plus.
Pour essayer des incréments plus importants, j'ai créé un autre JSPerf avec 1, 2, 4 et 10 enregistrements et je l'ai exécuté sur un MacBook Pro Apple M1 Max exécutant macOS 14.5 Sonoma et un PC AMD Ryzen 9 3950X exécutant Windows 11.
Dix enregistrements à la fois étaient 2,5 à 3,5 fois plus rapides que la boucle de base, mais seulement 12 à 15 % plus rapides que le traitement de quatre enregistrements sur Mac. Sur PC, nous avons quand même constaté une amélioration 2x entre un et deux enregistrements, mais dix enregistrements n'étaient que 2 % plus rapides que quatre enregistrements, ce que je n'aurais pas prédit pour un processeur à 16 cœurs.
Ces différents résultats nous rappellent de faire attention à l'optimisation. L'optimisation pour votre ordinateur pourrait créer une pire expérience sur du matériel moins performant ou simplement différent. Les problèmes de performances ou de fonctionnalités pour le matériel plus ancien ou d'entrée de gamme sont un problème courant lorsque les développeurs travaillent sur des machines rapides et puissantes, et c'est quelque chose qui m'a été confié à plusieurs reprises au cours de ma carrière.
Pour une certaine échelle de performances, un Chromebook d'entrée de gamme actuellement disponible de HP est équipé d'un processeur Intel Celeron N4120. C'est à peu près équivalent à mon MacBook Air Core i5-4250U 2013. Il n'a que un neuvième les performances du M1 Max dans un benchmark synthétique. Sur ce MacBook Air 2013, exécutant la dernière version de Chrome, la fonction 4 enregistrements était plus rapide que la fonction 10 enregistrements, mais toujours seulement 60 % plus rapide que la fonction enregistrement unique !
Les navigateurs et les normes évoluent également constamment. Une mise à jour de routine du navigateur ou une architecture de processeur différente pourrait rendre le code optimisé plus lent qu'une boucle normale. Lorsque vous vous retrouvez à optimiser profondément, vous devrez peut-être vous assurer que votre optimisation est pertinente pour vos consommateurs et qu'elle reste pertinente.
Cela me rappelle le livre High Performance JavaScript de Nicholas Zakas, que j'ai lu en 2012. C'était un excellent livre et contenait beaucoup d'informations. Cependant, en 2014, un certain nombre de problèmes de performances importants identifiés dans le livre avaient été résolus ou considérablement réduits par les mises à jour du moteur de navigateur, et nous avons pu concentrer davantage d'efforts sur l'écriture de code maintenable.
Si vous essayez de rester à la pointe de l'optimisation des performances, préparez-vous au changement et à une validation régulière.
En recherchant ce sujet, je suis tombé sur un fil de discussion sur la liste de diffusion du noyau Linux datant de l'année 2000 sur la suppression de certaines optimisations de déroulement de boucle, ce qui a finalement amélioré les performances de l'application. Il incluait ce point toujours d'actualité (c'est moi qui souligne) :
En fin de compte, nos hypothèses intuitives sur ce qui est rapide et ce qui ne l'est pas peuvent souvent être fausses, surtout compte tenu de l'évolution des processeurs au cours des deux dernières années.
– Théodore Ts'o
Il peut arriver que vous ayez besoin de réduire les performances d'une boucle, et si vous traitez suffisamment d'éléments, cela pourrait être l'une des façons de procéder. Il est bon de connaître ce type d'optimisations, mais pour la plupart des travaux, vous n'en aurez pas besoin™.
J'espère néanmoins que vous avez apprécié mes divagations et que peut-être qu'à l'avenir votre mémoire sera rafraîchie sur les considérations d'optimisation des performances.
Merci d'avoir lu !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!