In dem Material, dessen erster Teil der Übersetzung wir heute veröffentlichen, wird erläutert, wie die V8-JavaScript-Engine die besten Möglichkeiten zur Darstellung verschiedener JS-Werte im Speicher auswählt und wie sich dies auf die internen Mechanismen von V8 beim Arbeiten mit sogenannten
Formularen auswirkt
Objekte (Form). All dies wird uns helfen, das Wesentliche des jüngsten
Problems der
React-Leistung zu klären.

JavaScript-Datentypen
Jeder JavaScript-Wert kann nur einen der acht vorhandenen Datentypen haben:
Number
,
String
,
Symbol
,
BigInt
,
Boolean
,
Undefined
,
Null
und
Object
.
JavaScript-DatentypenDie Art des Wertes kann mit dem Operator
typeof
werden, es gibt jedoch eine wichtige Ausnahme:
typeof 42;
Wie Sie sehen können, gibt der Befehl
typeof null
'object'
und nicht
'null'
, obwohl
null
einen eigenen Typ hat -
Null
. Um den Grund für diese Art von Verhalten zu verstehen, berücksichtigen wir die Tatsache, dass die Menge aller JavaScript-Typen in zwei Gruppen unterteilt werden kann:
- Objekte (d. H. Typ
Object
). - Primitive Werte (dh alle nicht objektiven Werte).
In Anbetracht dieses Wissens stellt sich heraus, dass
null
„kein Objektwert“ bedeutet, während
undefined
„kein Wert“ bedeutet.
Primitive Werte, Objekte, null und undefiniertIn Anlehnung an diese Überlegungen im Geiste von Java hat Brendan Eich JavaScript so entworfen, dass der Operator
typeof
'object'
für die Werte der Typen zurückgibt, die sich in der vorherigen Abbildung rechts befinden. Alle Objektwerte und
null
kommen hierher. Aus diesem Grund ist der Ausdruck
typeof null === 'object'
wahr, obwohl die Sprachspezifikation einen separaten Typ
Null
.
Der Ausdruckstyp von v === 'Objekt' ist wahrDarstellung von Werten
JavaScript-Engines sollten in der Lage sein, alle JavaScript-Werte im Speicher darzustellen. Es ist jedoch wichtig zu beachten, dass Werttypen in JavaScript von der Darstellung durch JS-Engines im Speicher getrennt sind.
Beispielsweise ist ein Wert von 42 in JavaScript vom Typ
number
.
typeof 42;
Es gibt verschiedene Möglichkeiten, Ganzzahlen wie 42 im Speicher darzustellen:
Nach dem ECMAScript-Standard sind Zahlen 64-Bit-Gleitkommawerte, sogenannte Gleitkommazahlen mit doppelter Genauigkeit (Float64). Dies bedeutet jedoch nicht, dass JavaScript-Engines Zahlen immer in einer Float64-Ansicht speichern. Das wäre sehr, sehr ineffizient! Engines können andere interne Darstellungen von Zahlen verwenden - sofern das Verhalten der Werte genau mit dem Verhalten der Float64-Zahlen übereinstimmt.
Wie sich herausstellte, sind die meisten Zahlen in echten JS-Anwendungen gültige ECMAScript-Array-
Indizes . Das heißt - ganze Zahlen im Bereich von 0 bis 2
32 -2.
array[0];
JavaScript-Engines können das optimale Format für die Darstellung solcher Werte im Speicher auswählen. Dies geschieht, um den Code für Array-Elemente mithilfe von Indizes zu optimieren. Ein Prozessor, der Speicherzugriffsoperationen ausführt, benötigt die Array-Indizes als Zahlen, die in einer Ansicht mit einer
Addition von zwei gespeichert sind. Wenn wir stattdessen die Indizes von Arrays in Form von Float64-Werten darstellen, würde dies eine Verschwendung von Systemressourcen bedeuten, da die Engine dann Float64-Zahlen in ein Format mit zwei Additionen konvertieren müsste und umgekehrt, wenn jemand auf ein Array-Element zugreift.
Die Darstellung von 32-Bit-Zahlen mit der Hinzufügung von bis zu zwei ist nicht nur zur Optimierung der Arbeit mit Arrays nützlich. Im Allgemeinen kann festgestellt werden, dass der Prozessor Ganzzahloperationen viel schneller ausführt als Operationen, die Gleitkommawerte verwenden. Deshalb ist im folgenden Beispiel der erste Zyklus ohne Probleme doppelt so schnell wie der zweite Zyklus.
for (let i = 0; i < 1000; ++i) {
Gleiches gilt für Berechnungen mit mathematischen Operatoren.
Beispielsweise hängt die Leistung des Operators, den Rest der Division vom nächsten Codefragment zu übernehmen, davon ab, welche Zahlen in die Berechnungen einbezogen werden.
const remainder = value % divisor;
Wenn beide Operanden durch ganze Zahlen dargestellt werden, kann der Prozessor das Ergebnis sehr effizient berechnen. In V8 gibt es eine zusätzliche Optimierung für Fälle, in denen der
divisor
Operand durch eine Zahl mit einer Zweierpotenz dargestellt wird. Für Werte, die als Gleitkommazahlen dargestellt werden, sind die Berechnungen viel komplizierter und dauern viel länger.
Da Ganzzahloperationen normalerweise viel schneller ausgeführt werden als Operationen mit Gleitkommawerten, scheint es, dass Engines einfach immer alle Ganzzahlen und alle Ergebnisse von Ganzzahloperationen in einem Format mit einer Addition von zwei speichern können. Leider würde ein solcher Ansatz die ECMAScript-Spezifikation verletzen. Wie bereits erwähnt, sieht der Standard die Darstellung von Zahlen im Float64-Format vor, und einige Operationen mit ganzen Zahlen können dazu führen, dass Ergebnisse in Form von Gleitkommazahlen angezeigt werden. Es ist wichtig, dass JS-Engines in solchen Situationen korrekte Ergebnisse liefern.
Obwohl im vorherigen Beispiel alle Zahlen auf der linken Seite der Ausdrücke Ganzzahlen sind, sind alle Zahlen auf der rechten Seite der Ausdrücke Gleitkommawerte. Aus diesem Grund kann keine der vorherigen Operationen in einem 32-Bit-Format mit einer Addition von bis zu zwei korrekt ausgeführt werden. JavaScript-Engines müssen besonders darauf achten, dass Sie bei der Ausführung von Ganzzahloperationen die richtigen Float64-Ergebnisse erhalten (obwohl sie ungewöhnlich aussehen können - wie im vorherigen Beispiel).
Bei kleinen Ganzzahlen, die in den Bereich der 31-Bit-Darstellung vorzeichenbehafteter Ganzzahlen fallen, verwendet V8 eine spezielle Darstellung namens
Smi
. Alles, was kein
Smi
Wert ist, wird als
HeapObject
Wert dargestellt.
HeapObject
ist die Adresse einer Entität im Speicher. Für Zahlen, die nicht in den
Smi
Bereich fallen, haben wir eine spezielle Art von
HeapObject
- die sogenannte
HeapNumber
.
-Infinity
Wie Sie im vorherigen Beispiel sehen können, werden einige JS-Nummern als
Smi
und andere als
HeapNumber
. Die V8-Engine ist hinsichtlich der Verarbeitung von
Smi
Nummern optimiert. Tatsache ist, dass kleine Ganzzahlen in echten JS-Programmen sehr häufig sind. Bei der Arbeit mit
Smi
Werten ist es nicht erforderlich, Speicher für einzelne Entitäten zuzuweisen. Ihre Verwendung ermöglicht es Ihnen außerdem, schnelle Operationen mit ganzen Zahlen durchzuführen.
Vergleich von Smi, HeapNumber und MutableHeapNumber
Lassen Sie uns darüber sprechen, wie die interne Struktur dieser Mechanismen aussieht. Angenommen, wir haben das folgende Objekt:
const o = { x: 42,
Der Wert 42 der Eigenschaft des Objekts
x
wird als
Smi
codiert. Dies bedeutet, dass es im Objekt selbst gespeichert werden kann. Um den Wert 4.2 zu speichern, müssen Sie dagegen eine separate Entität erstellen. Im Objekt befindet sich eine Verknüpfung zu dieser Entität.
Speicherung verschiedener WerteAngenommen, wir führen den folgenden JavaScript-Code aus:
ox += 10;
In diesem Fall kann der Wert der Eigenschaft
x
an ihrem Speicherort aktualisiert werden. Tatsache ist, dass der neue Wert von
x
52 ist und diese Zahl in den Bereich von
Smi
fällt.
Der neue Wert der Eigenschaft x wird dort gespeichert, wo der vorherige Wert gespeichert wurde.Der neue Wert von
y
, 5.2, passt jedoch nicht in den Bereich von
Smi
und unterscheidet sich außerdem vom vorherigen Wert von y - 4.2. Infolgedessen muss V8 Speicher für die neue
HeapNumber
Entität
HeapNumber
und bereits vom Objekt aus darauf verweisen.
Neue Entität HeapNumber zum Speichern des neuen y-WertsHeapNumber
Entitäten sind unveränderlich. Auf diese Weise können Sie einige Optimierungen implementieren. Angenommen, wir möchten die Eigenschaft des Objekts
x
Wert der Eigenschaft
y
:
ox = oy;
Wenn Sie diesen Vorgang ausführen, können wir einfach auf dieselbe
HeapNumber
Entität verweisen und keinen zusätzlichen Speicher zuweisen, um denselben Wert zu speichern.
Einer der Nachteile der Immunität von HeapNuber-Entitäten besteht darin, dass die häufige Aktualisierung von Feldern mit Werten außerhalb des
Smi
Bereichs langsam ist. Dies wird im folgenden Beispiel demonstriert:
Bei der Verarbeitung der ersten Zeile wird eine Instanz von
HeapNumber
erstellt, deren Anfangswert 0,1 beträgt. Im Hauptteil des Zyklus ändert sich dieser Wert zu 1.1, 2.1, 3.1, 4.1 und schließlich zu 5.1. Infolgedessen werden beim Ausführen dieses Codes 6 Instanzen von
HeapNumber
, von denen fünf nach Abschluss der Schleife
HeapNumber
unterzogen werden.
HeapNumber-EntitätenUm dieses Problem zu vermeiden, verfügt V8 über eine Optimierung, bei der numerische Felder aktualisiert werden, deren Werte nicht an denselben Stellen in den
Smi
Bereich passen, an denen sie bereits gespeichert sind. Wenn in einem numerischen Feld Werte
Smi
werden, für die die
Smi
Entität nicht zum Speichern geeignet ist, markiert V8 dieses Feld in Form eines Objekts als
Double
und
MutableHeapNumber
der
MutableHeapNumber
Entität Speicher zu, in der der im Float64-Format dargestellte reale Wert gespeichert wird.
Verwenden von MutableHeapNumber-EntitätenInfolgedessen muss V8 nach Änderung des
HeapNumber
keinen Speicher mehr für die neue
HeapNumber
Entität
HeapNumber
. Schreiben Sie stattdessen einfach den neuen Wert in eine vorhandene
MutableHeapNumber
Entität.
Schreiben eines neuen Werts in MutableHeapNumberDieser Ansatz hat jedoch seine Nachteile. Da sich die Werte von
MutableHeapNumber
ändern können, ist es wichtig sicherzustellen, dass das System so funktioniert, dass sich diese Werte wie in der Sprachspezifikation angegeben verhalten.
Nachteile von MutableHeapNumberWenn Sie beispielsweise den Wert von
ox
anderen Variablen
y
zuweisen, müssen Sie sicherstellen, dass sich der Wert von
y
bei einer nachfolgenden Änderung von
ox
nicht ändert. Das wäre ein Verstoß gegen die JavaScript-Spezifikation! Daher muss beim Zugriff auf
ox
die Nummer auf den üblichen
HeapNumber
Wert neu
HeapNumber
werden, bevor sie
y
zugewiesen wird.
Im Fall von Gleitkommazahlen führt V8 die obigen Packoperationen unter Verwendung seiner internen Mechanismen aus. Bei kleinen Ganzzahlen wäre die Verwendung von
MutableHeapNumber
jedoch Zeitverschwendung, da
Smi
eine effizientere Methode zur Darstellung solcher Zahlen darstellt.
const object = { x: 1 };
Um eine ineffiziente Nutzung der Systemressourcen zu vermeiden, müssen wir für die Arbeit mit kleinen Ganzzahlen lediglich die entsprechenden Felder in Form von Objekten als
Smi
markieren. Infolgedessen können die Werte dieser Felder direkt innerhalb der Objekte aktualisiert werden, sofern sie dem
Smi
Bereich entsprechen.
Arbeiten Sie mit ganzen Zahlen, deren Werte im Bereich von Smi liegenFortsetzung folgt…
Liebe Leser! Haben Sie Probleme mit der JavaScript-Leistung festgestellt, die durch Funktionen der JS-Engine verursacht wurden?
