Comment JavaScript fonctionne: à l’intérieur du moteur V8 + 5 conseils sur la façon d’écrire du code optimisé

le 21 Août, 2017 · 11 min en lecture

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>

  • SpiderMonkey — le premier moteur JavaScript, qui à L’époque alimentait Netscape Navigator, et alimente Aujourd’hui Firefox
  • JavaScriptCore — open source, commercialisé sous le nom Nitro et développé par Apple pour Safari
  • KJS — le moteur de KDE initialement développé par Harri Porten pour le navigateur Web Konqueror du projet KDE
  • Chakra (JScript9) — Internet Explorer
  • Nashorn, Open Source dans le cadre D’OpenJDK, écrit par Oracle Java Languages and Tool Group
  • jerryscript — est un moteur léger pour l’internet des objets.,
  • 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”.,

    Chaque fois qu’un nouveau contenu est ajouté à un objet, la vieille classe caché est mis à jour avec une voie de transition vers la nouvelle classe caché. Les transitions de classes cachées sont importantes car elles permettent de partager des classes cachées entre des objets créés de la même manière., Si deux objets partagent une classe cachée et que la même propriété est ajoutée aux deux, les transitions garantissent que les deux objets reçoivent la même nouvelle classe cachée et tout le code optimisé qui l’accompagne.

    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.,

    Les deux objets sont fondamentalement les mêmes, mais le « a” et « b” les propriétés ont été créés dans un ordre différent.

    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.

    Améliorations sur le Web et le Nœud.,js repères

    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é

    1. 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.
    2. 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.,
    3. 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).
    4. 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.
    5. 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.

    Share

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *