Wie JavaScript funktioniert: Innerhalb der V8-Engine + 5 Tipps zum Schreiben von optimiertem Code

21.08.2017 · 11 min lesen

Vor einigen Wochen haben wir eine Serie gestartet, die darauf abzielt, tiefer in JavaScript einzudringen und wie es tatsächlich funktioniert: Wir dachten, dass Sie, wenn Sie die Bausteine von JavaScript kennen und wie sie zusammenspielen, besseren Code und bessere Apps schreiben können.,

Der erste Beitrag der Serie konzentrierte sich darauf, einen Überblick über die Engine, die Laufzeit und den Aufrufstapel zu geben. Dieser zweite Beitrag befasst sich mit den internen Teilen der V8-JavaScript-Engine von Google. Wir werden auch ein paar schnelle Tipps geben, wie man besseren JavaScript —Code schreibt-Best Practices, die unser Entwicklungsteam bei SessionStack beim Erstellen des Produkts befolgt.

Übersicht

Eine JavaScript-Engine ist ein Programm oder ein Interpreter, der JavaScript-Code ausführt., Eine JavaScript-Engine kann als Standardinterpreter oder Just-in-Time-Compiler implementiert werden, der JavaScript in irgendeiner Form zu Bytecode kompiliert.,/li>

  • SpiderMonkey — die erste JavaScript — Engine, die in den Tagen angetrieben Netscape Navigator, und heute treibt Firefox
  • JavaScriptCore — Open Source, vermarktet als Nitro und entwickelt von Apple für Safari
  • KJS — KDE — Engine ursprünglich entwickelt von Harri Porten für das KDE — Projekt Konqueror Web-Browser
  • Chakra (JScript9) – Internet Explorer
  • Chakra (JavaScript) – Microsoft Edge
  • Nashorn, open Source als Teil von OpenJDK, geschrieben von Oracle Java Languages und Tool Group
  • JerryScript-ist eine leichte Engine für das Internet der Dinge.,
  • Warum wurde die V8-Engine erstellt?

    Die V8-Engine, die von Google gebaut wird, ist Open Source und in C++geschrieben. Diese Engine wird in Google Chrome verwendet. Im Gegensatz zu den übrigen Motoren wird V8 jedoch auch für den beliebten Knoten verwendet.js-Laufzeitumgebung.

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

    V8 hatte vor Version 5 zwei Compiler

    .,9 von V8 kam heraus (Anfang dieses Jahres veröffentlicht), die Engine verwendete zwei Compiler:

    • full-codegen — einen einfachen und sehr schnellen Compiler, der einfachen und relativ langsamen Maschinencode erzeugte.
    • -ein komplexerer (Just-In-Time) optimierender Compiler, der hochoptimierten Code erzeugt.,l>
    • Der Hauptthread macht das, was Sie erwarten würden: Holen Sie Ihren Code, kompilieren Sie ihn und führen Sie ihn dann aus
    • Es gibt auch einen separaten Thread zum Kompilieren, damit der Hauptthread weiterhin ausgeführt werden kann, während ersteres den Code optimiert
    • Ein Profiler-Thread, der der Laufzeit mitteilt, für welche Methoden wir viel Zeit aufwenden, damit wir sie optimieren können
    • Ein paar Threads, um Garbage Collector Sweeps

    V8 nutzt beim ersten Ausführen des JavaScript-Codes full-codegen, der das analysierte JavaScript ohne Transformation direkt in Maschinencode übersetzt., Dadurch kann der Maschinencode sehr schnell ausgeführt werden. Beachten Sie, dass V8 auf diese Weise keine intermediäre Bytecode-Darstellung verwendet, sodass kein Interpreter erforderlich ist.

    Wenn Ihr Code einige Zeit ausgeführt wurde, hat der Profiler-Thread genügend Daten gesammelt, um anzugeben, welche Methode optimiert werden soll.

    Als nächstes beginnen die Optimierungen in einem anderen Thread. Es übersetzt den abstrakten JavaScript-Syntaxbaum in eine SSA-Darstellung (High-Level Static Single-Assignment) namens Hydrogen und versucht, diesen Wasserstoffgraphen zu optimieren. Die meisten Optimierungen werden auf dieser Ebene durchgeführt.,

    Inlining

    Die erste Optimierung ist inlining, so viel code wie möglich im Voraus. Inlining ist der Prozess, bei dem eine Aufrufstelle (die Codezeile, in der die Funktion aufgerufen wird) durch den Hauptteil der aufgerufenen Funktion ersetzt wird. Mit diesem einfachen Schritt können folgende Optimierungen aussagekräftiger sein.

    Versteckte Klasse

    JavaScript ist eine prototypenbasierte Sprache: Es gibt keine Klassen und Objekte erstellt mit einem Klonprozess., JavaScript ist auch eine dynamische Programmiersprache, was bedeutet, dass Eigenschaften leicht hinzugefügt oder aus einem Objekt nach seiner Instanziierung entfernt werden können.

    Die meisten JavaScript-Interpreter verwenden wörterbuchartige Strukturen (Hash-funktionsbasiert), um die Position von Objekteigenschaftswerten im Speicher zu speichern. Diese Struktur macht das Abrufen des Werts einer Eigenschaft in JavaScript rechenintensiver als in einer nicht dynamischen Programmiersprache wie Java oder C#., 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 (C# hat den dynamischen Typ, der ein anderes Thema ist). Infolgedessen können die Werte von Eigenschaften (oder Zeigern auf diese Eigenschaften) als kontinuierlicher Puffer im Speicher mit einem festen Offset zwischen jedem gespeichert werden. Die Länge eines Offsets kann leicht anhand des Eigenschaftstyps bestimmt werden, während dies in JavaScript nicht möglich ist, wo sich ein Eigenschaftstyp zur Laufzeit ändern kann.,

    Da die Verwendung von Wörterbüchern zum Auffinden der Position von Objekteigenschaften im Speicher sehr ineffizient ist, verwendet V8 stattdessen eine andere Methode: versteckte Klassen. Versteckte Klassen funktionieren ähnlich wie die festen Objektlayouts (Klassen), die in Sprachen wie Java verwendet werden, außer dass sie zur Laufzeit erstellt werden. Mal sehen, wie sie tatsächlich aussehen:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);

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

    Es wurden noch keine Eigenschaften für den Punkt definiert, daher ist „C0“ leer.

    Einmal die erste Aussage “ this.x = x “ wird ausgeführt (innerhalb der Funktion „Point“), V8 erstellt eine zweite versteckte Klasse namens „C1“, die auf „C0″basiert. „C1″ beschreibt die Position im Speicher (relativ zum Objektzeiger), an der sich die Eigenschaft x befindet., In diesem Fall wird“ x „bei Offset 0 gespeichert, was bedeutet, dass beim Betrachten eines Punktobjekts im Speicher als kontinuierlicher Puffer der erste Offset der Eigenschaft“x“ entspricht. V8 aktualisiert auch “ C0 „mit einem“ Klassenübergang“, der besagt, dass die versteckte Klasse von“ C0 „zu“ C1 „wechseln sollte, wenn einem Punktobjekt eine Eigenschaft“x“ hinzugefügt wird. Die versteckte Klasse für das Punktobjekt unten ist jetzt „C1“.,

    Jedes Mal, wenn eine neue Eigenschaft zu einem Objekt hinzugefügt wird, wird die alte versteckte Klasse mit einem Übergangspfad zur neuen versteckten Klasse aktualisiert. Versteckte Klassenübergänge sind wichtig, da sie die gemeinsame Nutzung versteckter Klassen für Objekte ermöglichen, die auf dieselbe Weise erstellt wurden., Wenn sich zwei Objekte eine versteckte Klasse teilen und zu beiden dieselbe Eigenschaft hinzugefügt wird, stellen Sie sicher, dass beide Objekte dieselbe neue versteckte Klasse und den gesamten damit verbundenen optimierten Code erhalten.

    Dieser Vorgang wird wiederholt, wenn die Aussage „das.y = y „wird ausgeführt (wieder innerhalb der Punktfunktion nach dem“ this.x = x“ – Anweisung).,

    Eine neue versteckte Klasse namens “ C2 „wird erstellt, ein Klassenübergang wird zu“ C1 „hinzugefügt, der besagt, dass, wenn eine Eigenschaft“ y „zu einem Punktobjekt hinzugefügt wird (das bereits die Eigenschaft“ x „enthält), die versteckte Klasse zu“ C2 „wechseln sollte und die versteckte Klasse des Punktobjekts auf“C2“ aktualisiert wird.

    Versteckte class übergänge sind abhängig von der Reihenfolge, in der die Eigenschaften zu einem Objekt Hinzugefügt werden., Schauen Sie sich das folgende Code-Snippet an:

    function Point(x, y) {
    this.x = x;
    this.y = y;
    }var p1 = new Point(1, 2);
    p1.a = 5;
    p1.b = 6;var p2 = new Point(3, 4);
    p2.b = 7;
    p2.a = 8;

    Jetzt würden Sie annehmen, dass für p1 und p2 dieselben versteckten Klassen und Übergänge verwendet werden. Nun, nicht wirklich. Für “ p1 „wird zuerst die Eigenschaft“ a „und dann die Eigenschaft“b“ hinzugefügt. Für “ p2 „wird jedoch zuerst“ b „zugewiesen, gefolgt von“a“. Somit erhalten „p1“ und „p2“ aufgrund der unterschiedlichen Übergangspfade unterschiedliche versteckte Klassen. In solchen Fällen ist es viel besser, dynamische Eigenschaften in derselben Reihenfolge zu initialisieren, damit die ausgeblendeten Klassen wiederverwendet werden können.,

    Inline-Caching

    V8 nutzt eine andere Technik zur Optimierung dynamisch typisierter Sprachen namens Inline-Caching. Inline-Caching beruht auf der Beobachtung, dass wiederholte Aufrufe derselben Methode für denselben Objekttyp tendenziell auftreten. Eine ausführliche Erklärung zum Inline-Caching finden Sie hier.

    Wir werden auf das allgemeine Konzept des Inline-Caching eingehen (falls Sie nicht die Zeit haben, die obige ausführliche Erklärung zu lesen).

    Wie funktioniert es also?, V8 verwaltet einen Cache des Typs von Objekten, die in den letzten Methodenaufrufen als Parameter übergeben wurden, und verwendet diese Informationen, um eine Annahme über den Typ des Objekts vorzunehmen, das in Zukunft als Parameter übergeben wird. Wenn V8 in der Lage ist, eine gute Annahme über den Objekttyp zu treffen, der an eine Methode übergeben wird, kann es den Prozess umgehen, herauszufinden, wie auf die Eigenschaften des Objekts zugegriffen werden soll, und stattdessen die gespeicherten Informationen aus früheren Lookups verwenden versteckte Klasse des Objekts.

    Wie hängen die Konzepte versteckter Klassen und Inline-Caching zusammen?, Immer wenn eine Methode für ein bestimmtes Objekt aufgerufen wird, muss die V8-Engine nach der versteckten Klasse dieses Objekts suchen, um den Offset für den Zugriff auf eine bestimmte Eigenschaft zu bestimmen. Nach zwei erfolgreichen Aufrufen derselben Methode an dieselbe versteckte Klasse lässt V8 die Suche nach versteckten Klassen aus und fügt dem Objektzeiger selbst einfach den Versatz der Eigenschaft hinzu. Bei allen zukünftigen Aufrufen dieser Methode geht die V8-Engine davon aus, dass sich die versteckte Klasse nicht geändert hat, und springt mithilfe der aus früheren Lookups gespeicherten Offsets direkt in die Speicheradresse für eine bestimmte Eigenschaft., Dies erhöht die Ausführungsgeschwindigkeit erheblich.

    Inline-Caching ist auch der Grund, warum es so wichtig ist, dass Objekte desselben Typs versteckte Klassen teilen. Wenn Sie zwei Objekte desselben Typs und mit unterschiedlichen ausgeblendeten Klassen erstellen (wie im Beispiel zuvor), können Sie kein Inline-Caching verwenden, da die entsprechenden ausgeblendeten Klassen, obwohl die beiden Objekte vom gleichen Typ sind, ihren Eigenschaften unterschiedliche Offsets zuweisen.,

    Die beiden Objekte sind grundsätzlich gleich, aber die Eigenschaften“ a „und“ b “ wurden in unterschiedlicher Reihenfolge erstellt.

    Kompilierung in Maschinencode

    Sobald der Wasserstoffgraph optimiert ist, senkt er ihn auf eine Darstellung auf niedrigerer Ebene namens Lithium. Der größte Teil der Lithiumimplementierung ist architekturspezifisch. Die Registerzuweisung erfolgt auf dieser Ebene.,

    Am Ende wird Lithium in Maschinencode kompiliert. Dann passiert etwas anderes namens OSR: On-Stack-Ersatz. Bevor wir mit dem Kompilieren und Optimieren einer offensichtlich langjährigen Methode begannen, haben wir sie wahrscheinlich ausgeführt. V8 wird nicht vergessen, was es nur langsam ausgeführt, um wieder mit der optimierten Version zu starten. Stattdessen wird der gesamte Kontext (Stapel, Register) transformiert, sodass wir mitten in der Ausführung zur optimierten Version wechseln können. Dies ist eine sehr komplexe Aufgabe, da V8 neben anderen Optimierungen den Code zunächst inlined hat., V8 ist nicht der einzige Motor, der dazu in der Lage ist.

    Es gibt Sicherheitsvorkehrungen, die als Desoptimierung bezeichnet werden, um die entgegengesetzte Transformation durchzuführen, und kehrt zum nicht optimierten Code zurück, falls eine Annahme, die die Engine gemacht hat, nicht mehr wahr ist.

    Garbage collection

    Für die Garbage Collection verwendet V8 einen traditionellen Generationsansatz von Mark-and-Sweep, um die alte Generation zu reinigen. Die Markierungsphase soll die JavaScript-Ausführung stoppen., Um die GC-Kosten zu kontrollieren und die Ausführung stabiler zu machen, verwendet V8 eine inkrementelle Markierung: Anstatt den gesamten Heap zu laufen und zu versuchen, jedes mögliche Objekt zu markieren, wird nur ein Teil des Heaps ausgeführt und die normale Ausführung fortgesetzt. Die nächste GC-Haltestelle wird dort fortgesetzt, wo der vorherige Heap-Walk aufgehört hat. Dies ermöglicht sehr kurze Pausen während der normalen Ausführung. Wie bereits erwähnt, wird die Sweep-Phase von separaten Threads behandelt.

    Ignition und TurboFan

    Mit der Veröffentlichung von V8 5.9 Anfang 2017 wurde eine neue Ausführungspipeline eingeführt., Diese neue Pipeline erzielt noch größere Leistungsverbesserungen und erhebliche Speichereinsparungen in realen JavaScript-Anwendungen.

    Die neue Ausführungspipeline basiert auf Ignition, V8 ’s Interpreter und TurboFan, V8′ s neuestem Optimierungs-Compiler.

    Den Blogbeitrag des V8-Teams zum Thema könnt ihr euch hier ansehen.

    Seit Version 5.,9 von V8 kam heraus, Full-Codegen und Kurbelwelle (die Technologien, die V8 seit 2010 gedient haben) wurden von V8 nicht mehr für die JavaScript-Ausführung verwendet, da das V8-Team Schwierigkeiten hatte, mit den neuen JavaScript-Sprachfunktionen und den für diese Funktionen erforderlichen Optimierungen Schritt zu halten.

    Dies bedeutet, dass V8 insgesamt in Zukunft eine viel einfachere und wartbarere Architektur haben wird.

    Verbesserungen auf Web-und Node.,js-benchmarks

    Diese Verbesserungen sind nur der Anfang. Die neue Ignition-und TurboFan-Pipeline ebnet den Weg für weitere Optimierungen, die die JavaScript-Leistung steigern und den Platzbedarf von V8 sowohl in Chrome als auch in Node verringern.js in den kommenden Jahren.

    Schließlich sind hier einige Tipps und Tricks, wie man gut optimiertes, besseres JavaScript schreibt., Sie können diese einfach aus dem obigen Inhalt ableiten, hier ist jedoch eine Zusammenfassung für Ihre Bequemlichkeit:

    Wie schreibe ich optimiertes JavaScript

    1. Reihenfolge der Objekteigenschaften: Instanziieren Sie Ihre Objekteigenschaften immer in der gleichen Reihenfolge, damit versteckte Klassen und anschließend optimierter Code gemeinsam genutzt werden können.
    2. Dynamische Eigenschaften: Das Hinzufügen von Eigenschaften zu einem Objekt nach der Instanziierung erzwingt eine Änderung der versteckten Klasse und verlangsamt alle Methoden, die für die vorherige versteckte Klasse optimiert wurden. Weisen Sie stattdessen alle Eigenschaften eines Objekts in seinem Konstruktor zu.,
    3. Methoden: Code, der dieselbe Methode wiederholt ausführt, wird schneller ausgeführt als Code, der viele verschiedene Methoden nur einmal ausführt (aufgrund von Inline-Caching).
    4. Arrays: Vermeiden Sie spärliche Arrays, bei denen Schlüssel keine inkrementellen Zahlen sind. Spärliche Arrays, in denen nicht jedes Element enthalten ist, sind eine Hash-Tabelle. Elemente in solchen Arrays sind teurer zuzugreifen. Versuchen Sie auch, die Vorzuweisung großer Arrays zu vermeiden. Es ist besser, zu wachsen, wie Sie gehen. Löschen Sie schließlich keine Elemente in Arrays. Es macht die Schlüssel spärlich.
    5. Tagged values: V8 repräsentiert Objekte und Zahlen mit 32 Bit., Es verwendet ein Bit, um zu wissen, ob es sich um ein Objekt (Flag = 1) oder eine Ganzzahl (Flag = 0) handelt, die aufgrund ihrer 31 Bits SMI (SMall Integer) genannt wird. Wenn ein numerischer Wert größer als 31 Bit ist, fügt V8 die Zahl ein, verwandelt sie in ein Double und erstellt ein neues Objekt, in das die Zahl eingefügt wird. Versuchen Sie, wann immer möglich vorzeichenbehaftete 31-Bit-Zahlen zu verwenden, um die teure Boxoperation in ein JS-Objekt zu vermeiden.

    Wir bei SessionStack versuchen, diese Best Practices beim Schreiben von hochoptimiertem JavaScript-Code zu befolgen., Der Grund dafür ist, dass nach der Integration von SessionStack in Ihre Produktions-Web-App alles aufgezeichnet wird: alle DOM-Änderungen, Benutzerinteraktionen, JavaScript-Ausnahmen, Stack-Traces, fehlgeschlagene Netzwerkanforderungen und Debug-Nachrichten.
    Mit SessionStack können Sie Probleme in Ihren Web-Apps als Videos wiedergeben und alles sehen, was Ihrem Benutzer passiert ist. Und all dies muss ohne Auswirkungen auf die Leistung Ihrer Web-App geschehen.
    Es gibt einen kostenlosen Plan, mit dem Sie kostenlos loslegen können.

    Share

    Schreibe einen Kommentar

    Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.