V8 est le moteur JavaScript et WebAssembly open source de Google, écrit en C++. Il est utilisé dans Chrome et dans Node.js, entre autres. Il met en œuvre ECMAScript et WebAssembly et fonctionne sous Windows 7 ou ultérieur, macOS 10.12+ et les systèmes Linux qui utilisent des processeurs x64, IA-32, ARM ou MIPS. V8 peut fonctionner de manière autonome ou être intégré à toute application C++.
L'interpréteur de V8 est hautement optimisé et très rapide, mais les interpréteurs ont des frais généraux inhérents dont il est impossible de se débarrasser ; des choses comme les frais généraux de répartition qui font partie intrinsèque de la fonctionnalité d'un interpréteur. Avec le modèle actuel à deux compilateurs, il est impossible d’atteindre un code optimisé beaucoup plus rapidement ; l’équipe de V8 travaille à rendre l'optimisation plus rapide, mais à un certain point.
Un compilateur rapide
Sparkplug est conçu pour compiler rapidement. Selon V8, il y a quelques astuces qui rendent le compilateur Sparkplug rapide. Tout d'abord, il triche ; les fonctions qu'il compile ont déjà été compilées en bytecode, et le compilateur bytecode a déjà fait la majorité du travail difficile comme la résolution des variables, déterminer si les parenthèses sont en fait des fonctions flèches, déboguer les déclarations de déstructuration, et ainsi de suite. Sparkplug compile à partir du bytecode plutôt qu'à partir du code source JavaScript, et n'a donc pas à se soucier de tout cela. La deuxième astuce est que Sparkplug ne génère pas de représentation intermédiaire (IR) comme le font la plupart des compilateurs. Au lieu de cela, Sparkplug compile directement en code machine en un seul passage linéaire sur le bytecode, émettant un code qui correspond à l'exécution de ce bytecode. En fait, l'ensemble du compilateur est une instruction switch à l'intérieur d'une boucle for, qui envoie des fonctions fixes de génération de code machine par bytecode.
Code : | Sélectionner tout |
1 2 3 4 | // The Sparkplug compiler (abridged). for (; !iterator.done(); iterator.Advance()) { VisitSingleBytecode(); } |
Frames compatibles avec l'interpréteur
L'ajout d'un nouveau compilateur à une VM JavaScript existante est une tâche ardue. Il y a toutes sortes de choses que à prendre en charge au-delà de la simple exécution standard ; V8 a un débogueur, un profileur de CPU qui marche sur la pile, il y a des traces de pile pour les exceptions, l'intégration dans le tier-up, le remplacement sur la pile du code optimisé pour les boucles. Sparkplug fait un habile tour de passe-passe qui simplifie la plupart de ces problèmes, à savoir qu'il maintient des « frames de pile compatibles avec l'interpréteur ».
Notons que les frames de pile sont la façon dont l'exécution du code stocke l'état des fonctions ; chaque fois qu’une nouvelle fonction est appelée, un nouveau frame de pile est créé pour les variables locales de cette fonction. Un frame de pile est défini par un pointeur de frame (marquant son début) et un pointeur de pile (marquant sa fin).
Lorsqu'une fonction est appelée, l'adresse de retour est poussée sur la pile ; celle-ci est enlevée par la fonction lorsqu'elle revient, pour savoir où retourner. Ensuite, lorsque cette fonction crée un nouveau cadre, elle sauvegarde l'ancien pointeur de cadre sur la pile et place le nouveau pointeur de cadre au début de son propre cadre de pile. Ainsi, la pile possède une chaîne de pointeurs de cadre, chacun marquant le début d'un cadre qui pointe vers le cadre précédent.
Il s'agit de la disposition générale de la pile pour tous les types de fonctions ; il existe ensuite des conventions sur la façon dont les arguments sont passés, et sur la façon dont la fonction stocke les valeurs dans son cadre. Dans V8, il existe une convention pour les frames JavaScript que les arguments (y compris le récepteur) sont poussés dans l'ordre inverse sur la pile avant que la fonction soit appelée, et que les premiers emplacements sur la pile sont : la fonction actuelle appelée ; le contexte avec lequel elle est appelée ; et le nombre d'arguments qui ont été passés. Il s'agit de la disposition "standard" du frame JS.
Cette convention d'appel JS est partagée entre les trames optimisées et interprétées, et c'est ce qui permet, par exemple, de parcourir la pile avec une surcharge minimale lors du profilage du code dans le panneau de performance du débogueur.
Dans le cas de l'interpréteur Ignition, la convention est plus explicite. Ignition est un interprète basé sur des registres, ce qui signifie qu'il existe des registres virtuels (à ne pas confondre avec les registres de la machine !) qui stockent l'état actuel de l'interprète. Cela inclut les des fonctions locals JavaScript (déclarations var/let/const), et les valeurs temporaires. Ces registres sont stockés sur le cadre de la pile de l'interpréteur, avec un pointeur vers le tableau de bytecode en cours d'exécution, et le décalage du bytecode actuel dans ce tableau.
Sparkplug crée et maintient intentionnellement une disposition de trame qui correspond à celle de l'interprète ; chaque fois que l'interprète aurait stocké une valeur de registre, Sparkplug en stocke une aussi. Il agit ainsi pour plusieurs raisons :
- cela simplifie la compilation de Sparkplug ; Sparkplug peut simplement refléter le comportement de l'interprète sans avoir à conserver une sorte de correspondance entre les registres de l'interprète et l'état de Sparkplug ;
- cela accélère également la compilation, puisque le compilateur de bytecode a fait le travail difficile d'allocation de registre ;
- il rend l'intégration avec le reste du système presque triviale ; le débogueur, le profileur, le déroulement de la pile des exceptions, l'impression de la trace de la pile, toutes ces opérations font des parcours de la pile pour découvrir quelle est la pile actuelle des fonctions en cours d'exécution, et toutes ces opérations continuent à travailler avec Sparkplug presque sans changement, parce qu'en ce qui les concerne, tout ce qu'ils ont est un cadre d'interprète ;
- cela rend le remplacement sur la pile (OSR) trivial. L'OSR consiste à remplacer la fonction en cours d'exécution, actuellement, cela se produit lorsqu'une fonction interprétée se trouve à l'intérieur d'une boucle chaude (où elle s'élève vers le code optimisé pour cette boucle), et lorsque le code optimisé se désoptimise (où il s'abaisse et poursuit l'exécution de la fonction dans l'interpréteur). Avec les cadres de Sparkplug reflétant les cadres de l'interpréteur, toute logique OSR qui fonctionne pour l'interpréteur fonctionnera pour Sparkplug ; mieux encore, nous pouvons passer du code de l'interpréteur à celui de Sparkplug avec un surcoût de traduction de cadre presque nul.
Il y a un petit changement que nous faisons à la trame de la pile de l'interpréteur, qui est que nous ne gardons pas l'offset du bytecode à jour pendant l'exécution du code Sparkplug. Au lieu de cela, nous stockons une correspondance bidirectionnelle entre la plage d'adresses du code Sparkplug et l'offset du bytecode correspondant ; une correspondance relativement simple à coder, puisque le code Sparkplug est émis directement à partir d'une marche linéaire sur le bytecode. Chaque fois qu'un accès à la trame de la pile veut connaître le "bytecode offset" d'une trame Sparkplug, nous recherchons l'instruction en cours d'exécution dans ce mappage et renvoyons le bytecode offset correspondant. De même, chaque fois que nous voulons faire un OSR de l'interpréteur vers Sparkplug, nous pouvons rechercher l'offset de bytecode actuel dans le mappage, et sauter à l'instruction Sparkplug correspondante.
Source : L'équipe V8
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
Prisma : un ORM de nouvelle génération pour Node.js et TypeScript, pour concurrencer TypeORM et Sequelize et devenir la norme de l'industrie
StackBlitz annonce WebContainers, un outil qui permet de créer des environnements Node.js fullstack, il s'exécute dans un navigateur avec l'expérience d'édition de VS Code
TeaVM : un outil pour développer des applications Web rapides et modernes en Java, sans les difficultés d'une pile de développement JavaScript
Node.js 16 est maintenant disponible, et vient avec une mise à niveau du moteur JavaScript V8, des binaires préconstruits pour les puces Apple et des API stables supplémentaires