L'article, dont nous publions aujourd'hui la première partie de la traduction, expliquera comment le moteur JavaScript V8 sélectionne les meilleures façons de représenter diverses valeurs JS en mémoire, et comment cela affecte les mécanismes internes de V8 concernant l'utilisation de
formulaires dits
objets (forme). Tout cela nous aidera à trier l'essence du récent
problème de performances de React .

Types de données JavaScript
Chaque valeur JavaScript ne peut avoir qu'un seul des huit types de données existants:
Number
,
String
,
Symbol
,
BigInt
,
Boolean
,
Undefined
,
Null
et
Object
.
Types de données JavaScriptLe type de valeur peut être déterminé à l'aide de l'opérateur
typeof
, mais il existe une exception importante:
typeof 42;
Comme vous pouvez le voir, la commande
typeof null
renvoie
'object'
, pas
'null'
, malgré le fait que
null
ait son propre type -
Null
. Afin de comprendre la raison de ce
typeof
comportement, nous prenons en compte le fait que l'ensemble de tous les types JavaScript peut être divisé en deux groupes:
- Objets (c'est-à-dire, tapez
Object
). - Valeurs primitives (c'est-à-dire toutes les valeurs non objectives).
À la lumière de ces connaissances, il s'avère que
null
signifie «aucune valeur d'objet», tandis
undefined
signifie «aucune valeur».
Valeurs primitives, objets, nuls et non définisSuite à ces réflexions dans l'esprit de Java, Brendan Eich a conçu JavaScript pour que l'opérateur
typeof
renvoie
'object'
pour les valeurs de ces types qui se trouvent dans la figure précédente à droite. Toutes les valeurs d'objet et
null
arrivent ici. C'est pourquoi l'expression
typeof null === 'object'
est vraie, bien qu'il existe un type distinct
Null
dans la spécification du langage.
L'expression typeof v === 'object' est vraieReprésentation des valeurs
Les moteurs JavaScript doivent pouvoir représenter toutes les valeurs JavaScript en mémoire. Cependant, il est important de noter que les types de valeurs en JavaScript sont distincts de la façon dont les moteurs JS les représentent en mémoire.
Par exemple, une valeur de 42 en JavaScript est de type
number
.
typeof 42;
Il existe plusieurs façons de représenter des entiers comme 42 en mémoire:
Selon la norme ECMAScript, les nombres sont des valeurs à virgule flottante 64 bits, appelées nombres à virgule flottante double précision (Float64). Cependant, cela ne signifie pas que les moteurs JavaScript stockent toujours des nombres dans une vue Float64. Ce serait très, très inefficace! Les moteurs peuvent utiliser d'autres représentations internes des nombres - tant que le comportement des valeurs correspond exactement au comportement des nombres Float64.
Il s'est avéré que la plupart des nombres dans les applications JS réelles sont des
index de tableau ECMAScript valides. C'est-à-dire des entiers compris entre 0 et 2
32 -2.
array[0];
Les moteurs JavaScript peuvent choisir le format optimal pour représenter ces valeurs en mémoire. Ceci est fait afin d'optimiser le code qui fonctionne avec les éléments du tableau à l'aide d'index. Un processeur qui effectue des opérations d'accès à la mémoire a besoin que les indices de la matrice soient disponibles sous forme de nombres stockés dans une vue avec une
addition de deux . Si, à la place, nous représentons les index des tableaux sous la forme de valeurs Float64, cela signifierait un gaspillage de ressources système, car le moteur aurait alors besoin de convertir les nombres Float64 dans un format avec addition de deux et vice versa chaque fois que quelqu'un accède à un élément de tableau.
La représentation des nombres de 32 bits avec l'ajout de jusqu'à deux est utile non seulement pour optimiser le travail avec les tableaux. En général, on peut noter que le processeur effectue des opérations entières beaucoup plus rapidement que les opérations qui utilisent des valeurs à virgule flottante. C'est pourquoi dans l'exemple suivant, le premier cycle sans problème est deux fois plus rapide que le deuxième cycle.
for (let i = 0; i < 1000; ++i) {
Il en va de même pour les calculs utilisant des opérateurs mathématiques.
Par exemple, la performance de l'opérateur de prendre le reste de la division du fragment de code suivant dépend des nombres impliqués dans les calculs.
const remainder = value % divisor;
Si les deux opérandes sont représentés par des entiers, le processeur peut calculer le résultat très efficacement. Il existe une optimisation supplémentaire dans V8 pour les cas où l'opérande
divisor
est représenté par un nombre qui est une puissance de deux. Pour les valeurs représentées par des nombres à virgule flottante, les calculs sont beaucoup plus compliqués et prennent beaucoup plus de temps.
Étant donné que les opérations entières sont généralement effectuées beaucoup plus rapidement que les opérations sur des valeurs à virgule flottante, il peut sembler que les moteurs peuvent simplement toujours stocker tous les entiers et tous les résultats des opérations entières dans un format avec une addition de deux. Malheureusement, une telle approche violerait la spécification ECMAScript. Comme déjà mentionné, la norme prévoit la représentation des nombres au format Float64, et certaines opérations avec des entiers peuvent conduire à l'apparition de résultats sous forme de nombres à virgule flottante. Il est important que dans de telles situations, les moteurs JS produisent des résultats corrects.
Même si dans l'exemple précédent, tous les nombres sur le côté gauche des expressions sont des entiers, tous les nombres sur le côté droit des expressions sont des valeurs à virgule flottante. C'est pourquoi aucune des opérations précédentes ne peut être effectuée correctement en utilisant un format 32 bits avec un maximum de deux. Les moteurs JavaScript doivent accorder une attention particulière pour garantir que lorsque vous effectuez des opérations entières, vous obtenez les résultats Float64 corrects (quoique capables de paraître inhabituels - comme dans l'exemple précédent).
Dans le cas de petits entiers qui entrent dans la plage de la représentation à 31 bits des entiers signés, V8 utilise une représentation spéciale appelée
Smi
. Tout ce qui n'est pas une valeur
Smi
est représenté comme une valeur
HeapObject
, qui est l'adresse d'une entité en mémoire. Pour les nombres qui ne tombent pas dans la plage
Smi
, nous avons un type spécial de
HeapObject
- le soi-disant
HeapNumber
.
-Infinity
Comme vous pouvez le voir dans l'exemple précédent, certains numéros JS sont représentés comme
Smi
, et certains comme
HeapNumber
. Le moteur V8 est optimisé en termes de traitement des nombres
Smi
. Le fait est que les petits entiers sont très courants dans les vrais programmes JS. Lorsque vous travaillez avec des valeurs
Smi
, il n'est pas nécessaire d'allouer de la mémoire aux entités individuelles. De plus, leur utilisation vous permet d'effectuer des opérations rapides avec des nombres entiers.
Comparaison de Smi, HeapNumber et MutableHeapNumber
Parlons de ce à quoi ressemble la structure interne de ces mécanismes. Supposons que nous ayons l'objet suivant:
const o = { x: 42,
La valeur 42 de la propriété de l'objet
x
est codée en
Smi
. Cela signifie qu'il peut être stocké à l'intérieur de l'objet lui-même. Pour stocker la valeur 4.2, en revanche, vous devrez créer une entité distincte. Dans l'objet, il y aura un lien vers cette entité.
Stockage de diverses valeursSupposons que nous exécutons le morceau de code JavaScript suivant:
ox += 10;
Dans ce cas, la valeur de la propriété
x
peut être mise à jour à son emplacement de stockage. Le fait est que la nouvelle valeur de
x
est 52, et ce nombre se situe dans la plage de
Smi
.
La nouvelle valeur de la propriété x est stockée là où la valeur précédente a été stockée.Cependant, la nouvelle valeur de
y
, 5,2, ne rentre pas dans la gamme de
Smi
, et elle diffère en outre de la valeur précédente de y - 4,2. Par conséquent, V8 doit allouer de la mémoire à la nouvelle entité
HeapNumber
et la référencer à partir de l'objet déjà.
Nouvelle entité HeapNumber pour stocker la nouvelle valeur yHeapNumber
entités
HeapNumber
sont immuables. Cela vous permet d'implémenter certaines optimisations. Supposons que nous voulons définir la propriété de l'objet
x
valeur de la propriété
y
:
ox = oy;
Lorsque vous effectuez cette opération, nous pouvons simplement faire référence à la même entité
HeapNumber
et ne pas allouer de mémoire supplémentaire pour stocker la même valeur.
L'un des inconvénients de l'immunité des entités HeapNuber est que la mise à jour fréquente des champs avec des valeurs en dehors de la plage
Smi
est lente. Cela est démontré dans l'exemple suivant:
Lors du traitement de la première ligne, une instance de
HeapNumber
est créée, dont la valeur initiale est 0,1. Dans le corps du cycle, cette valeur passe à 1.1, 2.1, 3.1, 4.1 et enfin à 5.1. Par conséquent, au cours de l'exécution de ce code, 6 instances de
HeapNumber
, dont cinq seront soumises à des opérations de récupération de place après la fin de la boucle.
Entités HeapNumberAfin d'éviter ce problème, V8 dispose d'une optimisation, qui est un mécanisme de mise à jour des champs numériques dont les valeurs ne correspondent pas à la plage
Smi
aux mêmes endroits où ils sont déjà stockés. Si un champ numérique stocke des valeurs pour lesquelles l'entité
Smi
ne convient pas au stockage, alors V8, sous la forme d'un objet, marque ce champ comme
Double
et alloue de la mémoire à l'entité
MutableHeapNumber
, qui stocke la valeur réelle représentée au format Float64.
Utilisation des entités MutableHeapNumberPar conséquent, après la modification de la valeur du champ, V8 n'a plus besoin d'allouer de mémoire à la nouvelle entité
HeapNumber
. Au lieu de cela, écrivez simplement la nouvelle valeur dans une entité
MutableHeapNumber
existante.
Écriture d'une nouvelle valeur dans MutableHeapNumberCependant, cette approche a ses inconvénients. À savoir, puisque les valeurs de
MutableHeapNumber
peuvent changer, il est important de s'assurer que le système fonctionne de telle manière que ces valeurs se comportent comme indiqué dans la spécification du langage.
Inconvénients de MutableHeapNumberPar exemple, si vous affectez la valeur de
ox
une autre variable
y
, vous devez vous assurer que la valeur de
y
ne change pas avec un changement ultérieur de
ox
. Ce serait une violation de la spécification JavaScript! Par conséquent, lors de l'accès à
ox
, le numéro doit être reconditionné à la valeur
HeapNumber
habituelle avant d'être affecté
y
.
Dans le cas des nombres à virgule flottante, V8 effectue les opérations d'emballage ci-dessus en utilisant ses mécanismes internes. Mais dans le cas de petits entiers, l'utilisation de
MutableHeapNumber
serait une perte de temps car
Smi
est un moyen plus efficace de représenter de tels nombres.
const object = { x: 1 };
Afin d'éviter une utilisation inefficace des ressources système, tout ce que nous devons faire pour travailler avec de petits entiers est de marquer les champs correspondants dans les formes d'objets comme
Smi
. Par conséquent, les valeurs de ces champs, tant qu'elles correspondent à la plage
Smi
, peuvent être mises à jour directement à l'intérieur des objets.
Travailler avec des entiers dont les valeurs se situent dans la plage de SmiÀ suivre ...
Chers lecteurs! Avez-vous rencontré des problèmes de performances JavaScript causés par les fonctionnalités du moteur JS?
