Ne tombez pas dans le piège de l'optimisation prématurée

Donald Knuth a dit un jour les mots qui sont devenus célèbres par la suite: «Le vrai problème est que les programmeurs, pas là où ils doivent et pas quand ils en ont besoin, passent trop de temps à se soucier de l'efficacité. L'optimisation prématurée est la racine de tous les maux (ou du moins la plupart d'entre eux) dans la programmation. »



L'auteur du document, dont nous publions aujourd'hui la traduction, souhaite expliquer comment il est tombé une fois dans le piège de l'optimisation prématurée et comment il a compris, par sa propre expérience amère, que l'optimisation prématurée est à l'origine de tous les maux.

Jeu GeoArena en ligne


Il y a quelques années, j'ai travaillé sur le jeu Web GeoArena Online (puis je l'ai vendu , les nouveaux propriétaires l'ont posté sur geoarena.io ). C'était un jeu multijoueur dans le style du «dernier survivant». Là, le joueur contrôlait le navire, se battant face à face avec un autre joueur.


Jeu GeoArena en ligne


Jeu GeoArena en ligne

Un jeu dynamique, dont le monde est plein de particules et d'effets, nécessite de sérieuses ressources informatiques. En conséquence, le jeu sur certains vieux ordinateurs a "ralenti" dans des moments particulièrement tendus. Moi, un homme qui n'est pas indifférent aux problèmes de productivité, j'ai pris la solution à ce problème avec intérêt. «Comment accélérer la partie JavaScript côté client GeoArena», me suis-je demandé.

Bibliothèque Fast.js


Après avoir cherché un peu sur Internet, j'ai découvert la bibliothèque fast.js. Il s'agissait d'une "collection de micro-optimisations visant à simplifier le développement de programmes JavaScript très rapides". Cette bibliothèque a été accélérée par la disponibilité d'implémentations plus rapides des méthodes standard intégrées comme Array.prototype.forEach () .

J'ai trouvé cela extrêmement intéressant. GeoArena a utilisé de nombreux tableaux, effectué de nombreuses opérations avec des tableaux, donc l'utilisation de fast.js pourrait très bien m'aider à accélérer le jeu. Les résultats suivants de l'étude de performance forEach() ont été inclus dans le fichier README de fast.js.

 Native .forEach() vs fast.forEach() (10 items)  ✓ Array::forEach() x 8,557,082 ops/sec ±0.37% (97 runs sampled)  ✓ fast.forEach() x 8,799,272 ops/sec ±0.41% (97 runs sampled)  Result: fast.js is 2.83% faster than Array::forEach(). 

Comment une méthode implémentée dans une bibliothèque externe peut-elle être plus rapide que sa version standard? Le truc, c'est qu'il y avait une astuce (elles, ces astuces, se trouvent partout où vous regardez). La bibliothèque ne convenait que pour travailler avec des tableaux qui n'étaient pas rares.

Voici quelques exemples simples de tels tableaux:

 //  -  :   1  . const sparse1 = [0, , 1]; console.log(sparse1.length); // 3 //  -   const sparse2 = []; // ...   - .   0 - 4    . sparse2[5] = 0; console.log(sparse2.length); // 6 

Afin de comprendre pourquoi la bibliothèque ne peut pas fonctionner normalement avec des tableaux clairsemés, j'ai examiné son code source. Il s'est avéré que l' forEach() dans fast.js est basée sur des boucles for. Une implémentation rapide de la méthode forEach() ressemblerait à ceci:

 //     . function fastForEach(array, f) {  for (let i = 0; i < array.length; i++) {    f(array[i], i, array);  } } const sparseArray = [1, , 2]; const print = x => console.log(x); fastForEach(sparseArray, print); //  print() 3 . sparseArray.forEach(print); //  print()  2 . 

Un appel à la méthode fastForEach() trois valeurs:

 1 undefined 2 

L'appel de sparseArray.forEach() ne mène qu'à la conclusion de deux valeurs:

 1 2 

Cette différence est due au fait que les spécifications JS concernant l'utilisation des fonctions de rappel indiquent que ces fonctions ne doivent pas être appelées sur des index de tableau distants ou non initialisés (également appelés «trous»). L' fastForEach() n'a pas vérifié la présence de trous dans le tableau. Cela a conduit à une augmentation de la vitesse au détriment d'un travail correct avec des tableaux clairsemés. C'était parfait pour moi, car les tableaux clairsemés n'étaient pas utilisés dans GeoArena.

À ce stade, je devrais juste avoir un test rapide sur fast.js. Je devrais installer la bibliothèque, changer les méthodes standard de l'objet Array en méthodes de fast.js et tester les performances du jeu. Mais à la place, j'ai évolué dans une direction complètement différente.

Mon développement s'appelle plus rapide.js


Le perfectionniste maniaque vivant en moi voulait absolument tout presser pour optimiser les performances du jeu. La bibliothèque fast.js ne m'a tout simplement pas semblé être une assez bonne solution, car son utilisation impliquait d'appeler ses méthodes. Puis j'ai pensé: «Et si je remplaçais les méthodes standard des tableaux en incorporant simplement de nouvelles implémentations plus rapides de ces méthodes dans le code? Cela me sauverait du besoin d'appels de méthode de bibliothèque. »

C'est cette idée qui m'a conduit à l'idée ingénieuse, qui était de créer un compilateur, que j'ai appelé effrontément plus rapidement.js . J'avais prévu de l'utiliser au lieu de fast.js. Par exemple, voici l'extrait de code source:

 //   const arr = [1, 2, 3]; const results = arr.map(e => 2 * e); 

Le compilateur fast.js convertirait ce code en ce qui suit - plus rapide, mais en pire:

 //      faster.js const arr = [1, 2, 3]; const results = new Array(arr.length); const _f = (e => 2 * e); for (let _i = 0; _i < arr.length; _i++) {  results[_i] = _f(arr[_i], _i, arr); } 

La création de fast.js a été provoquée par la même idée qui sous-tendait fast.js. À savoir, nous parlons de micro-optimisations des performances en raison du rejet de la prise en charge des tableaux clairsemés.

À première vue, plus rapide.js m'a semblé un développement extrêmement réussi. Voici quelques résultats d'une étude de performance plus rapide.js:

   array-filter large    ✓ native x 232,063 ops/sec ±0.36% (58 runs sampled)    ✓ faster.js x 1,083,695 ops/sec ±0.58% (57 runs sampled) faster.js is 367.0% faster (3.386μs) than native  array-map large    ✓ native x 223,896 ops/sec ±1.10% (58 runs sampled)    ✓ faster.js x 1,726,376 ops/sec ±1.13% (60 runs sampled) faster.js is 671.1% faster (3.887μs) than native  array-reduce large    ✓ native x 268,919 ops/sec ±0.41% (57 runs sampled)    ✓ faster.js x 1,621,540 ops/sec ±0.80% (57 runs sampled) faster.js is 503.0% faster (3.102μs) than native  array-reduceRight large    ✓ native x 68,671 ops/sec ±0.92% (53 runs sampled)    ✓ faster.js x 1,571,918 ops/sec ±1.16% (57 runs sampled) faster.js is 2189.1% faster (13.926μs) than native 

Les résultats complets des tests peuvent être trouvés ici . Ils ont eu lieu dans Node v8.16.1, sur le MacBook Pro 15 pouces 2018.

Mon développement est-il 2000% plus rapide que l'implémentation standard? Une augmentation si sérieuse de la productivité est, sans aucun doute, quelque chose qui peut avoir l'impact positif le plus fort sur n'importe quel programme. Non?
Non, pas vrai.

Prenons un exemple simple.

  • Imaginez que le jeu GeoArena moyen nécessite 5 000 millisecondes (ms) de calcul.
  • Le compilateur plus rapide.js accélère l'exécution des méthodes de tableau par une moyenne de 10 fois (c'est une estimation approximative, de plus, elle est surestimée; dans la plupart des applications réelles, il n'y a même pas de double accélération).

Et voici la question qui nous intéresse vraiment: "Quelle partie de ces 5000 ms est consacrée à l'implémentation des méthodes de tableau?".

Supposons que la moitié. Autrement dit, 2500 ms sont dépensés pour les méthodes de tableau, les 2500 ms restants pour tout le reste. Si c'est le cas, l'utilisation de fast.js augmentera considérablement les performances.


Exemple conditionnel: le temps d'exécution du programme est très réduit

En conséquence, il s'avère que le temps de calcul total a été réduit de 45%.

Malheureusement, tous ces arguments sont très, très loin de la réalité. GeoArena, bien sûr, utilise de nombreuses méthodes de tableau. Mais la distribution réelle du temps d'exécution du code pour différentes tâches ressemble à ceci.


Une dure réalité

Malheureusement, que puis-je dire.

C'est exactement l'erreur dont Donald Knuth a mis en garde. Je n'ai pas mis mes efforts sur ce à quoi ils devaient s'appliquer, et je ne l'ai pas fait quand cela en valait la peine.

Ici, les mathématiques simples entrent en jeu. Si quelque chose ne prend que 1% du temps d'exécution du programme, son optimisation ne donnera, dans le meilleur des cas, qu'une augmentation de 1% de la productivité.

C'est exactement ce que Donald Knuth avait en tête quand il a dit "pas là où c'est nécessaire". Et si vous pensez à «où vous en avez besoin», il s'avère que ce sont les parties des programmes qui représentent les goulots d'étranglement des performances. Ce sont les morceaux de code qui contribuent de manière significative à la performance globale du programme. Ici, le concept de «productivité» est utilisé dans un sens très large. Il peut inclure le temps d'exécution du programme, la taille de son code compilé et autre chose. Une amélioration de 10% dans cette partie du programme qui affecte grandement les performances est meilleure qu'une amélioration de 100% dans une petite chose.

Knut a également parlé de l'application des efforts "pas lorsque cela était nécessaire". Le fait est que vous devez optimiser quelque chose uniquement lorsque cela est nécessaire. Bien sûr, j'avais une bonne raison de penser à l'optimisation. Mais rappelez-vous que j'ai commencé à développer fast.js et avant cela, je n'ai même pas essayé de tester la bibliothèque fast.js dans GeoArena? Les minutes passées à tester fast.js dans mon jeu me feraient gagner des semaines de travail. J'espère que vous ne tomberez pas dans le même piège dans lequel je suis tombé.

Résumé


Si vous êtes intéressé à expérimenter avec fast.js, vous pouvez jeter un œil à cette démo. Les résultats que vous obtenez dépendent de votre appareil et de votre navigateur. Voici, par exemple, ce qui s'est passé dans Chrome 76 sur le MacBook Pro 15 pouces 2018.


Résultats des tests Faster.js

Vous pourriez être intéressé à en savoir plus sur les résultats réels de l'utilisation de faster.js dans GeoArena. Moi, quand le jeu était encore à moi (comme je l'ai dit, je l'ai vendu), j'ai effectué quelques recherches de base. En conséquence, il s'est avéré ce qui suit:

  • L'utilisation de faster.js accélère l'exécution du cycle de jeu principal dans un jeu typique d'environ 1%.
  • En raison de l'utilisation de fast.js, la taille de l'ensemble du jeu a augmenté de 0,3%. Cela ralentit un peu le chargement de la page de jeu. La taille du bundle a augmenté du fait que fast.js convertit le code court standard en code plus rapide, mais aussi plus long.

En général, fast.js a ses avantages et ses inconvénients, mais ce développement du mien n'a pas eu beaucoup d'impact sur les performances de GeoArena. J'aurais compris cela bien plus tôt si j'avais pris la peine de tester d'abord le jeu en utilisant fast.js.

Que mon histoire vous serve d'avertissement.

Chers lecteurs! Êtes-vous tombé dans le piège de l'optimisation prématurée?

Source: https://habr.com/ru/post/fr465809/


All Articles