Aujourd'hui, nous publions la deuxième partie de la traduction du matériel sur les mécanismes internes de la V8 et l'enquête sur le problème de performance de React.

→
La première partieObsolescence et migration des formes d'objets
Que faire si le champ contenait initialement une
Smi
, puis que la situation a changé et qu'il fallait stocker une valeur pour laquelle la représentation
Smi
ne convient pas? Par exemple, comme dans l'exemple suivant, lorsque deux objets sont représentés en utilisant la même forme de l'objet dans lequel
x
initialement stocké en tant que
Smi
:
const a = { x: 1 }; const b = { x: 2 };
Au début de l'exemple, nous avons deux objets, pour la représentation desquels nous utilisons la même forme de l'objet dans lequel le format
Smi
est utilisé pour stocker
x
.
Le même formulaire est utilisé pour représenter les objetsLorsque la propriété
bx
change et que vous devez utiliser le format
Double
pour la représenter, V8 alloue de l'espace mémoire pour la nouvelle forme de l'objet, dans laquelle
x
est affectée à la représentation
Double
et qui indique une forme vide. V8 crée également une entité
MutableHeapNumber
, qui est utilisée pour stocker la valeur 0,2 de la propriété
x
. Ensuite, nous mettons à jour l'objet
b
pour qu'il se réfère à ce nouveau formulaire et modifions l'emplacement dans l'objet afin qu'il se
MutableHeapNumber
à l'entité
MutableHeapNumber
précédemment créée à l'offset 0. Enfin, nous marquons l'ancien formulaire de l'objet comme obsolète et le déconnectons de l'arborescence transitions. Cela se fait en créant une nouvelle transition pour
'x'
du formulaire vide à celui que nous venons de créer.
Conséquences de l'attribution d'une nouvelle valeur à une propriété d'objetPour le moment, nous ne pouvons pas supprimer complètement l'ancien formulaire, car il est toujours utilisé par l'objet
a
. De plus, il sera très coûteux de contourner toute la mémoire dans la recherche de tous les objets qui se réfèrent à l'ancien formulaire et de mettre à jour immédiatement l'état de ces objets. Au lieu de cela, le V8 utilise ici une approche «paresseuse». A savoir, toutes les opérations de lecture ou d'écriture des propriétés de l'objet
a
abord transférées vers l'utilisation d'un nouveau formulaire. L'idée derrière cette action est de rendre finalement la forme obsolète de l'objet inaccessible. Cela entraînera le garbage collector à y faire face.
La mémoire hors forme libère le ramasse-miettesLes choses sont plus compliquées dans les situations où le champ qui change la vue n'est pas le dernier de la chaîne:
const o = { x: 1, y: 2, z: 3, }; oy = 0.1;
Dans ce cas, le V8 doit trouver la soi-disant forme fendue. Il s'agit du dernier formulaire de la chaîne, situé avant le formulaire dans lequel la propriété correspondante apparaît. Ici, nous changeons
y
, c'est-à-dire que nous devons trouver la dernière forme sous laquelle il n'y avait pas de
y
. Dans notre exemple, c'est la forme sous laquelle
x
apparaît.
Rechercher le dernier formulaire dans lequel aucune valeur n'a été modifiéeIci, en commençant par ce formulaire, nous créons une nouvelle chaîne de transition pour
y
qui reproduit toutes les transitions précédentes. Seulement maintenant, la propriété
'y'
sera représentée comme un
Double
. Nous utilisons maintenant cette nouvelle chaîne de transition pour
y
, la marquant comme un ancien sous-arbre obsolète. Dans la dernière étape, nous migrons l'instance de l'objet
o
vers un nouveau formulaire, en utilisant maintenant l'entité
MutableHeapNumber
pour stocker la
MutableHeapNumber
y
. Avec cette approche, le nouvel objet n'utilisera pas de fragments de l'ancien arbre de transition et, après que toutes les références à l'ancienne forme auront disparu, la partie obsolète de l'arbre disparaîtra également.
Extensibilité et intégrité de transition
La commande
Object.preventExtensions()
vous permet d'empêcher complètement l'ajout de nouvelles propriétés à un objet. Si vous traitez l'objet avec cette commande et essayez d'y ajouter une nouvelle propriété, une exception sera levée. (Vrai, si le code n'est pas exécuté en mode strict, une exception ne sera pas levée, cependant, une tentative d'ajouter une propriété ne conduira tout simplement pas à des conséquences). Voici un exemple:
const object = { x: 1 }; Object.preventExtensions(object); object.y = 2;
La méthode
Object.seal()
agit sur les objets de la même manière que
Object.preventExtensions()
, mais elle marque également toutes les propriétés comme non configurables. Cela signifie qu'ils ne peuvent pas être supprimés et que leurs propriétés ne peuvent pas être modifiées en ce qui concerne les possibilités de listage, de définition ou de réécriture.
const object = { x: 1 }; Object.seal(object); object.y = 2;
La méthode
Object.freeze()
effectue les mêmes actions que
Object.seal()
, mais son utilisation, en outre, conduit au fait que les valeurs des propriétés existantes ne peuvent pas être modifiées. Ils sont marqués comme des propriétés dans lesquelles de nouvelles valeurs ne peuvent pas être écrites.
const object = { x: 1 }; Object.freeze(object); object.y = 2;
Prenons un exemple spécifique. Nous avons deux objets, chacun ayant une valeur unique
x
. Ensuite, nous interdisons l'extension du deuxième objet:
const a = { x: 1 }; const b = { x: 2 }; Object.preventExtensions(b);
Le traitement de ce code commence par des actions que nous connaissons déjà. A savoir, une transition est effectuée de la forme vide de l'objet vers la nouvelle forme, qui contient la propriété
'x'
(représentée comme une entité
Smi
). Lorsque nous interdisons l'expansion de l'objet
b
, cela conduit à une transition spéciale vers une nouvelle forme, qui est marquée comme non extensible. Cette transition spéciale ne conduit pas à l'apparition d'une nouvelle propriété. Ce n'est en fait qu'un marqueur.
Résultat du traitement d'un objet à l'aide de la méthode Object.preventExtensions ()Veuillez noter que nous ne pouvons pas simplement modifier le formulaire existant avec la valeur
x
, car il est nécessaire à un autre objet, à savoir l'objet
a
, qui est toujours extensible.
Problème de performances de React
Maintenant, rassemblons tout ce dont nous avons parlé et utilisons les connaissances que nous avons acquises pour comprendre l'essence du récent
problème de performances de React. Lorsque l'équipe React a dressé le profil des applications réelles, elle a remarqué une étrange dégradation des performances du V8 qui agissait sur le noyau React. Voici une reproduction simplifiée de la partie problématique du code:
const o = { x: 1, y: 2 }; Object.preventExtensions(o); oy = 0.2;
Nous avons un objet avec deux champs représentés comme des entités
Smi
. Nous empêchons la poursuite de l'expansion de l'objet, puis effectuons une action qui conduit au fait que le deuxième champ doit être représenté au format
Double
.
Nous avons déjà constaté que l'interdiction de l'expansion de l'objet conduit à peu près à la situation suivante.
Conséquences de l'interdiction de l'expansion des objetsPour représenter les deux propriétés de l'objet, des entités
Smi
sont
Smi
et la dernière transition est nécessaire afin de marquer la forme de l'objet comme non extensible.
Maintenant, nous devons changer la façon dont la propriété
y
est représentée par
Double
. Cela signifie que nous devons commencer à chercher une forme de séparation. Dans ce cas, il s'agit de la forme sous laquelle la propriété
x
apparaît. Mais maintenant, le V8 est confus. Le fait est que le formulaire de séparation était extensible et que le formulaire actuel était marqué comme non extensible. V8 ne sait pas reproduire le processus de transition dans une situation similaire. En conséquence, le moteur refuse simplement d'essayer de tout comprendre. Au lieu de cela, il crée simplement un formulaire séparé qui n'est pas connecté à l'arborescence de formulaires actuelle et n'est pas partagé avec d'autres objets. C'est quelque chose comme une forme orpheline d'un objet.
Forme orphelineIl est facile de deviner que cela, si cela se produit avec de nombreux objets, est très mauvais. Le fait est que cela rend le système entier des formes d'objets V8 inutile.
Lorsqu'un problème React récent s'est produit, les événements suivants se sont produits. Chaque objet de la classe
FiberNode
avait des champs destinés à stocker des horodatages lorsque le profilage est activé.
class FiberNode { constructor() { this.actualStartTime = 0; Object.preventExtensions(this); } } const node1 = new FiberNode(); const node2 = new FiberNode();
Ces champs (par exemple,
actualStartTime
) ont été initialisés à 0 ou -1. Cela a conduit au fait que les entités
Smi
étaient utilisées pour représenter leur signification en
Smi
. Mais plus tard, ils ont enregistré des horodatages en temps réel au format de nombres à virgule flottante renvoyés par la méthode
performance.now (). Cela a conduit au fait que ces valeurs ne pouvaient plus être représentées sous la forme de
Smi
. Pour représenter ces champs, des entités
Double
étaient désormais requises. En plus de tout cela, React a également empêché l'expansion des instances de la classe
FiberNode
.
Initialement, notre exemple simplifié pourrait être présenté sous la forme suivante.
État initial du systèmeIl existe deux instances de la classe qui partagent la même arborescence de transitions de la forme des objets. Strictement parlant, c'est à cela que le système de formes d'objets dans V8 est destiné. Mais ensuite, lorsque les horodatages en temps réel sont stockés dans l'objet, V8 ne peut pas comprendre comment il peut trouver la forme de séparation.
V8 est confusV8 attribue un nouveau formulaire orphelin à
node1
. La même chose se produit un peu plus tard avec l'objet
node2
. Par conséquent, nous avons maintenant deux formulaires «orphelins», chacun étant utilisé par un seul objet. Dans de nombreuses applications React réelles, le nombre de ces objets est bien plus de deux. Il peut s'agir de dizaines, voire de milliers d'objets de la classe
FiberNode
. Il est facile de comprendre que cette situation n’affecte pas très bien les performances du V8.
Heureusement, nous avons
corrigé ce problème dans la
V8 v7.4 , et nous
explorons la possibilité de rendre l'opération de modification de la représentation des champs d'objets moins gourmande en ressources. Cela nous permettra de résoudre les problèmes de performances restants qui surviennent dans de telles situations. V8, grâce au correctif, se comporte désormais correctement dans la situation de problème décrite ci-dessus.
État initial du systèmeVoici à quoi ça ressemble. Deux instances de la classe
FiberNode
référence à un formulaire non extensible. Dans ce cas,
'actualStartTime'
est représenté comme un champ
Smi
. Lorsque la première opération d'attribution d'une valeur à la propriété
node1.actualStartTime
est
node1.actualStartTime
, une nouvelle chaîne de transition est créée et la chaîne précédente est marquée comme obsolète.
Résultats de l'attribution d'une nouvelle valeur à la propriété Node1.actualStartTimeVeuillez noter que la transition vers le formulaire non extensible est désormais correctement reproduite dans la nouvelle chaîne. C'est dans quoi le système entre après avoir changé la valeur de
node2.actualStartTime
.
Les résultats de l'attribution d'une nouvelle valeur à la propriété node2.actualStartTimeUne fois la nouvelle valeur affectée à la propriété
node2.actualStartTime
, les deux objets font référence au nouveau formulaire et la partie obsolète de l'arborescence de transition peut être détruite par le garbage collector.
Veuillez noter que les opérations de marquage des formes d'objets comme obsolètes et leur migration peuvent sembler compliquées. En fait - c'est comme ça. Nous pensons que sur de vrais sites Web, cela fait plus de mal (en termes de performances, d'utilisation de la mémoire, de complexité) que de bien. Surtout - après, dans le cas de la
compression de
pointeurs , nous ne pouvons plus utiliser cette approche pour stocker des champs
Double
sous la forme de valeurs incorporées dans des objets. En conséquence, nous espérons
abandonner complètement le mécanisme d'obsolescence des formes d'objets V8 et rendre ce mécanisme lui-même obsolète.
Il convient de noter que l'équipe React a
résolu ce problème par elle-même, en s'assurant que les champs dans les objets de la classe
FiberNodes
initialement représentés par des valeurs doubles:
class FiberNode { constructor() {
Ici, au lieu de
Number.NaN
, toute valeur à virgule flottante qui ne correspond pas à la plage
Smi
peut être utilisée. Parmi ces valeurs figurent 0,000001,
Number.MIN_VALUE
, -0 et
Infinity
.
Il convient de noter que le problème décrit dans React était spécifique à la V8 et que lors de la création de code, les développeurs n'ont pas besoin de s'efforcer de l'optimiser en fonction d'une version spécifique d'un certain moteur JavaScript. Cependant, il est utile de pouvoir corriger quelque chose en optimisant le code dans le cas où les causes de certaines erreurs sont enracinées dans les fonctionnalités du moteur.
Il convient de rappeler que dans les entrailles des moteurs JS, il y a beaucoup de choses incroyables. Le développeur JS peut aider tous ces mécanismes, si possible sans affecter les mêmes valeurs de variables de types différents. Par exemple, vous ne devez pas initialiser les champs numériques à
null
, car cela annulera tous les avantages de l'observation de la représentation des champs et améliorera la lisibilité du code:
En d'autres termes - écrivez du code lisible, et la performance viendra d'elle-même!
Résumé
Dans cet article, nous avons examiné les questions importantes suivantes:
- JavaScript fait la distinction entre les valeurs "primitives" et "objet", et les résultats de
typeof
ne sont pas fiables. - Même les valeurs qui ont le même type JavaScript peuvent être représentées de différentes manières dans les entrailles du moteur.
- V8 essaie de trouver la meilleure façon de représenter chaque propriété de l'objet utilisé dans les programmes JS.
- Dans certaines situations, V8 effectue des opérations sur le marquage des formes d'objets comme obsolètes et effectue la migration des formes. Y compris - met en œuvre des transitions associées à l'interdiction de l'expansion des objets.
Sur la base de ce qui précède, nous pouvons fournir quelques conseils de programmation JavaScript pratiques qui peuvent aider à améliorer les performances du code:
- Initialisez toujours vos objets de la même manière. Cela contribue au travail efficace avec des formes d'objets.
- Sélectionnez de manière responsable les valeurs initiales pour les champs d'objets. Cela aidera les moteurs JavaScript à choisir comment représenter en interne ces valeurs.
Chers lecteurs! Avez-vous déjà optimisé votre code en fonction des fonctionnalités internes de certains moteurs JavaScript?
