Fermer

mai 23, 2019

du téléchargement de scripts à l'exécution (2e partie)


Dans cet article, vous apprendrez comment les moteurs JavaScript sont passés d’un simple interpréteur à un moteur performant et efficace produisant un code machine hautement optimisé. Vous découvrirez également les composants sous-jacents du moteur JavaScript V8, notamment la manière dont l'interpréteur génère un code-octet avec une arborescence de syntaxe abstraite et comment le compilateur l'utilise pour générer un code machine optimisé. Cet article vous aidera également à comprendre certaines techniques d'optimisation des performances d'objets et de tableaux.

Cet article fait partie de la série sur La trajectoire de JavaScript – du téléchargement de scripts à l'exécution .

Faits saillants de la partie I de la série

  1. Nous avons appris les différentes façons de télécharger des scripts en fonction du cas d'utilisation. Les scripts sont téléchargés de manière synchrone et bloquent par nature. Lorsque le thread principal rencontre une balise de script, il bloque l'analyse de HTML DOM jusqu'à ce que le script complet soit téléchargé, analysé et exécuté. Cependant, les scripts peuvent également être téléchargés de manière asynchrone, sans bloquer le thread principal, avec l'utilisation du mot clé async dans la balise script. Si aucun de ces scripts n'est requis au chargement, nous pouvons différer leur exécution jusqu'à ce que le DOM soit prêt en utilisant le mot clé defer .
  2. Les moteurs JavaScript sont constitués d'un analyseur, d'un interpréteur et d'un compilateur. Le code source JavaScript est divisé en jetons, qui sont transmis à l'analyseur. L'analyseur génère un arbre de syntaxe abstraite (AST) et des portées basées sur ces jetons.
  3. Les moteurs JavaScript n'analysent pas tout le code source lors du chargement. Nous avons vu les heuristiques utilisées par le moteur V8 pour analyser le code JavaScript.

Vous pouvez en savoir plus sur les points ci-dessus ici .

Présentation des moteurs JavaScript

La sémantique de JavaScript est définie par les spécifications ECMAScript et ces spécifications sont rédigées par le comité TC39. Les moteurs JavaScript sont tenus de respecter ces spécifications tout en implémentant différentes fonctionnalités en JavaScript. La plupart des principaux navigateurs ont leur propre implémentation de ces moteurs, mais ils ont le même objectif final de respecter la sémantique établie par le comité TC39. Ainsi, la plupart des techniques liées aux performances sont applicables dans presque tous les navigateurs.

Regardons la liste des moteurs JavaScript de certains des principaux navigateurs.

Dans cet article, nous allons approfondir les informations internes du Moteur JavaScript V8 .

Architecture de haut niveau du V8

 alt Architecture de haut niveau du V8

Comme nous pouvons le voir sur l'image ci-dessus, le code source JavaScript est alimenté à l'analyseur. L'analyseur génère un AST. Ignition génère du bytecode et TurboFan génère un code machine optimisé. Ne vous inquiétez pas des flèches rouges et vertes pour le moment. Ils auront du sens une fois que nous aurons utilisé Ignition et TurboFan.

En quoi les moteurs JavaScript sont-ils différents des moteurs des autres langages de programmation?

Les langages de haut niveau tels que C ++ et Java prennent deux mesures pour convertir le code source. au code machine. Le compilateur convertit d'abord le code source en un code intermédiaire, puis l'interpréteur prend ce code intermédiaire et le convertit en code machine. Par exemple, un fichier Java est compilé en tant que javac nom_fichier.java . Cette commande génère un bytecode et le stocke dans le fichier filename.class . Ce bytecode peut être exécuté sur n’importe quel ordinateur doté de Java Virtual Machine / Java Interpreter. Il peut être exécuté à l'aide de la commande java filename.class . L'étape de compilation initiale nécessite beaucoup de travail et le serveur peut donc exécuter le bytecode Java plus rapidement.

Malheureusement, la stratégie ci-dessus n'est pas implémentée dans les moteurs JavaScript. Les moteurs JavaScript ne suivent pas une procédure en deux étapes pour l'exécution du code source. Nous ne faisons jamais compiler nom_fichier.js . Nous exécutons directement le fichier JavaScript dans les navigateurs ou au moment de l'exécution avec Node.js. Les moteurs ne compilent pas tout le code source en une seule fois. Au lieu de cela, les moteurs JavaScript interprètent le code source ligne par ligne et exécutent cette ligne simultanément.

Ne pensez-vous pas que cela ralentirait beaucoup l'exécution de JavaScript par rapport à d'autres langages de haut niveau? JavaScript exécute le code source, alors que d'autres langages de haut niveau que nous avons vus précédemment utilisent le bytecode optimisé, généré à l'étape précédente de la compilation.

Regardons certaines statistiques

Voici les informations suivantes: résultats de performance avec test de tests d'utilisation du matériel. Il est tiré de de cet article d'IBM.

 alt Comparaison de nodejs avec Java

Node.js utilise le moteur JavaScript V8 comme run-time. Dans l'image ci-dessus, nous pouvons voir que Node.js fonctionne mieux que Java en ce qui concerne l'utilisation du processeur, alors que ses performances sont équivalentes à celles de Java en ce qui concerne l'utilisation en mémoire.

Voici l'instantané des langages les plus populaires. d'après l'enquête Stack Overflow de 2018 .

 alt Enquête Stack Overflow 2018

Conformément à l'image ci-dessus, JavaScript est le langage le plus utilisé par les développeurs professionnels.

Même si les moteurs JavaScript ignorent l'étape de compilation, ses performances et sa popularité sont meilleures que celles des autres langues. Voyons comment les moteurs JavaScript rendent cela possible.

Évolution des moteurs JavaScript

Le premier moteur JavaScript était un simple interprète. Un interpréteur est un logiciel qui exécute le code source ligne par ligne.

Prenons un extrait de code JavaScript ci-dessous pour comprendre le fonctionnement des moteurs précédents.

 function   arrSum   ( arr [3])   {
     var  sum  =   0 
     pour   ( var  i  =   0  ;  i  < arr .  length ;  i  ++ )   {
        sum  + =  arr  [ i ] 
    } 
} 

Il a une fonction simple arrSum qui ajoute les éléments de un tableau arr . Dans la boucle de il n’ya qu’une déclaration qui ajoute des éléments de tableau à la variable sum .

Considérez ceci du point de vue de l’interprète –

Il Il est possible en JavaScript qu'un tableau puisse avoir des nombres ainsi que des chaînes simultanément. arr pourrait avoir un mélange de différents types de données. À chaque itération de la boucle pour l'interpréteur vérifie le type d'élément dans le tableau et effectue l'opération d'ajout / concaténation en conséquence. + se comporte comme un opérateur addition pour les nombres et comme opérateur de concaténation pour les chaînes.

Ce type de vérification et de calcul à chaque itération le ralentit. Auparavant, JavaScript n'était pas considéré comme une langue de choix, car il était très lent par rapport à d'autres langages de haut niveau.

Toutefois, comme nous l'avons vu à partir des statistiques ci-dessus, JavaScript est désormais plus performant et est très apprécié. langage entre développeurs professionnels.

Chrome a développé le premier moteur JavaScript moderne, le V8, en 2008. Ses performances étaient bien meilleures que celles de tous les moteurs précédents. Chrome a utilisé la compilation juste à temps dans le moteur V8 pour améliorer ses performances. La plupart des navigateurs / systèmes d'exécution JavaScript utilisent désormais la même technique pour accélérer la vitesse d'exécution du code JavaScript.

Qu'est-ce que la compilation juste-à-temps (JIT)?

Si le code d'ajout d'éléments ci-dessus d’un tableau a été compilé en premier, le démarrage aurait pris un certain temps, mais son exécution aurait été plus rapide que celle de la technique précédente. JIT prend les bonnes parties du compilateur et de l'interprète. Il interprète le code source ligne par ligne, génère du bytecode pour cette ligne et le transmet au compilateur, qui utilise les informations de profilage pour produire des optimisations spéculatives. Il compile le code pendant l'exécution au moment de l'exécution.

Les navigateurs ont commencé à être expédiés dans des moteurs JavaScript avec JIT et un profileur. Un profileur, ou un moniteur, surveille le code qui s'exécute et note le nombre de fois qu'un extrait de code particulier est exécuté. Si les mêmes lignes de code sont exécutées pour plus d'une valeur de seuil, ce code est appelé hot. Le profileur envoie ensuite ce code chaud au compilateur optimiseur.

Les détails de la compilation du code chaud sont enregistrés. Si le profileur rencontre à nouveau ce code chaud, il le convertit dans sa version optimisée existante. Cela aide à améliorer les performances d'exécution du code JavaScript. Nous aborderons les hypothèses du compilateur pour générer du code optimisé plus tard dans cet article.

Comment JIT fonctionne-t-il en V8

 alt Interprète et Compilateur dans le moteur JavaScript V8

Ignition ( interprète) prend AST et passe par ses nœuds un par un et produit en conséquence les bytecodes. Le profileur surveille la fréquence à laquelle un extrait de code particulier est exécuté. Si la fréquence dépasse une valeur seuil, elle envoie le code à chaud avec certaines informations de profilage au TurboFan (compilateur). TurboFan émet certaines hypothèses pour optimiser davantage ce code et, si ces hypothèses sont vérifiées, il génère une version optimisée pour le code dynamique. La flèche verte qui indique le succès de l'optimisation!

Si les hypothèses ne sont pas correctes, elle revient au bytecode généré par Ignition. C'est ce qu'on appelle un sauvetage par désoptimisation ou optimisation. La flèche rouge signifie la désoptimisation!

Découpons JIT en plusieurs parties et expliquons en détail le fonctionnement de chacun des composants.

Comment Ignition génère-t-il le bytecode

Les bytecodes sont considérés comme de petits blocs de construction pouvant être composés ensemble construire une fonctionnalité JavaScript. Ils font abstraction des détails de bas niveau du code machine. V8 a des centaines de bytecodes pour différentes fonctionnalités. Par exemple, il utilise le bytecode Add pour l'opérateur d'addition et le bytecode CreateObjectLiteral pour créer un objet.

Ignition utilise une machine à registre pour conserver l'état local. des registres. Il possède un registre spécial appelé un accumulateur qui stocke la valeur précédemment calculée.

Considérons un simple extrait de code JavaScript.

 function   add   ( x  y )   {
     return  x  +  y ; 
} 
 add   [ 1   2 ) ; 

Voici le bytecode généré pour le code ci-dessus:

 alt ByteCode de la fonction add générée par Ignition

Concentrez-vous uniquement sur la partie droite. . Les registres a0 et a1 indiquent la valeur des paramètres formels. Add est un bytecode utilisé pour ajouter les valeurs dans les registres a0 et a1 .

Avant de sauter sur le fonctionnement de TurboFan, nous devons comprendre deux concepts importants dans la V8:

  1. Implémentation du modèle objet
  2. Implémentation des tableaux

Implémentation du modèle objet

La spécification ECMAScript définit des objets tels que des dictionnaires avec des clés de chaîne mappant des valeurs. Dans cette section, nous allons apprendre comment les moteurs JavaScript stockent les objets et comment ils implémentent l'accès aux propriétés sur ces objets.

 let  pokemonObj  =   {
    id :   12 
    nom :   'Sans beurre' 
    hauteur :   11 
    poids :   22 
} 

Ci-dessous l'AST de l'objet de pokemonObj .

 {
   du "type" :   "du programme "
  " début ":   0 
  " fin ":   87 
  " corps " :   [
     {
       "type" :   "VariableDeclaration" 
       "start" :   0 [196590115] "end" :   87 
       "déclarations" :   [
         {
           "type" :   "VariableDeclarator" [19659042]
           "début" :   4 
           "fin" :   87 
           "id" : [id19659034] {
             "type" :   "Identificateur" 
             "début" :   4 
             "fin"  :   14 
             "nom" :   "pokemonObj" 
          } 
           "dans it ":   {
            " type ":  " ObjectExpression "
            " start ":   17 [19659146] "end" :   87 
             "propriétés" :   [
               {
                 "type" :   "Propriété" 
                 "début" :   23 
                 "fin" :   29 
                 "méthode" :   false 
                 "sténographie" :   false 
                 "calculée" :   false 
                "clé" :   {
                   "type" :   "Identificateur" 
                   "début" :   23 
                   " fin ":   25 
                  " nom ":  " id "
                } 
                " valeur ":   ] {
                   "type" :   "Littéral" 
                   "début" :   27 
                   "fin" [19659042]:   29 
                   "valeur" :   12 
                   "raw" :   "12" 
                } [19659042]
                 "kind" :   "init" 
              } 
               {
                 "type" :   "Propriété"  , 
                 "début" :   35 
                 "fin" :   53 
                 "méthode" :   false 
                 "sténographie" :   false 
                 "calculée" :   false 
                 "clé"  :   {
                   "type" :   "Identifiant" 
                   "start" :   35 
                   "end" [fin] [[19659042]:   39 
                   "nom" :   "nom" 
                } 
                 "valeur" :   {[19659209] "type" :   "Littéral" 
                   "début" :   41 
                   "fin" :   53 [19659042]
                   "valeur" :   "Sans beurre" 
                   "Brut" :   "'Sans beurre" "
                } [19659182] "genre" :   "init" 
              } 
               {
                 "type" :   "Propriété" 
                 "début" :   59 
                 "fin" :   69 
                 "méthode" :   fausse  , 
                 "raccourci" :   false 
                 "calculé" :   false 
                 "clé" :   {
                   "type" :   "Identificateur" 
                   "début" :   59 
                   "fin" : [19659037] 65 
                   "nom" :   "hauteur" 
                } 
                 "valeur" :   {
                   "type ":  " Littéral "
                  " début ":   67 
                  " fin ":   69  ]
                   "valeur" :   11 
                   "raw" :   "11" 
                } 
                 "nature" :   "init" 
              } 
               {
                 "type" :   "Propriété" 
                 "start" [19659042]:   75 
                 "end" :   85 
                 "méthode" :   false 
                 " raccourci ":   false 
                " calculé ":   false 
                " clé ":   {
                  " type ":  " Identificateur "
                  " début ":   75 
                  " fin ":   81  ]
                   "nom" :   "poids" 
                } 
                 "valeur" :   {
                   "type" :   "Littéral" 
                   "début" :   83 
                   "fin" :   85 
                   " valeur "[19659042]:   22 
                   "raw" :   "22" 
                } 
                 "kind" :   "init "
              } 
            ] 
          } 
        } 
      ] 
      " genre ":  " let "
    ] 
  ] [1965922] 19659042]
   "sourceType" :   "module" 
} 

Notez l'utilisation du tableau properties qui contient les paires clé-valeur de l'objet. Conformément à la spécification ECMAScript, les clés d'un objet doivent être mappées sur leurs attributs de propriété respectifs. Les attributs de propriété en disent plus sur la configuration de cette clé dans l'objet.

Voici la liste des attributs de propriété:

value La valeur associée à la propriété. Peut être n'importe quelle valeur JavaScript valide (nombre, objet, fonction, etc.). undefined
enumerable true si et seulement si cette propriété apparaît lors de l'énumération des propriétés du objet correspondant. false
accessible en écriture true si et seulement si la valeur associée à la propriété peut être modifiée à l'aide d'un opérateur d'affectation . false [19659496] configurable true si et seulement si le type de ce descripteur de propriété peut être modifié et si la propriété peut être supprimée de l'objet correspondant. false

Supposons qu'un programme a cent occurrences de l'objet de pokemonObj ce qui est probablement le cas dans des scénarios courants. Cela signifierait que le moteur est censé créer une centaine d'objets avec les quatre clés de pokemonObj - id nom hauteur et poids - avec leurs attributs de propriété. C’est un tel gaspillage de mémoire que de stocker des instances répétées des métadonnées d’un objet. Les moteurs JavaScript résolvent ce problème en stockant une copie partagée des métadonnées d'un objet. Ainsi, pour des centaines d'instances pokemonObj il ne crée qu'un seul objet pour stocker les métadonnées communes.

 alt Plusieurs objets partageant les mêmes métadonnées

Nous pouvons l'optimiser davantage. Chacun de ces objets contient les quatre mêmes clés - id name height et weight . Ils ont tous la même forme de de pokemonObj . Nous pouvons créer une forme avec ces quatre clés et laisser toutes les occurrences pointer vers cette forme. Ceci apparaîtra clairement dans le diagramme ci-dessous.

 alt Les objets JavaScript partagent la même forme

Comme on peut le voir sur l'image ci-dessus, tous les Pokémons différents se réfèrent à la même forme (id, nom, hauteur, poids) et chacune des clés de cette structure de forme fait référence à la structure de son attribut de propriété respectif. C’est une économie importante en termes de mémoire!

Il existe une propriété supplémentaire offset qui est incluse dans la liste des attributs de propriété. Veuillez noter que id a un offset de 0 ; name a un offset de 1 ; etc. Si nous suivons le modèle, on peut en déduire que les clés de de pokemonObj sont affectées à un index séquentiel selon leur position dans le tableau de propriétés défini sur l'AST.

Comprenons la nécessité de la propriété offset .

Pour accéder à une propriété située sur le pokemonObj nous utilisons le . (point) opérateur sous pokemonObj.property . Disons que nous voulons les valeurs de nom, hauteur et capacités d’un Pokémon.

 let  name  =  pokemonObj .  name
 let  height  =  pokemonObj .  hauteur
 let  capacités  =  pokemonObj .  capacités

Lorsque le moteur JavaScript rencontre quelque chose comme de pokemonObj.name il recherche la propriété name définie sur la forme de de pokemonObj . S'il est trouvé, il retourne la valeur. Puisque capacités n'est pas défini sur la forme de pokemonObj le moteur recherche la propriété capacités sur l'objet prototype de . pokemonObj . Il continue à parcourir la chaîne jusqu'à trouver la clé des capacités ou à atteindre le bout de la chaîne. Dans notre cas, il est retourné undefined ce qui signifie que les capacités n'étaient définies sur aucun des ancêtres de de pokemonObj . Les accès aux propriétés sont difficiles en JavaScript. C'est une opération coûteuse pour un moteur de parcourir toute la chaîne de prototypes pour une seule propriété.

L'accès aux propriétés pour les clés le nom et la une hauteur est très simple en raison du ] offset propriété. Le moteur JavaScript renvoie simplement la valeur de l'objet en utilisant le décalage tel que défini sur une propriété particulière. Pour name il renverrait la première valeur de pokemonObj et pour height il renverrait sa deuxième valeur.

Disons que nous avons un autre Pokémon appelé weedle a rejoint notre gang. Celui-ci possède des capacités uniques et nous voulons vraiment stocker toutes ses capacités dans notre objet de pokemonObj .

 let  weedle  =   {
    id :   13 
    nom :   'mauvaise herbe' 
    hauteur :   3 
    poids :   32 
    capacités :   'fugue' 
} 

Nous avons déjà défini la forme de de pokemonObj . JavaScript permet d'ajouter / supprimer des propriétés d'un objet au moment de l'exécution. Comment gérons-nous l'ajout de la propriété capacités à notre forme de pokemonObj ? Devrions-nous créer une nouvelle forme pour cet objet, ou devrions-nous agrandir la forme précédente? Vous avez peut-être bien deviné. Nous allons étendre notre forme originale de pokemonObj pour laisser la place à une clé supplémentaire appelée capacités .

 alt Les objets JavaScript étendent les formes

L'extension des formes permet il est facile de stocker des objets dont certaines propriétés diffèrent de leurs homologues.

Ceci peut également être considéré comme une chaîne hiérarchique qui part d'un objet vide et construit finalement l'objet entier.

 let  obj  =   {} 
obj .  a  =   'a' 
obj .  b  =   'b' 
obj .  c  =   'c' 

Il commence par une forme vide. obj.a = 'une' déclaration étend la forme vide et lui ajoute une propriété a . Cela continue jusqu'à ce que nous atteignions c . Veuillez noter que ce n'est pas un moyen efficace de créer des objets. Pour créer l'objet ci-dessus, suivez la méthode de déclaration d'objet ci-dessous:

 let  obj  =   {
    a :   'a' 
    b :   'b' 
    c :   'c' 
} 

Points à retenir de la mise en œuvre du modèle objet

  1. Évitez d'ajouter ou de supprimer des propriétés d'un objet. Efforcez-vous de maintenir une structure constante pour les objets.
  2. Évitez de réorganiser les clés d'un objet. Le moteur JavaScript prend en compte le classement des clés d'un objet à l'aide de la propriété offset .
  3. Les accès aux propriétés sont coûteux en JavaScript. Le moteur va à la racine pour trouver une propriété particulière. Au lieu d'accéder directement à une propriété comme pokemonObj.abilities utilisez la méthode hasOwnProperty comme ci-dessous.
 if   [ pokemonObj .  hasOwnProperty [19659042] ( "capacités" ) )   {
    
} 

La méthode hasOwnProperty ne recherche que sur l'objet et non sur sa chaîne prototype.

] Implémentation de tableaux

Semblables aux objets, les tableaux sont également considérés comme des dictionnaires. Mais dans le cas des tableaux, les clés sont numériques, appelées indices de tableau. Les moteurs JavaScript effectuent des optimisations spéciales pour les propriétés dont les noms sont purement numériques. Les objets ont des propriétés qui correspondent à des valeurs, tandis que les tableaux ont des index qui correspondent à des éléments.

En JavaScript, nous ne définissons pas le type des éléments qu'un tableau contiendra auparavant. Lors de l'exécution de code JavaScript, le moteur détermine le type des éléments d'un tableau et effectue des optimisations en fonction de ce type.

 let  arrIntegers  =   [ 1   ] 2   3   4 ] 
 let  arrDoubles  =   [ 1   2.2.   3.3 ] 
 let  arrMix  =   [ 1   2.2  "Hello World ! '] 

Le arrIntegers contient des éléments de type nombres entiers; arrDoubles contient des entiers ainsi que des doubles; tandis que arrMix contient des nombres entiers, des doubles ainsi que des chaînes

Les moteurs identifient ces trois types de tableaux a:
arrIntegers - SMI_ELEMENTS arrDoubles – DOUBLE_ELEMENTS
arrMix ELMENTS

SMI_ELEMENTS est un type plus spécifique qui ne contient que de petits entiers. DOUBLE_ELEMENTS contient des nombres à virgule flottante et un entier, alors que ELEMENTS est une forme générale contenant tout.

Il existe deux variantes à ces types définis ci-dessus.

Si un tableau est dense, il ne contient aucune valeur indéfinie – il est défini comme une variante PACKED . Toutefois, si un tableau contient peu d'espaces ou de trous ou de valeurs non définies il est défini comme un tableau HOLEY .

 let  emballéArr  =    1   2   3   4 ] 
 let  holeyArr  =   nouveau [19659611] Tableau  ( 4 ) 
holeyArr  [ 0 ]   =   1 

PackArr a des valeurs définies pour tous ses indices. Dans la deuxième déclaration, le constructeur Array crée un tableau vide de longueur 4. Il a quatre valeurs undefined . Quand on fait holeyArr [0] = 1 il contient encore trois valeurs non définies . Même si nous peuplons tous les indices de holeyArr avec des valeurs définies, il sera toujours appelé HOLEY . Les tableaux ne peuvent à aucun moment passer d'un type plus générique à son variant spécifique. PACKED est une variante plus spécifique par rapport à HOLEY .

Le moteur JavaScript catégorise wrapper comme PACKED_SMI_ELEMENTS car il contient tout. il ne contient aucune valeur undefined . V8 peut effectuer de meilleures optimisations pour des éléments de types plus spécifiques. Les opérations sur les baies PACKED sont plus efficaces que celles sur les baies HOLEY .

La V8 distingue actuellement 21 types d'éléments chacun livré avec son propre jeu. des optimisations possibles.

 alt Elements Kinds

Il est uniquement possible de descendre le réseau. Un tableau ne peut revenir à sa version plus spécifique à aucun moment.

Évitez de créer des trous dans un tableau

Si vous initialisez un tableau en utilisant un constructeur de tableau comme ci-dessous –

 let  arr  =   new   Tableau  ( 3 ) 

– Il crée des trous dans le tableau. Le moteur identifie ce tableau comme étant du type HOLEY_ELEMENTS . Les accès à la propriété sur HOLEY_ELEMENTS sont chers. Si vous voulez que la valeur soit arr [0]le moteur recherche d'abord la propriété 0 sur le tableau. Array arr ne contient encore aucune des propriétés. Le moteur ne pouvant pas trouver la clé 0 sur arr il descend jusqu'à sa chaîne de prototypes jusqu'à ce qu'il trouve la clé 0 ou soit arrivé au bout de la chaîne. Cela le rend similaire aux objets, et donc même après l'utilisation de tableaux, nous ne sommes pas en mesure de l'optimiser.

Créez plutôt des tableaux à l'aide d'un littéral vide, puis continuez à leur appliquer des valeurs.

 let  arr.  =   [] 
arr .  push  ( 1 ) 
arr.push(2)

With this approach, the number of elements present in the array is the same as that of its length.

For the same reason, access only those properties on the array that are defined within its length. If you access a property that is not defined on an array, it will enter into expensive lookups on the prototype chain.

V8 performs several optimizations for array built-in methods like mapreduce and filter.

Speculative Optimizations by the Compiler

If a certain code snippet has been executed some x number of times, the profiler passes it to the optimizing compiler. TurboFan uses the profiling information and makes some assumptions to produce optimized machine code.

Some of its assumptions are:

  1. The type of a variable is not changed.
  2. The structure of the object has not changed, and, hence, it can directly use the offset property defined on the object.

Inline Caches

Inline caches are the key ingredients in making JavaScript run fast. JavaScript engines use inline caches to memorize information on where to find a particular property of an object. The engines store the offset values and the bytecodes that are repeated too often in inline caches.

Deoptimization or Optimization Bailout

If the assumptions made by the compiler are not true, work done by the compiler is ignored and the engine falls back to the bytecode generated by the Ignition. This is called as deoptimization or optimization bailout. This happens because of one of the following reasons:

  1. The type of element has been changed at run time
let a = 2
a = 'Hello World!'
  1. The structure of an object has been changed.
  2. Element kinds of an array has been changed from PACKED to HOLEY version. Please note, the engines use a lot of optimization techniques for PACKED arrays. Avoid holes or undefined in your arrays.

Let’s Recap All That We Have Learned in this Tutorial

  1. JavaScript engines evolved from a mere interpreter to modern engines that use just-in-time-compilation. Just-in-time compilation combines the good parts of both the compiler and an interpreter. It compiles JavaScript code at run-time.
  2. V8 uses Ignition as the interpreter and TurboFan as the optimizing compiler. Ignition produces bytecode using AST as the input. The profiler watches code as it runs. It sends off the hot code to TurboFan. TurboFan uses profiling information to generate optimized machine code.
  3. We learned that interpreter has a stack of registers and it uses these registers to store parameters/variables. V8 has defined bytecodes for various operations like adding and creating objects or functions.
  4. We learned how JavaScript engines handle different Elements Kinds.
  5. Objects are stored in the form of shapes, which saves a lot of memory and also makes it easy to fetch any of the properties on an object.
  6. We saw some of the assumptions that a compiler makes to produce optimized code. If any of the assumptions fail, it deoptimizes its code and falls back to the bytecode that was generated by Ignition.

References

  1. https://www.youtube.com/watch?v=p-iiEDtpy6I
  2. https://www.youtube.com/watch?v=5nmpokoRaZI
  3. https://mathiasbynens.be/notes/shapes-ics
  4. https://v8.dev/blog/elements-kinds

This post has been brought to you by Kendo UI

Want to learn more about creating great web apps? It all starts out with Kendo UI – the complete UI component library that allows you to quickly build high-quality, responsive apps. It includes everything you need, from grids and charts to dropdowns and gauges.

KendoJSft" title="KendoJSft" style="vertical-align: middle;





Source link