Maison > Article > interface Web > Comprendre le fonctionnement de JavaScript, plonger dans le moteur V8 et écrire du code optimisé
Recommandations d'apprentissage gratuites associées : javascript( Vidéo)
Un moteur JavaScript est un programme ou un interpréteur qui exécute du code JavaScript. Un moteur JavaScript peut être implémenté comme un interpréteur standard ou comme une forme de compilateur juste à temps qui compile JavaScript en bytecode.
Liste des projets populaires qui implémentent des moteurs JavaScript :
Le moteur V8 construit par Google est open source et écrit en C++. Ce moteur est utilisé dans Google Chrome, cependant, contrairement à d'autres moteurs, le V8 est également utilisé dans le populaire Node.js.
La V8 a été initialement conçue pour améliorer les performances d'exécution de JavaScript dans les navigateurs Web. Pour gagner en vitesse, V8 convertit le code JavaScript en code machine plus efficace au lieu d'utiliser un interpréteur. Il compile le code JavaScript en code machine au moment de l'exécution en implémentant un compilateur JIT (Just-In-Time), tout comme le font de nombreux moteurs JavaScript modernes tels que SpiderMonkey ou Rhino (Mozilla). La principale différence ici est que V8 ne génère pas de bytecode ni de code intermédiaire.
Avant la sortie de la version 5.9 du V8, le moteur V8 utilisait deux compilateurs :
Le moteur V8 utilise également plusieurs threads en interne :
Lorsque le code JavaScript est exécuté pour la première fois, V8 utilise le compilateur full-codegen pour traduire directement le JavaScript analysé en code machine sans aucune conversion. Cela lui permet de commencer à exécuter du code machine très rapidement. Notez que la V8 n'utilise pas de bytecode intermédiaire, éliminant ainsi le besoin d'un interprète.
Lorsque le code est exécuté depuis un certain temps, le fil d'analyse a collecté suffisamment de données pour déterminer quelle méthode doit être optimisée.
Ensuite, Vilebrequin démarre l'optimisation à partir d'un autre fil de discussion. Il convertit les arbres de syntaxe abstraite JavaScript en une représentation statique à allocation unique (SSA) de haut niveau appelée Hydrogen et tente d'optimiser le graphe Hydrogen, la plupart des optimisations sont effectuées à ce niveau.
La première optimisation consiste à intégrer autant de code que possible à l'avance. L'inlining est le processus de remplacement du site d'appel (la ligne de code qui appelle la fonction) par le corps de la fonction appelée. Cette étape simple permet aux optimisations suivantes de prendre plus de sens.
JavaScript est un langage basé sur des prototypes : les classes et les objets ne sont pas créés à l'aide d'un processus de clonage. JavaScript est également un langage de programmation dynamique, ce qui signifie que des propriétés peuvent être facilement ajoutées ou supprimées d'un objet après instanciation.
La plupart des interpréteurs JavaScript utilisent une structure de type dictionnaire (basée sur une fonction de hachage) pour stocker l'emplacement des valeurs de propriété d'objet en mémoire. Cette structure rend la récupération des valeurs de propriété en JavaScript plus rapide qu'en Java ou. C# Les coûts de calcul sont plus élevés dans les langages de programmation non dynamiques.
En Java, toutes les propriétés des objets 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 (bien sûr, C# a un typage dynamique, ce qui est un autre sujet).
Ainsi, les valeurs d'attribut (ou les pointeurs vers ces attributs) peuvent être stockées en mémoire sous forme de tampons contigus, avec des décalages fixes entre chaque tampon. La longueur du décalage peut être facilement déterminée en fonction du type d'attribut, tandis qu'en vous. peut modifier le type de propriété au moment de l'exécution, ce qui n'est pas possible en JavaScript.
Comme utiliser un dictionnaire pour trouver l'emplacement des propriétés d'un objet en mémoire est très inefficace, V8 utilise une approche différente : les classes cachées. Les classes cachées fonctionnent de la même manière que les objets fixes (classes) utilisés dans des langages comme Java, sauf qu'ils sont créés au moment de l'exécution. Maintenant, regardons leur exemple réel :
Une fois l'appel "new Point(1, 2)" effectué, V8 créera un type caché nommé "C0".
Aucune propriété n'a été définie pour Point, donc 'C0' est vide.
Une fois la première instruction "this.x = x" exécutée (au sein de la fonction "Point"), V8 créera une deuxième classe cachée appelée "C1", qui est basée sur "C0" . "C1" décrit l'emplacement en mémoire (par rapport au pointeur d'objet) où la propriété x peut être trouvée.
Dans ce cas, "x" est stocké au décalage 0, ce qui signifie que lorsque l'on considère l'objet point en mémoire comme un tampon contigu, le premier décalage correspondra à l'attribut "x" . La V8 mettra également à jour "C0" avec une "transformation de classe" qui stipule que si l'attribut "x" est ajouté à l'objet point, la classe cachée doit passer de "C0" à "C1". La classe cachée de l'objet point ci-dessous est désormais "C1".
Chaque fois qu'une nouvelle propriété est ajoutée à un objet, l'ancienne classe cachée est mise à jour avec un chemin de transformation pointant vers la nouvelle classe cachée. Les conversions 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é leur est ajoutée, la transformation garantira que les deux objets reçoivent la même nouvelle classe cachée et tout le code d'optimisation qui l'accompagne.
Le même processus est répété lorsque l'instruction "this.y = y" est exécutée (à l'intérieur de la fonction "Point", après l'instruction "this.x = x").
Une nouvelle classe cachée nommée "C2" sera créée. Si une propriété "y" est ajoutée à un objet Point (qui contient déjà la propriété "x"), une classe cast sera ajoutée à "C1". " , alors la classe cachée doit être remplacée par "C2" et la classe cachée de l'objet ponctuel doit être mise à jour par "C2".
La conversion de classe cachée dépend de l'ordre dans lequel les propriétés sont ajoutées à l'objet. Jetez un œil à l'extrait de code suivant :
Supposons maintenant que pour p1 et p2, la même classe cachée et la même transformation seront utilisées. Ainsi, pour "p1", ajoutez d'abord l'attribut "a", puis ajoutez l'attribut "b". Cependant, "p2" se voit d'abord attribuer "b", puis "a". Par conséquent, « p1 » et « p2 » se retrouvent avec des catégories cachées différentes en raison de chemins de transformation différents. Dans ce cas, il est préférable d'initialiser les propriétés dynamiques dans le même ordre afin que la classe cachée puisse être réutilisée.
V8 utilise une autre technique pour optimiser les langages typés dynamiquement, appelée mise en cache en ligne. La mise en cache en ligne repose sur l'observation selon laquelle les appels répétés à la même méthode ont tendance à se produire sur des objets du même type. Une explication détaillée de la mise en cache en ligne peut être trouvée ici.
Le concept général de la mise en cache en ligne sera abordé ensuite (si vous n'avez pas le temps d'examiner en profondeur ce qui précède).
Alors, comment ça marche ? La V8 maintient un cache des types d'objets passés en arguments dans les appels de méthode récents et utilise ces informations pour prédire les types d'objets passés en arguments à l'avenir. Si V8 peut prédire suffisamment bien le type d'objet transmis à une méthode, il peut contourner le processus d'accès aux propriétés de l'objet et utiliser à la place les informations stockées lors des recherches précédentes dans la classe cachée de l'objet.
Alors, quel est le lien entre les concepts de classes cachées et de mise en cache en ligne ? Chaque fois qu'une méthode est appelée sur un objet spécifique, le moteur V8 doit effectuer une recherche dans la classe cachée de cet objet pour déterminer le décalage auquel accéder à la propriété spécifique. Après deux appels réussis à la même classe cachée, V8 omet la recherche de la classe cachée et ajoute simplement le décalage de la propriété au pointeur d'objet lui-même. Pour tous les prochains appels à cette méthode, le moteur V8 suppose que la classe cachée n'a pas changé et passe directement à l'adresse mémoire de la propriété spécifique en utilisant le décalage stocké lors de la recherche précédente. Cela améliore considérablement la vitesse d’exécution.
La mise en cache en ligne est également la raison pour laquelle il est important que les objets du même type partagent des classes cachées. Si vous créez deux objets du même type et des classes cachées différentes (comme nous l'avons fait dans notre 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 sont ses les propriétés se voient attribuer différents décalages.
Les deux objets sont fondamentalement les mêmes, mais les propriétés "a" et "b" sont créées dans un ordre différent.
Une fois le graphique de l'hydrogène optimisé, Vilebrequin le réduit à une représentation de niveau inférieur appelée Lithium. La plupart des implémentations de Lithium sont spécifiques à l'architecture. L'attribution des registres se produit souvent à ce niveau.
Enfin, Lithium est compilé en code machine. Ensuite, il y a OSR : remplacement sur la pile. Avant de commencer à compiler et à optimiser une méthode explicite de longue durée, nous pouvons exécuter le remplacement de pile. Le V8 ne se contente pas d'effectuer lentement le remplacement de la pile et de recommencer l'optimisation. Au lieu de cela, il convertit tout le contexte dont nous disposons (pile, registres) pour passer à la version optimisée lors de l'exécution. Il s'agit d'une tâche très complexe, étant donné que, entre autres optimisations, la V8 intègre initialement le code. Le V8 n’est pas le seul moteur capable de le faire.
Il existe une mesure de sécurité appelée désoptimisation qui effectue la conversion inverse et renvoie du code non optimisé en supposant que le moteur n'est pas valide.
Pour la collecte des ordures, V8 utilise l'algorithme traditionnel de marquage et de balayage pour nettoyer l'ancienne génération. La phase de marquage doit arrêter l'exécution de JavaScript. Pour contrôler les coûts du GC et rendre l'exécution plus stable, V8 utilise un marquage incrémentiel : au lieu de parcourir tout le tas et d'essayer de marquer tous les objets possibles, il parcourt simplement une partie du tas puis reprend l'exécution normale. Le prochain arrêt du GC continuera là où le tas précédent s'est arrêté, ce qui permet une très brève pause pendant l'exécution normale, comme mentionné précédemment, la phase d'analyse est gérée par un thread séparé.
Avec la sortie de la V8 5.9 début 2017, un nouveau pipeline d'exécution a été introduit. Ce nouveau pipeline permet des gains de performances plus importants et des économies de mémoire significatives dans les applications JavaScript réelles.
Le nouveau flux d'exécution est construit sur Ignition (l'interpréteur de V8) et TurboFan (le dernier compilateur d'optimisation de V8).
Depuis la sortie de la V8 5.9, l'équipe V8 s'est éloignée du full-codegen et de Crankshaft (depuis 2010) car l'équipe V8 a eu du mal à suivre les nouvelles fonctionnalités du langage JavaScript et les optimisations que ces fonctionnalités nécessitent. Desservi par la technologie V8).
Cela signifie que la V8 aura globalement une architecture plus simple et plus maintenable.
Ces améliorations ne sont que le début. Les nouveaux pipelines Ignition et TurboFan ouvrent la voie à de nouvelles optimisations qui amélioreront les performances de JavaScript et réduiront l'empreinte de la V8 dans Chrome et Node.js pour les années à venir.
Recommandations d'apprentissage gratuites associées : programmation php (vidéo)
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!