Verwandte kostenlose Lernempfehlungen: Javascript (Video)
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:
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.
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.
Bevor Version 5.9 von V8 herauskam, verwendete die V8-Engine zwei Compiler:
Die V8-Engine verwendet auch intern mehrere Threads:
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.
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.
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:
Sobald der Aufruf „new Point(1,2)“ erfolgt, erstellt V8 eine versteckte Klasse namens „C0“.
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“.
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.
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:
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.
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.
Die beiden Objekte sind grundsätzlich gleich, die Eigenschaften „a“ und „b“ werden jedoch in einer anderen Reihenfolge erstellt.
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.
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.
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.
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!