Une histoire sur V8, React et une baisse des performances. 2e partie

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 partie

Obsolescence 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 }; //  `x`       `Smi` bx = 0.2; //  `bx`     `Double` y = ax; 

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 objets

Lorsque 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'objet

Pour 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-miettes

Les 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ée

Ici, 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; // TypeError: Cannot add property y; //      object is not extensible 

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; // TypeError: Cannot add property y; //      object is not extensible delete object.x; // TypeError: Cannot delete property x 

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; // TypeError: Cannot add property y; //      object is not extensible delete object.x; // TypeError: Cannot delete property x object.x = 3; // TypeError: Cannot assign to read-only property x 

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 objets

Pour 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 orpheline

Il 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ème

Il 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 confus

V8 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ème

Voici à 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.actualStartTime

Veuillez 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.actualStartTime

Une 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() {    //     `Double`   .    this.actualStartTime = Number.NaN;    //       ,  :    this.actualStartTime = 0;    Object.preventExtensions(this);  } } const node1 = new FiberNode(); const node2 = new FiberNode(); 

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:

 //   ! class Point {  x = null;  y = null; } const p = new Point(); px = 0.1; py = 402; 

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?

Source: https://habr.com/ru/post/fr467249/


All Articles