Développeur V8 ici. Compte tenu de l'intérêt suscité par cette question et du manque d'autres réponses, je peux tenter le coup; J'ai bien peur que ce ne soit pas la réponse que vous espériez.
Existe-t-il un ensemble de directives sur la façon de programmer tout en restant dans le monde des baies SMI emballées (par exemple)?
Réponse courte: il est ici: const guidelines = ["keep your integers small enough"]
.
Réponse plus longue: il est difficile de donner un ensemble complet de directives pour diverses raisons. En général, nous pensons que les développeurs JavaScript doivent écrire du code qui a du sens pour eux et leur cas d'utilisation, et les développeurs de moteurs JavaScript doivent comprendre comment exécuter ce code rapidement sur leurs moteurs. D'un autre côté, il y a évidemment des limites à cet idéal, dans le sens où certains modèles de codage auront toujours des coûts de performance plus élevés que d'autres, quels que soient les choix d'implémentation du moteur et les efforts d'optimisation.
Lorsque nous parlons de conseils sur les performances, nous essayons de garder cela à l'esprit et d'estimer soigneusement les recommandations qui ont une forte probabilité de rester valables sur de nombreux moteurs et de nombreuses années, et sont également raisonnablement idiomatiques / non intrusives.
Revenons à l'exemple: l'utilisation de Smis en interne est censée être un détail d'implémentation que le code utilisateur n'a pas besoin de connaître. Cela rendra certains cas plus efficaces et ne devrait pas faire de mal dans d'autres cas. Tous les moteurs n'utilisent pas Smis (par exemple, AFAIK Firefox / Spidermonkey n'a pas historiquement; j'ai entendu dire que dans certains cas, ils utilisent Smis de nos jours; mais je ne connais aucun détail et je ne peux parler avec aucune autorité sur la question). Dans V8, la taille de Smis est un détail interne et a en fait changé au fil du temps et des versions. Sur les plates-formes 32 bits, qui étaient le cas d'utilisation majoritaire, les Smis ont toujours été des entiers signés 31 bits; sur les plates-formes 64 bits, il s'agissait auparavant d'entiers signés 32 bits, ce qui semblait récemment être le cas le plus courant, jusqu'à ce que dans Chrome 80, nous livrions la "compression de pointeur" pour les architectures 64 bits, qui nécessitaient de réduire la taille Smi aux 31 bits connus des plates-formes 32 bits. S'il vous arrivait d'avoir basé une implémentation sur l'hypothèse que les Smis sont généralement 32 bits, vous auriez des situations malheureuses commeça .
Heureusement, comme vous l'avez noté, les tableaux doubles sont toujours très rapides. Pour le code lourd en chiffres, il est probablement logique de supposer / cibler des tableaux doubles. Étant donné la prévalence des doubles en JavaScript, il est raisonnable de supposer que tous les moteurs ont un bon support pour les doubles et les doubles tableaux.
Est-il possible de faire de la programmation générique haute performance en Javascript sans utiliser quelque chose comme un système de macro pour intégrer des choses comme vec.add () dans les sites d'appels?
«générique» est généralement en contradiction avec «haute performance». Ceci n'est pas lié à JavaScript ou à des implémentations de moteur spécifiques.
Le code "générique" signifie que les décisions doivent être prises au moment de l'exécution. Chaque fois que vous exécutez une fonction, le code doit s'exécuter pour déterminer, par exemple, "est x
un entier? Si c'est le cas, prenez ce chemin de code. Est-ce x
une chaîne? Sautez par-dessus ici. Est-ce un objet? Est-ce qu'il a .valueOf
? Non? Alors Peut .toString()
- être ? Peut-être sur sa chaîne prototype? Appelez ça, et recommencez depuis le début avec son résultat ". Le code optimisé "hautes performances" repose essentiellement sur l'idée de supprimer tous ces contrôles dynamiques; cela n'est possible que lorsque le moteur / compilateur a un moyen d'inférer les types à l'avance: s'il peut prouver (ou supposer avec une probabilité suffisamment élevée) qu'il x
va toujours être un entier, alors il n'a qu'à générer du code pour ce cas ( gardé par une vérification de type si des hypothèses non prouvées étaient impliquées).
L'incrustation est orthogonale à tout cela. Une fonction "générique" peut toujours être intégrée. Dans certains cas, le compilateur peut être capable de propager des informations de type dans la fonction en ligne pour y réduire le polymorphisme.
(À titre de comparaison: C ++, étant un langage compilé statiquement, possède des modèles pour résoudre un problème connexe. En bref, ils permettent au programmeur de demander explicitement au compilateur de créer des copies spécialisées de fonctions (ou de classes entières), paramétrées sur des types donnés. belle solution dans certains cas, mais pas sans son propre ensemble d'inconvénients, par exemple de longs temps de compilation et de gros fichiers binaires. JavaScript, bien sûr, n'a pas de modèles. Vous pouvez utiliser eval
pour construire un système quelque peu similaire, mais ensuite vous aurait des inconvénients similaires: vous devriez faire l'équivalent du travail du compilateur C ++ lors de l'exécution, et vous devriez vous soucier de la quantité de code que vous générez.)
Comment peut-on modulariser du code haute performance en librairies à la lumière de choses comme les sites d'appel mégamorphiques et les désoptimisations? Par exemple, si j'utilise avec bonheur le package d'algèbre linéaire A à grande vitesse, puis que j'importe un package B qui dépend de A, mais B l'appelle avec d'autres types et le désoptimise, soudainement (sans que mon code change), mon code s'exécute plus lentement .
Oui, c'est un problème général avec JavaScript. La V8 avait l'habitude d'implémenter certaines fonctions internes (des choses comme Array.sort
) en JavaScript en interne, et ce problème (que nous appelons "pollution par rétroaction de type") était l'une des principales raisons pour lesquelles nous nous sommes complètement éloignés de cette technique.
Cela dit, pour le code numérique, il n'y a pas beaucoup de types (seulement Smis et doubles), et comme vous l'avez noté, ils devraient avoir des performances similaires dans la pratique, donc bien que la pollution par rétroaction de type soit en effet une préoccupation théorique, et dans certains cas, peut avoir un impact significatif, il est également assez probable que dans les scénarios d'algèbre linéaire, vous ne verrez pas de différence mesurable.
De plus, à l'intérieur du moteur, il y a beaucoup plus de situations que "un type == rapide" et "plus d'un type == lent". Si une opération donnée a vu à la fois des Smis et des doubles, c'est très bien. Le chargement d'éléments à partir de deux types de tableaux convient également. Nous utilisons le terme "mégamorphique" pour la situation où une charge a vu tellement de types différents qu'elle est abandonnée pour les suivre individuellement et utilise à la place un mécanisme plus générique qui s'adapte mieux à un grand nombre de types - une fonction contenant de telles charges peut toujours optimisé. Une "désoptimisation" est l'acte très spécifique d'avoir à jeter du code optimisé pour une fonction parce qu'un nouveau type est vu qui n'a pas été vu auparavant, et que le code optimisé n'est donc pas équipé pour gérer. Mais même ça va: il suffit de revenir au code non optimisé pour collecter plus de commentaires de type et d'optimiser à nouveau plus tard. Si cela se produit plusieurs fois, il n'y a rien à craindre; cela ne devient un problème que dans les cas pathologiquement mauvais.
Donc, le résumé de tout cela: ne vous inquiétez pas . Écrivez simplement du code raisonnable, laissez le moteur s'en occuper. Et par "raisonnable", je veux dire: ce qui a du sens pour votre cas d'utilisation, est lisible, maintenable, utilise des algorithmes efficaces, ne contient pas de bogues comme la lecture au-delà de la longueur des tableaux. Idéalement, c'est tout ce qu'il y a à faire et vous n'avez rien d'autre à faire. Si cela vous fait vous sentir mieux de faire quelque chose et / ou si vous observez réellement des problèmes de performance, je peux vous proposer deux idées:
L'utilisation de TypeScript peut vous aider. Gros avertissement: les types de TypeScript visent la productivité du développeur et non les performances d'exécution (et il s'avère que ces deux perspectives ont des exigences très différentes d'un système de type). Cela dit, il y a un certain chevauchement: par exemple, si vous annotez régulièrement des choses comme number
, le compilateur TS vous avertira si vous mettez accidentellement null
dans un tableau ou une fonction qui est censée contenir / fonctionner uniquement sur des nombres. Bien sûr, la discipline est toujours requise: une seule number_func(random_object as number)
trappe d'échappement peut saper tout silencieusement, car l'exactitude des annotations de type n'est appliquée nulle part.
L'utilisation de TypedArrays peut également vous aider. Ils ont un peu plus de surcharge (consommation de mémoire et vitesse d'allocation) par baie par rapport aux baies JavaScript standard (donc si vous avez besoin de plusieurs petites baies, alors les baies régulières sont probablement plus efficaces), et elles sont moins flexibles car elles ne peuvent pas grandir ou rétrécir après allocation, mais ils garantissent que tous les éléments ont exactement un type.
Existe-t-il de bons outils de mesure faciles à utiliser pour vérifier ce que le moteur Javascript fait en interne avec les types?
Non, et c'est intentionnel. Comme expliqué ci-dessus, nous ne voulons pas que vous adaptiez spécifiquement votre code aux modèles que V8 peut optimiser particulièrement bien aujourd'hui, et nous ne pensons pas que vous souhaitiez vraiment le faire non plus. Cet ensemble de choses peut changer dans les deux sens: s'il y a un modèle que vous aimeriez utiliser, nous pourrions l'optimiser pour cela dans une future version (nous avons déjà joué avec l'idée de stocker des entiers 32 bits non mis en boîte comme éléments de tableau .. . mais le travail sur ce point n'a pas encore commencé, donc aucune promesse); et parfois, s'il y a un modèle que nous avons utilisé pour l'optimisation dans le passé, nous pourrions décider de l'abandonner s'il gêne d'autres optimisations plus importantes / ayant un impact. En outre, des choses comme l'heuristique en ligne sont notoirement difficiles à obtenir correctement, ainsi, prendre la bonne décision en ligne au bon moment est un domaine de recherche continue et des changements correspondants dans le comportement du moteur / compilateur; ce qui en fait un autre cas où il serait regrettable pour tout le monde (vouset nous) si vous avez passé beaucoup de temps à peaufiner votre code jusqu'à ce qu'un certain ensemble de versions de navigateur actuelles fasse à peu près les décisions en ligne que vous pensez (ou savez?) sont les meilleures, pour revenir une demi-année plus tard pour réaliser que les navigateurs actuels ont changé leur heuristique.
Bien sûr, vous pouvez toujours mesurer les performances de votre application dans son ensemble - c'est ce qui compte en fin de compte, et non les choix spécifiques du moteur effectués en interne. Méfiez-vous des microbenchmarks, car ils sont trompeurs: si vous extrayez seulement deux lignes de code et que vous les comparez, il y a de fortes chances que le scénario soit suffisamment différent (par exemple, un type de rétroaction différent) pour que le moteur prenne des décisions très différentes.