Eine Geschichte über V8, React und einen Leistungsabfall. Teil 1

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-Datentypen

Die Art des Wertes kann mit dem Operator typeof werden, es gibt jedoch eine wichtige Ausnahme:

 typeof 42; // 'number' typeof 'foo'; // 'string' typeof Symbol('bar'); // 'symbol' typeof 42n; // 'bigint' typeof true; // 'boolean' typeof undefined; // 'undefined' typeof null; // 'object' -   ,     typeof { x: 42 }; // 'object' 

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 undefiniert

In 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 wahr

Darstellung 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; // 'number' 

Es gibt verschiedene Möglichkeiten, Ganzzahlen wie 42 im Speicher darzustellen:
Einreichung
Bits
8 Bits zusätzlich zu zwei
0010 1010
32 Bit, mit bis zu zwei
0000 0000 0000 0000 0000 0000 0010 1010
Gepackte binärcodierte Dezimalzahl (BCD)
0100 0010
32 Bit, Gleitkommazahl IEEE-754
0 100 0010 0010 1000 0000 0000 0000 0000
64 Bit, IEEE-754-Gleitkommazahl
0100 0000 0100 0101 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

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]; //      . array[42]; array[2**32-2]; //      . 

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) {  //  } for (let i = 0.1; i < 1000.1; ++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; //  -  `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.

 //  Float64   53-  . //         . 2**53 === 2**53+1; // true // Float64   ,   -1 * 0   -0,  //           . -1*0 === -0; // true // Float64   Infinity,   , //     . 1/0 === Infinity; // true -1/0 === -Infinity; // true // Float64    NaN. 0/0 === NaN; 

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 // HeapNumber -(2**30)-1 // HeapNumber  -(2**30) // Smi       -42 // Smi        -0 // HeapNumber         0 // Smi       4.2 // HeapNumber        42 // Smi   2**30-1 // Smi     2**30 // HeapNumber  Infinity // HeapNumber       NaN // HeapNumber 

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, // Smi  y: 4.2, // HeapNumber }; 

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 Werte

Angenommen, wir führen den folgenden JavaScript-Code aus:

 ox += 10; // ox   52 oy += 1; // oy   5.2 

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-Werts

HeapNumber 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; // ox   5.2 

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:

 //   `HeapNumber`. const o = { x: 0.1 }; for (let i = 0; i < 5; ++i) {  //    `HeapNumber`.  ox += 1; } 

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äten

Um 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äten

Infolgedessen 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 MutableHeapNumber

Dieser 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 MutableHeapNumber

Wenn 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 }; // ""  `x`    object.x += 1; //   `x`   

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 liegen

Fortsetzung folgt…

Liebe Leser! Haben Sie Probleme mit der JavaScript-Leistung festgestellt, die durch Funktionen der JS-Engine verursacht wurden?

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


All Articles