Heim  >  Artikel  >  Web-Frontend  >  Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

coldplay.xixi
coldplay.xixinach vorne
2020-12-08 17:10:562851Durchsuche

JavascriptSpalte stellt ausführliche V8-Engine und das Schreiben von optimiertem Code vor

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Verwandte kostenlose Lernempfehlungen: Javascript (Video)

Übersicht

Die JavaScript-Engine ist ein Programm oder Interpreter, der JavaScript-Code ausführt. Eine JavaScript-Engine kann als Standardinterpreter oder als eine Art Just-in-Time-Compiler implementiert werden, der JavaScript in Bytecode kompiliert.

Liste beliebter Projekte, die JavaScript-Engines implementieren:

  • V8 – Open Source, entwickelt von Google, geschrieben in C++
  • Rhino – Verwaltet von der Mozilla Foundation, Open Source, vollständig in Java entwickelt
  • SpiderMonkey – ist die erste JavaScript-Engine, die Netscape Navigator unterstützt und wird derzeit von Firefox verwendet.
  • JavaScriptCore – Open Source, verkauft als Nitro und entwickelt von Apple für Safari.
  • KJS – ist eine Engine für KDE, ursprünglich entwickelt von Harri Porten entwickelt
  • Chakra (JScript9) für den Konqueror-Webbrowser im KDE-Projekt – Internet Explorer
  • Chakra (JavaScript) – Microsoft Edge
  • Nashorn, als Teil von OpenJDK, powered by Oracle Java Sprache und Toolset geschrieben von
  • JerryScript – Eine leichte Engine für das Internet der Dinge

Warum eine V8-Engine erstellen?

Die von Google erstellte V8-Engine ist Open Source und in C++ geschrieben. Diese Engine wird in Google Chrome verwendet, im Gegensatz zu anderen Engines wird V8 jedoch auch im beliebten Node.js verwendet.
Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

V8 wurde ursprünglich entwickelt, um die Leistung der JavaScript-Ausführung in Webbrowsern zu verbessern. Um die Geschwindigkeit zu erhöhen, wandelt V8 JavaScript-Code in effizienteren Maschinencode um, anstatt einen Interpreter zu verwenden. Es kompiliert JavaScript-Code in Maschinencode zur Ausführungszeit, indem es einen JIT-Compiler (Just-In-Time) implementiert, genau wie viele moderne JavaScript-Engines wie SpiderMonkey oder Rhino (Mozilla). Der Hauptunterschied besteht darin, dass V8 keinen Bytecode oder Zwischencode generiert.

V8 hatte früher zwei Compiler

Bevor Version 5.9 von V8 herauskam, verwendete die V8-Engine zwei Compiler:

  • full-codegen – ein einfacher und sehr schneller Compiler, der einfachen und relativ langsamen Maschinencode erzeugt.
  • Crankshaft – Ein ausgefeilterer (Just-In-Time) optimierender Compiler, der hochoptimierten Code generiert.

Die V8-Engine verwendet auch intern mehrere Threads:

  • Der Hauptthread macht das, was Sie erwarten: ruft den Code ab, kompiliert ihn und führt ihn aus
  • Es gibt auch einen separaten Thread für die Kompilierung, sodass der Hauptthread dies tun kann Ersteres optimiert den Code, während die Ausführung fortgesetzt wird.
  • Ein Profiler-Thread, der der Laufzeit mitteilt, dass wir viel Zeit aufgewendet haben, damit Crankshaft sie optimieren kann.
  • Einige Threads verarbeiten den Garbage Collector.

Wenn JavaScript-Code zum ersten Mal ausgeführt wird, nutzt V8 das volle Potenzial -codegen-Compiler, um geparstes JavaScript ohne Konvertierung direkt in Maschinencode zu übersetzen. Dadurch kann es sehr schnell mit der Ausführung von Maschinencode beginnen. Beachten Sie, dass V8 keinen Zwischenbytecode verwendet, sodass kein Interpreter erforderlich ist.

Wenn der Code eine Weile ausgeführt wurde, hat der Analysethread genügend Daten gesammelt, um zu bestimmen, welche Methode optimiert werden sollte.

Als nächstes startet Crankshaft die Optimierung in einem anderen Thread. Es konvertiert abstrakte JavaScript-Syntaxbäume in eine SSA-Darstellung (Static Single Allocation) auf hoher Ebene namens Hydrogen und versucht, den Hydrogen-Graphen zu optimieren. Die meisten Optimierungen werden auf dieser Ebene durchgeführt.

Inline-Code

Die erste Optimierung besteht darin, so viel Code wie möglich im Voraus zu inline. Beim Inlining wird die Aufrufstelle (die Codezeile, die die Funktion aufruft) durch den Hauptteil der aufgerufenen Funktion ersetzt. Dieser einfache Schritt macht die folgenden Optimierungen sinnvoller.

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Versteckte Klassen

JavaScript ist eine prototypbasierte Sprache: Klassen und Objekte werden nicht durch einen Klonprozess erstellt. JavaScript ist außerdem eine dynamische Programmiersprache, was bedeutet, dass Eigenschaften nach der Instanziierung einfach einem Objekt hinzugefügt oder daraus entfernt werden können.

Die meisten JavaScript-Interpreter verwenden eine wörterbuchähnliche Struktur (basierend auf einer Hash-Funktion), um die Position von Objekteigenschaftswerten im Speicher zu speichern. Diese Struktur macht das Abrufen von Eigenschaftswerten in JavaScript schneller als bei nicht dynamischer Programmierung wie z Bei Java oder C# sind die Rechenkosten höher.

In Java werden alle Objekteigenschaften vor der Kompilierung durch ein festes Objektlayout bestimmt und können zur Laufzeit nicht dynamisch hinzugefügt oder entfernt werden (natürlich verfügt C# über dynamische Typisierung, was ein anderes Thema ist).

Daher können Eigenschaftswerte (oder Zeiger auf diese Eigenschaften) als zusammenhängende Puffer mit festen Versätzen zwischen den einzelnen Puffern gespeichert werden. Die Länge des Versatzes kann zur Laufzeit leicht anhand des Eigenschaftstyps bestimmt werden Es ist möglich, den Eigenschaftstyp in JavaScript zu ändern, was nicht möglich ist.

Da die Verwendung eines Wörterbuchs zum Ermitteln der Position der Eigenschaften eines Objekts im Speicher sehr ineffizient ist, verwendet V8 einen anderen Ansatz: versteckte Klassen. Versteckte Klassen funktionieren ähnlich wie feste Objekte (Klassen), die in Sprachen wie Java verwendet werden, mit dem Unterschied, dass sie zur Laufzeit erstellt werden. Schauen wir uns nun ihr praktisches Beispiel an:

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Sobald der Aufruf „new Point(1,2)“ erfolgt, erstellt V8 eine versteckte Klasse namens „C0“.

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Für Point wurden noch keine Eigenschaften definiert, daher ist „C0“ leer.

Sobald die erste Anweisung „this.x = x“ ausgeführt wird (innerhalb der Funktion „Point“), erstellt V8 eine zweite versteckte Klasse namens „C1“, die auf „C0“ basiert. „C1“ beschreibt den Ort im Speicher (relativ zum Objektzeiger), an dem die Eigenschaft x gefunden werden kann.

In diesem Fall wird „x“ bei Offset 0 gespeichert, was bedeutet, dass bei Betrachtung des Punktobjekts im Speicher als zusammenhängender Puffer der erste Offset dem Attribut „x“ entspricht. V8 wird außerdem „C0“ mit einer „Klassentransformation“ aktualisieren, die besagt, dass, wenn das Attribut „x“ zum Punktobjekt hinzugefügt wird, die ausgeblendete Klasse von „C0“ zu „C1“ wechseln sollte. Die versteckte Klasse des Punktobjekts unten ist jetzt „C1“.

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Jedes Mal, wenn einem Objekt eine neue Eigenschaft hinzugefügt wird, wird die alte ausgeblendete Klasse mit einem Transformationspfad aktualisiert, der auf die neue ausgeblendete Klasse verweist. Umwandlungen versteckter Klassen sind wichtig, da sie die gemeinsame Nutzung versteckter Klassen zwischen Objekten ermöglichen, die auf die gleiche Weise erstellt wurden. Wenn zwei Objekte eine versteckte Klasse teilen und ihnen dieselbe Eigenschaft hinzugefügt wird, stellt die Transformation sicher, dass beide Objekte dieselbe neue versteckte Klasse und den gesamten damit verbundenen Optimierungscode erhalten.

Der gleiche Vorgang wird wiederholt, wenn die Anweisung „this.y = y“ ausgeführt wird (innerhalb der Funktion „Point“, nach der Anweisung „this.x = x“).

Eine neue versteckte Klasse mit dem Namen „C2“ wird erstellt. Wenn einem Point-Objekt (das bereits das Attribut „x“ enthält) ein Attribut „y“ hinzugefügt wird, wird eine Klassentransformation zu „C1“ hinzugefügt Die verborgene Klasse sollte in „C2“ geändert werden und die verborgene Klasse des Punktobjekts wird auf „C2“ aktualisiert.

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Die Konvertierung versteckter Klassen hängt von der Reihenfolge ab, in der Eigenschaften zum Objekt hinzugefügt werden. Schauen Sie sich den folgenden Codeausschnitt an:

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Nehmen Sie nun an, dass für p1 und p2 dieselbe versteckte Klasse und Transformation verwendet wird. Fügen Sie also für „p1“ zuerst das Attribut „a“ und dann das Attribut „b“ hinzu. Allerdings wird „p2“ zuerst „b“ und dann „a“ zugewiesen. Daher haben „p1“ und „p2“ aufgrund unterschiedlicher Transformationspfade unterschiedliche versteckte Kategorien. In diesem Fall ist es viel besser, die dynamischen Eigenschaften in derselben Reihenfolge zu initialisieren, damit die ausgeblendete Klasse wiederverwendet werden kann.

Inline-Caching

V8 verwendet eine andere Technik zur Optimierung dynamisch typisierter Sprachen, die als Inline-Caching bezeichnet wird. Inline-Caching basiert auf der Beobachtung, dass wiederholte Aufrufe derselben Methode tendenziell bei Objekten desselben Typs auftreten. Eine ausführliche Erklärung zum Inline-Caching finden Sie hier.

Das allgemeine Konzept des Inline-Caching wird als nächstes besprochen (falls Sie keine Zeit haben, das ausführliche Tutorial oben durchzugehen).

Wie funktioniert es also? V8 verwaltet einen Cache mit Objekttypen, die in den letzten Methodenaufrufen als Argumente übergeben wurden, und verwendet diese Informationen, um künftig als Argumente übergebene Objekttypen vorherzusagen. Wenn V8 den Typ des an eine Methode übergebenen Objekts gut genug vorhersagen kann, kann es den Prozess des Zugriffs auf die Eigenschaften des Objekts umgehen und stattdessen gespeicherte Informationen aus früheren Suchvorgängen für die verborgene Klasse des Objekts verwenden.

Wie hängen also die Konzepte versteckter Klassen und Inline-Caching zusammen? Immer wenn eine Methode für ein bestimmtes Objekt aufgerufen wird, muss die V8-Engine eine Suche nach der verborgenen Klasse dieses Objekts durchführen, um den Offset zu bestimmen, bei dem auf die bestimmte Eigenschaft zugegriffen werden soll. Nach zwei erfolgreichen Aufrufen derselben versteckten Klasse unterlässt V8 die Suche nach der versteckten Klasse und fügt einfach den Offset der Eigenschaft zum Objektzeiger selbst hinzu. Bei allen nächsten Aufrufen dieser Methode geht die V8-Engine davon aus, dass sich die ausgeblendete Klasse nicht geändert hat, und springt unter Verwendung des bei der vorherigen Suche gespeicherten Offsets direkt zur Speicheradresse der spezifischen Eigenschaft. Dadurch wird die Ausführungsgeschwindigkeit erheblich verbessert.

Inline-Caching ist auch der Grund, warum es wichtig ist, dass Objekte desselben Typs versteckte Klassen gemeinsam nutzen. Wenn Sie zwei Objekte desselben Typs und unterschiedlicher versteckter Klassen erstellen (wie wir es in unserem vorherigen Beispiel getan haben), kann V8 kein Inline-Caching verwenden, da die beiden Objekte zwar vom gleichen Typ sind, ihre entsprechenden versteckten Klassen jedoch Itas sind Eigenschaften werden unterschiedliche Offsets zugewiesen.

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Die beiden Objekte sind grundsätzlich gleich, die Eigenschaften „a“ und „b“ werden jedoch in einer anderen Reihenfolge erstellt.

In Maschinencode kompilieren

Sobald das Wasserstoffdiagramm optimiert ist, reduziert Crankshaft es auf eine niedrigere Darstellungsebene namens Lithium. Die meisten Lithium-Implementierungen sind architekturspezifisch. Auf dieser Ebene erfolgt häufig die Registerzuordnung.

Schließlich wird Lithium in Maschinencode kompiliert. Dann gibt es noch OSR: On-Stack-Ersatz. Bevor wir mit dem Kompilieren und Optimieren einer expliziten Methode mit langer Laufzeit beginnen, führen wir möglicherweise eine Stapelersetzung durch. V8 führt nicht nur langsam den Stapelaustausch durch und beginnt erneut mit der Optimierung. Stattdessen konvertiert es den gesamten Kontext, den wir haben (Stack, Register), um während der Ausführung zur optimierten Version zu wechseln. Dies ist eine sehr komplexe Aufgabe, wenn man bedenkt, dass V8 neben anderen Optimierungen zunächst den Code einbettet. Der V8 ist nicht der einzige Motor, der das kann.

Es gibt eine Sicherheitsmaßnahme namens Deoptimierung, die die gegenteilige Konvertierung durchführt und nicht optimierten Code zurückgibt, vorausgesetzt, die Engine ist ungültig.

Garbage Collection

Für die Garbage Collection verwendet V8 den traditionellen Mark-and-Sweep-Algorithmus, um die alte Generation zu bereinigen. Die Markierungsphase sollte die Ausführung von JavaScript stoppen. Um die GC-Kosten zu kontrollieren und die Ausführung stabiler zu machen, verwendet V8 inkrementelle Markierung: Anstatt den gesamten Heap zu durchlaufen und zu versuchen, jedes mögliche Objekt zu markieren, durchläuft es nur einen Teil des Heaps und nimmt dann die normale Ausführung wieder auf. Der nächste GC-Stopp wird dort fortgesetzt, wo der vorherige Heap-Walk aufgehört hat, was eine sehr kurze Pause während der normalen Ausführung ermöglicht, da die Scan-Phase, wie bereits erwähnt, von einem separaten Thread verarbeitet wird.

So schreiben Sie optimiertes JavaScript

  1. Reihenfolge der Objekteigenschaften: Instanziieren Sie Objekteigenschaften immer in derselben Reihenfolge, damit versteckte Klassen und anschließend optimierter Code gemeinsam genutzt werden können.
  2. Dynamische Eigenschaften: Da das Hinzufügen von Eigenschaften zu einem Objekt nach der Instanziierung eine Änderung einer versteckten Klasse erzwingt und die Ausführung aller zuvor von der versteckten Klasse optimierten Methoden verlangsamt, weisen Sie alle Eigenschaften des Objekts in seinem Konstruktor zu.
  3. Methoden: Code, der dieselbe Methode wiederholt ausführt, wird schneller ausgeführt als Code, der mehrere verschiedene Methoden nur einmal ausführt (aufgrund des Inline-Caching).
  4. Arrays: Vermeiden Sie Arrays mit geringer Dichte, bei denen die Schlüsselwerte keine automatisch inkrementierenden Zahlen sind, und Arrays mit geringer Dichte, die nicht alle Elemente speichern, sind Hash-Tabellen. Der Zugriff auf Elemente in solchen Arrays ist teuer. Versuchen Sie außerdem, die Vorabzuweisung großer Arrays zu vermeiden. Besser nach Bedarf wachsen. Löschen Sie schließlich keine Elemente aus dem Array, da dies die Schlüssel spärlich macht.
  5. Tag-Wert: V8 verwendet 32 ​​Bit zur Darstellung von Objekten und Werten. Da der Wert 31 Bit beträgt, wird anhand eines Bits unterschieden, ob es sich um ein Objekt (Flag = 1) oder eine Ganzzahl namens SMI (SMall Integer) (Flag = 0) handelt. Wenn dann eine Zahl mehr als 31 Ziffern hat, umrahmt V8 die Zahl, wandelt sie in ein Double um und erstellt ein neues Objekt, das die Zahl enthält. Verwenden Sie nach Möglichkeit vorzeichenbehaftete 31-Bit-Zahlen, um teure Boxing-Operationen für JS-Objekte zu vermeiden.

Zündung und TurboFan

Mit der Veröffentlichung von V8 5.9 Anfang 2017 wurde eine neue Ausführungspipeline eingeführt. Diese neue Pipeline ermöglicht größere Leistungssteigerungen und erhebliche Speichereinsparungen in echten JavaScript-Anwendungen.

Der neue Ausführungsablauf basiert auf Ignition (V8s Interpreter) und TurboFan (V8s neuestem Optimierungscompiler).

Da das V8-Team seit der Veröffentlichung von V8 5.9 Schwierigkeiten hatte, mit den neuen JavaScript-Sprachfunktionen und den für diese Funktionen erforderlichen Optimierungen Schritt zu halten, hat das V8-Team nicht mehr Full-Codegen und Crankshaft verwendet (die seitdem die V8-Technologie bedienen). 2010).

Das bedeutet, dass V8 insgesamt eine einfachere und wartbarere Architektur haben wird.

Verstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code

Diese Verbesserungen sind erst der Anfang. Die neuen Ignition- und TurboFan-Pipelines ebnen den Weg für weitere Optimierungen, die die JavaScript-Leistung verbessern und den Platzbedarf von V8 in Chrome und Node.js für die kommenden Jahre verringern werden.

Verwandte kostenlose Lernempfehlungen: php-Programmierung (Video)

Das obige ist der detaillierte Inhalt vonVerstehen Sie, wie JavaScript funktioniert, tauchen Sie ein in die V8-Engine und schreiben Sie optimierten Code. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen