Il ya quelques semaines, nous avons commencé une série visant à creuser plus profondément dans le JavaScript et comment elle fonctionne: nous avons pensé qu’en connaissant les blocs de construction de JavaScript et comment ils arrivent à jouer ensemble, vous serez en mesure de mieux écrire du code et des applications.,
le premier post de la série s’est concentré sur la présentation du moteur, du runtime et de la pile d’appels. Ce deuxième article plongera dans les parties internes du moteur JavaScript V8 de Google. Nous fournirons également quelques conseils rapides sur la façon d’écrire un meilleur code JavaScript —les meilleures pratiques que notre équipe de développement de SessionStack suit lors de la construction du produit.
aperçu
Un moteur JavaScript est un programme ou un interpréteur qui exécute du code JavaScript., Un moteur JavaScript peut être implémenté en tant qu’interpréteur standard ou compilateur juste à temps qui compile JavaScript en bytecode sous une forme ou une autre.,/li>
pourquoi le moteur V8 a-t-il été créé?
le moteur V8 construit par Google est open source et écrit en C++. Ce moteur est utilisé dans Google Chrome. Contrairement au reste des moteurs, cependant, V8 est également utilisé pour le populaire Nœud.js de l’exécution.
V8 a d’abord été conçu pour augmenter la performance de l’exécution de JavaScript à l’intérieur des navigateurs web., Afin d’obtenir de la vitesse, V8 traduit le code JavaScript en code machine plus efficace au lieu d’utiliser un interpréteur. Il compile le code JavaScript en code machine à l’exécution en implémentant un compilateur JIT (Just-In-Time) comme le font beaucoup de moteurs JavaScript modernes tels que SpiderMonkey ou Rhino (Mozilla). La principale différence ici est que V8 ne produit pas de bytecode ou de code intermédiaire.
V8 a deux compilateurs
Avant la version 5.,9 de V8 est sorti (sorti plus tôt cette année), le moteur utilisait deux compilateurs:
- full-codegen — un compilateur simple et très rapide qui produisait du code machine simple et relativement lent.
- Vilebrequin — un compilateur d’optimisation plus complexe (juste-à-temps) qui a produit du code hautement optimisé.,l>
- le thread principal fait ce que vous attendez: récupérer votre code, le compiler puis l’exécuter
- Il existe également un thread séparé pour la compilation, de sorte que le thread principal puisse continuer à s’exécuter pendant que le premier optimise le code
- un thread profileur qui indiquera au runtime sur quelles méthodes nous passons beaucoup de temps afin que Vilebrequin puisse les optimiser
- quelques threads pour gérer les balayages de Garbage Collector
lors de la première exécution du code JavaScript, V8 exploite full-codegen qui traduit directement le JavaScript analysé en code machine sans aucune transformation., Cela lui permet de commencer à exécuter du code machine très rapidement. Notez que la V8 n’utilise pas de représentation de bytecode intermédiaire de cette façon, ce qui supprime le besoin d’un interpréteur.
lorsque votre code est exécuté depuis un certain temps, le thread du profileur a rassemblé suffisamment de données pour indiquer quelle méthode doit être optimisée.
ensuite, les optimisations de vilebrequin commencent dans un autre filetage. Il traduit L’arbre de syntaxe abstraite JavaScript en une représentation statique à affectation unique (SSA) de haut niveau appelée hydrogène et tente d’optimiser ce graphique hydrogène. La plupart des optimisations se font à ce niveau.,
Inline
La première optimisation est inline autant de code que possible à l’avance. Inline est le processus de remplacement d’un site d’appel (la ligne de code où la fonction est appelée) avec le corps de la fonction appelée. Cette étape simple permet aux optimisations suivantes d’être plus significatives.
classe Cachée
JavaScript est un langage basé sur des prototypes: il n’y a pas de classes et d’objets sont créés à l’aide d’un processus de clonage., JavaScript est également un langage de programmation dynamique, ce qui signifie que les propriétés peuvent être facilement ajoutées ou supprimées d’un objet après son instanciation.
La Plupart Des interprètes JavaScript utilisent des structures de type dictionnaire (fonction de hachage) pour stocker l’emplacement des valeurs de propriété d’objet dans la mémoire. Cette structure rend la récupération de la valeur d’une propriété en JavaScript plus coûteuse en calcul qu’elle ne le serait dans un langage de programmation non dynamique comme Java ou C#., En Java, toutes les propriétés de l’objet sont déterminées par une disposition d’objet fixe avant la compilation et ne peuvent pas être ajoutées ou supprimées dynamiquement au moment de l’exécution (enfin, C# a le type dynamique qui est un autre sujet). Par conséquent, les valeurs des propriétés (ou des pointeurs vers ces propriétés) peuvent être stockées en tant que tampon continu dans la mémoire avec un décalage fixe entre chacune. La longueur d’un décalage peut facilement être déterminée en fonction du type de propriété, alors que ce n’est pas possible en JavaScript où un type de propriété peut changer pendant l’exécution.,
comme l’utilisation de dictionnaires pour trouver l’emplacement des propriétés de l’objet dans la mémoire est très inefficace, la V8 utilise une méthode différente à la place: les classes cachées. Les classes cachées fonctionnent de manière similaire aux mises en page d’objets fixes (classes) utilisées dans des langages comme Java, sauf qu’elles sont créées lors de l’exécution. Maintenant, voyons à quoi ils ressemblent réellement:
function Point(x, y) {
this.x = x;
this.y = y;
}var p1 = new Point(1, 2);
Une fois que l’invocation « new Point(1, 2)” se produira, V8 créera une classe cachée appelée « C0”.,
Pas de propriétés ont été définies pour Point encore, donc « C0” est vide.
Une fois la première déclaration « ceci.x = x » est exécuté (à l’intérieur de la fonction « Point”), V8 créera une deuxième classe cachée appelée « C1” basée sur « C0”. « C1 » décrit l’emplacement dans la mémoire (par rapport au pointeur d’objet) où la propriété x peut être trouvée., Dans ce cas, « x” est stocké à l’offset 0, ce qui signifie que lors de l’affichage d’un objet point dans la mémoire en tant que tampon continu, le premier offset correspondra à la propriété « x”. V8 mettra également à jour” C0 « avec une” transition de classe « qui indique que si une propriété” x « est ajoutée à un objet point, la classe cachée doit passer de” C0 « à”C1 ». La classe cachée pour l’objet point ci-dessous est maintenant « C1”.,
Ce processus est répété lors de la déclaration « ce.y = y » est exécuté (encore une fois, à l’intérieur de la fonction Point, après le « this.x = x » Instruction).,
Une nouvelle classe cachée appelée « C2” est créée, une transition de classe est ajoutée à « C1” indiquant que si une propriété « y” est ajoutée à un objet Point (qui contient déjà la propriété « x”), alors la classe cachée devrait changer en « C2”, et la classe cachée de l’objet point est mise à jour en « C2”.
Classe caché transitions dépendent de l’ordre dans lequel les propriétés sont ajoutées à un objet., Jetez un oeil à l’extrait de code ci-dessous:
function Point(x, y) {
this.x = x;
this.y = y;
}var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;
Maintenant, vous supposeriez que pour p1 et p2 les mêmes classes et transitions cachées seraient utilisées. Eh bien, pas vraiment. Pour « p1 », d’abord la propriété” a « sera ajoutée, puis la propriété”b ». Pour « p2 », cependant, le premier” b « est attribué, suivi de”a ». Ainsi, « p1” et » p2 » se retrouvent avec différentes classes cachées à la suite des différents chemins de transition. Dans de tels cas, il est préférable d’initialiser les propriétés dynamiques dans le même ordre afin que les classes cachées puissent être réutilisées.,
mise en cache en ligne
V8 tire parti d’une autre technique d’optimisation des langages typés dynamiquement appelée mise en cache en ligne. La mise en cache en ligne repose sur l’observation que les appels répétés à la même méthode ont tendance à se produire sur le même type d’objet. Une explication approfondie de la mise en cache en ligne peut être trouvée ici.
Nous allons aborder le concept général de mise en cache en ligne (au cas où vous n’auriez pas le temps de passer en revue l’explication approfondie ci-dessus).
Alors, comment ça fonctionne?, V8 maintient un cache du type d’objets qui ont été passés en tant que paramètre dans les appels de méthode récents et utilise ces informations pour faire une hypothèse sur le type d’objet qui sera passé en tant que paramètre à l’avenir. Si V8 est capable de faire une bonne hypothèse sur le type d’objet qui sera transmis à une méthode, il peut contourner le processus de déterminer comment accéder aux propriétés de l’objet et, à la place, utiliser les informations stockées des recherches précédentes à la classe cachée de l’objet.
alors, comment les concepts de classes cachées et de mise en cache en ligne sont-ils liés?, Chaque fois qu’une méthode est appelée sur un objet spécifique, le moteur V8 doit effectuer une recherche sur la classe cachée de cet objet afin de déterminer le décalage pour accéder à une propriété spécifique. Après deux appels réussis de la même méthode à la même classe cachée, V8 omet la recherche de classe cachée et ajoute simplement le décalage de la propriété au pointeur d’objet lui-même. Pour tous les appels futurs de cette méthode, le moteur V8 suppose que la classe cachée n’a pas changé et saute directement dans l’adresse mémoire d’une propriété spécifique en utilisant les décalages stockés à partir des recherches précédentes., Cela augmente considérablement la vitesse d’exécution.
la mise en cache en ligne est également la raison pour laquelle il est si important que les objets du même type partagent des classes cachées. Si vous créez deux objets du même type et avec des classes cachées différentes (comme nous l’avons fait dans l’exemple précédent), V8 ne pourra pas utiliser la mise en cache en ligne car même si les deux objets sont du même type, leurs classes cachées correspondantes attribuent différents décalages à leurs propriétés.,
Compilation en code machine
Une fois le graphique hydrogène optimisé, le vilebrequin l’abaisse à une représentation de niveau inférieur appelée Lithium. La plupart de L’implémentation Lithium est spécifique à l’architecture. L’allocation de Registre se produit à ce niveau.,
à la fin, le Lithium est compilé en code machine. Ensuite, quelque chose d’autre se produit appelé OSR: remplacement sur pile. Avant de commencer à compiler et à optimiser une méthode évidemment longue, nous l’exécutions probablement. V8 ne va pas oublier ce qu’il vient d’exécuter lentement pour recommencer avec la version optimisée. Au lieu de cela, il transformera tout le contexte que nous avons (pile, registres) afin que nous puissions passer à la version optimisée au milieu de l’exécution. C’est une tâche très complexe, ayant à l’esprit que, parmi d’autres optimisations, V8 a incorporé le code initialement., V8 n’est pas le seul moteur capable de le faire.
Il existe des garanties appelées désoptimisation pour effectuer la transformation inverse et revenir au code non optimisé au cas où une hypothèse faite par le moteur ne tiendrait plus vrai.
Garbage collection
Pour garbage collection, V8 utilise une approche générationnelle traditionnelle de mark-and-sweep pour nettoyer l’ancienne génération. La phase de marquage est censée arrêter L’exécution JavaScript., Afin de contrôler les coûts du GC et de rendre l’exécution plus stable, la V8 utilise un marquage incrémental: au lieu de parcourir tout le tas, en essayant de marquer tous les objets possibles, elle ne parcourt qu’une partie du tas, puis reprend l’exécution normale. Le prochain arrêt du GC continuera à partir de l’endroit où la marche de tas précédente s’est arrêtée. Cela permet des pauses très courtes pendant l’exécution normale. Comme mentionné précédemment, la phase de balayage est gérée par des threads séparés.
allumage et TurboFan
avec la sortie de la V8 5.9 plus tôt en 2017, un nouveau pipeline d’exécution a été introduit., Ce nouveau pipeline permet d’améliorer encore plus les performances et d’économiser de la mémoire dans les applications JavaScript réelles.
le nouveau pipeline d’exécution est construit sur Ignition, l’interpréteur de V8, et TurboFan, le plus récent compilateur d’optimisation de V8.
Vous pouvez consulter l’article de blog de L’équipe V8 sur le sujet ici.
Depuis la version 5.,9 de V8 est sorti, full-codegen et Vilebrequin (les technologies qui ont servi V8 depuis 2010) n’ont plus été utilisées par V8 pour L’exécution JavaScript car L’équipe V8 a eu du mal à suivre le rythme avec les nouvelles fonctionnalités du langage JavaScript et les optimisations nécessaires pour ces fonctionnalités.
cela signifie que dans l’ensemble, la V8 aura une architecture beaucoup plus simple et plus maintenable à l’avenir.
Ces améliorations ne sont que le début. Le nouveau pipeline D’allumage et de TurboFan ouvre la voie à d’autres optimisations qui augmenteront les performances JavaScript et réduiront L’empreinte de V8 dans Chrome et Node.js dans les années à venir.
enfin, voici quelques trucs et astuces pour écrire un JavaScript bien optimisé et meilleur., Vous pouvez facilement les dériver du contenu ci-dessus, cependant, voici un résumé pour votre commodité:
comment écrire du JavaScript optimisé
- Ordre des propriétés d’objet: instanciez toujours vos propriétés d’objet dans le même ordre afin que les classes cachées, puis le code optimisé, puissent être partagés.
- propriétés dynamiques: l’ajout de propriétés à un objet après l’instanciation forcera un changement de classe cachée et ralentira toutes les méthodes optimisées pour la classe cachée précédente. Au lieu de cela, affectez toutes les propriétés d’un objet dans son constructeur.,
- Méthodes: le code qui exécute la même méthode à plusieurs reprises s’exécutera plus rapidement que le code qui n’exécute de nombreuses méthodes différentes qu’une seule fois (en raison de la mise en cache en ligne).
- tableaux: évitez les tableaux clairsemés où les clés ne sont pas des nombres incrémentaux. Les tableaux clairsemés qui n’ont pas tous les éléments à l’intérieur sont une table de hachage. Les éléments de ces tableaux sont plus coûteux à accéder. Essayez également d’éviter de pré-allouer de grands tableaux. Il vaut mieux grandir au fur et à mesure. Enfin, ne supprimez pas d’éléments dans les tableaux. Cela rend les clés clairsemées.
- valeurs marquées: V8 représente des objets et des nombres avec 32 bits., Il utilise un bit pour savoir s’il s’agit d’un objet (flag = 1) ou d’un entier (flag = 0) appelé SMI (SMall Integer) à cause de ses 31 bits. Ensuite, si une valeur numérique est supérieure à 31 bits, V8 encadrera le nombre, le transformera en un double et créera un nouvel objet pour mettre le nombre à l’intérieur. Essayez d’utiliser des nombres signés 31 bits autant que possible pour éviter l’opération de boxe coûteuse dans un objet JS.
chez SessionStack, nous essayons de suivre ces meilleures pratiques en écrivant du code JavaScript hautement optimisé., La raison en est qu’une fois que vous intégrez SessionStack dans votre application web de production, elle commence à tout enregistrer: toutes les modifications DOM, les interactions utilisateur, les exceptions JavaScript, les traces de pile, les demandes réseau ayant échoué et les messages de débogage.
Avec SessionStack, vous pouvez rejouer les problèmes dans vos applications web sous forme de vidéos et voir tout ce qui est arrivé à votre utilisateur. Et tout cela doit se produire sans impact sur les performances de votre application web.
Il existe un plan gratuit qui vous permet de commencer gratuitement.