Detaillierte Erläuterung des Ausführungsprozesses der JavaScript-Engine V8

Detaillierte Erläuterung des Ausführungsprozesses der JavaScript-Engine V8

1. V8-Quelle

Der Name V8 leitet sich vom „V-förmigen 8-Zylinder-Motor“ (V8-Motor) des Autos ab. Der V8-Motor wurde hauptsächlich in den USA entwickelt und ist weithin für seine hohe Leistung bekannt. Mit der V8-Engine möchte Google seinen Benutzern zeigen, dass es sich um eine leistungsstarke und schnelle JavaScript-Engine handelt.

Vor der Einführung von V8 war die JavaScriptCore-Engine die erste Mainstream-JavaScript-Engine. JavaScriptCore dient hauptsächlich dem von Apple entwickelten und als Open Source bereitgestellten Webkit-Browserkernel. Es wird gesagt, dass Google mit der Entwicklungsgeschwindigkeit und Ausführungsgeschwindigkeit von JavaScriptCore und Webkit nicht zufrieden war und daher mit der Entwicklung einer neuen JavaScript-Engine und einer neuen Browser-Kernel-Engine begann. So entstanden die beiden Haupt-Engines V8 und Chromium, die mittlerweile zur beliebtesten Browser-bezogenen Software geworden sind.

2. V8-Serviceziel

V8 wurde auf Basis von Chrome entwickelt, ist jedoch nicht auf den Browserkernel beschränkt. V8 wurde bisher in vielen Szenarien eingesetzt, beispielsweise in den beliebten Node.js, Weex, Quick Applications und frühen RN.

3. Frühe Architektur von V8

Der V8-Motor wurde mit der Mission geboren, Geschwindigkeit und Speicherrecycling zu revolutionieren. Die Architektur von JavaScriptCore besteht darin, Bytecode zu generieren und dann den Bytecode auszuführen. Google ist der Ansicht, dass die JavaScriptCore-Architektur nicht umsetzbar ist und dass die Generierung von Bytecode zeitaufwändig wäre und nicht so schnell wäre wie die direkte Generierung von Maschinencode. Daher war V8 in seinem frühen Architekturdesign sehr radikal und übernahm die Methode der direkten Kompilierung in Maschinencode. Die Praxis zeigte später, dass die Architektur von Google zwar die Geschwindigkeit verbesserte, jedoch auch Probleme mit dem Speicherverbrauch verursachte. Sie können sich das frühe Flussdiagramm von V8 ansehen:

Frühe V8 hatten zwei Compiler: Full-Codegen und Crankshaft. V8 kompiliert zunächst alle Codes einmal mit Full-Codegen, um den entsprechenden Maschinencode zu erzeugen. Während der Ausführung von JS wählt der integrierte Profiler von V8 Hot-Funktionen aus, zeichnet den Feedback-Typ der Parameter auf und übergibt sie dann zur Optimierung an Crankshaft. Daher generiert Full-Codegen im Wesentlichen nicht optimierten Maschinencode, während Crankshaft optimierten Maschinencode generiert.

IV. Mängel der frühen V8-Architektur

Mit der Einführung neuer Versionen und der zunehmenden Komplexität der Webseiten wurden die Architekturmängel von V8 nach und nach deutlich:

  • Durch die Full-Codegen-Kompilierung wird direkt Maschinencode generiert, was zu einem hohen Speicherverbrauch führt
  • Bei der Full-Codegen-Kompilierung wird direkt Maschinencode generiert, was zu langen Kompilierungszeit und langsamer Startgeschwindigkeit führt
  • Crankshaft kann keine Codeblöcke optimieren, die durch Schlüsselwörter wie try, catch und finally abgegrenzt sind.
  • Crankshaft fügt neue Syntaxunterstützung hinzu, die das Schreiben von Code erfordert, um ihn an verschiedene CPU-Architekturen anzupassen

5. Die aktuelle Architektur des V8

Um die oben genannten Mängel zu beheben, verwendet V8 die JavaScriptCore-Architektur zur Generierung von Bytecode. Fühlt es sich an, als hätte sich für Google hier der Kreis geschlossen? V8 verwendet die Methode zum Generieren von Bytecode. Der Gesamtprozess ist wie folgt:

Ignition ist ein Interpreter für V8 und die ursprüngliche Motivation dahinter bestand darin, den Speicherverbrauch auf Mobilgeräten zu reduzieren. Vor Ignition belegte der vom Full-Codegen-Baseline-Compiler von V8 generierte Code normalerweise fast ein Drittel des gesamten JavaScript-Heaps von Chrome. Dadurch bleibt weniger Platz für die eigentlichen Daten Ihrer Webanwendung.

Der Bytecode von Ignition kann direkt mit TurboFan verwendet werden, um optimierten Maschinencode zu generieren, ohne dass er wie bei Crankshaft aus dem Quellcode neu kompiliert werden muss. Der Bytecode von Ignition bietet ein saubereres und weniger fehleranfälliges Basisausführungsmodell in V8 und vereinfacht den Deoptimierungsmechanismus, der ein Hauptmerkmal der adaptiven Optimierungen von V8 ist. Und schließlich verbessert die Aktivierung von Ignition im Allgemeinen die Startzeiten von Skripts und damit auch die Ladezeiten von Webseiten, da die Generierung von Bytecode schneller ist als die Generierung des kompilierten Basiscodes von Full-CodeGen.

TurboFan ist ein optimierender Compiler für V8. Das TurboFan-Projekt wurde ursprünglich Ende 2013 gestartet, um die Mängel von Crankshaft zu beheben. Crankshaft kann nur eine Teilmenge der JavaScript-Sprache optimieren. Es ist beispielsweise nicht dafür ausgelegt, JavaScript-Code durch strukturierte Ausnahmebehandlung zu optimieren, d. h. durch Codeblöcke, die durch die JavaScript-Schlüsselwörter „try“, „catch“ und „finally“ getrennt sind. Es ist schwierig, Unterstützung für neue Sprachfunktionen in Crankshaft hinzuzufügen, da für diese Funktionen fast immer das Schreiben von architekturspezifischem Code für die neun unterstützten Plattformen erforderlich ist.

Vorteile der Einführung der neuen Architektur

Der Speichervergleich von V8 unter verschiedenen Architekturen ist in der Abbildung dargestellt:

Fazit: Es ist deutlich zu erkennen, dass der Speicherverbrauch der Ignition+TurboFan-Architektur im Vergleich zur Full-Codegen+Crankshaft-Architektur um mehr als die Hälfte reduziert ist.

Der Vergleich der Geschwindigkeitsverbesserungen von Webseiten unter verschiedenen Architekturen wird in der Abbildung dargestellt:

Fazit: Es ist deutlich zu erkennen, dass die Ignition+TurboFan-Architektur die Webseitengeschwindigkeit im Vergleich zur Full-codegen+Crankshaft-Architektur um 70 % verbessert.

Als nächstes erläutern wir kurz jeden Prozess der vorhandenen Architektur:

6. Lexikalische Analyse und Syntaxanalyse von V8

Studenten, die Compilertheorie studiert haben, wissen, dass eine JS-Datei nur ein Quellcode ist, der nicht von einer Maschine ausgeführt werden kann. Die lexikalische Analyse besteht darin, die Quellcodezeichenfolge aufzuteilen und eine Reihe von Token zu generieren. Wie in der folgenden Abbildung gezeigt, entsprechen unterschiedliche Zeichenfolgen unterschiedlichen Tokentypen.

Nach der lexikalischen Analyse folgt als nächster Schritt die grammatikalische Analyse. Die Eingabe der Grammatikanalyse ist die Ausgabe der lexikalischen Analyse und die Ausgabe ist der abstrakte AST-Syntaxbaum. Wenn im Programm ein Syntaxfehler auftritt, löst V8 während der Syntaxanalysephase eine Ausnahme aus.

7. V8 AST Abstrakter Syntaxbaum

Die folgende Abbildung zeigt eine abstrakte Syntaxbaum-Datenstruktur der Add-Funktion

Nach der V8-Analysephase besteht der nächste Schritt darin, Bytecode basierend auf dem abstrakten Syntaxbaum zu generieren. Wie in der folgenden Abbildung gezeigt, generiert die Add-Funktion den entsprechenden Bytecode:

Die Funktion der BytecodeGenerator-Klasse besteht darin, den entsprechenden Bytecode gemäß dem abstrakten Syntaxbaum zu generieren. Verschiedene Knoten entsprechen einer Bytecode-Generierungsfunktion, und die Funktion beginnt mit Visit****. Der dem +-Zeichen entsprechende Funktionsbytecode wird wie folgt generiert:

void BytecodeGenerator::VisitArithmeticExpression(BinaryOperation* Ausdruck) {
  FeedbackSlot-Slot = feedback_spec()->AddBinaryOpICSlot();
  Ausdruck* Unterausdruck;
  Smi* wörtlich;
  
  wenn (Ausdruck->IsSmiLiteralOperation(&Unterausdruck, &Literal)) {
    BesuchFürAkkumulatorwert(Unterausdruck);
    builder()->SetExpressionPosition(Ausdruck);
    builder()->BinaryOperationSmiLiteral(Ausdruck->op(), Literal,
                                         feedback_index(Steckplatz));
  } anders {
    Registrieren Sie sich links = VisitForRegisterValue(expr->left());
    BesuchFürAkkumulatorwert(Ausdruck->rechts());
    builder()->SetExpressionPosition(expr); // Quellcodeposition zum Debuggen speichern builder()->BinaryOperation(expr->op(), lhs, feedback_index(slot)); // Add-Bytecode generieren }
}

Aus dem Obigen können wir erkennen, dass ein Quellcode-Speicherortdatensatz vorhanden ist. Die folgende Abbildung zeigt die Entsprechung zwischen den Quellcode- und Bytecode-Speicherorten:

Bytecode generieren, wie wird der Bytecode ausgeführt? Lassen Sie uns als Nächstes Folgendes erklären:

8. Bytecode

Lassen Sie uns zunächst über den V8-Bytecode sprechen:

Jeder Bytecode gibt seine Eingabe und Ausgabe als Registeroperanden an

Ignition verwendet die Register r0, r1, r2... und das Akkumulatorregister

Register: Funktionsparameter und lokale Variablen werden in für den Benutzer sichtbaren Registern gespeichert

Akkumulator: Ein für den Benutzer nicht sichtbares Register zum Speichern von Zwischenergebnissen

Der ADD-Bytecode wird unten angezeigt:

Bytecode-Ausführung

Die folgende Reihe von Abbildungen zeigt die Änderungen in den entsprechenden Registern und Akkumulatoren, wenn jeder Bytecode ausgeführt wird. Die Add-Funktion übergibt die Parameter 10 und 20, und das vom Akkumulator zurückgegebene Endergebnis ist 50.

Jeder Bytecode entspricht einer Verarbeitungsfunktion, und die Adresse des Bytecode-Handlers wird in der dispatch_table_ gespeichert. Wenn der Bytecode ausgeführt wird, wird der entsprechende Bytecode-Handler zur Ausführung aufgerufen. Das Mitglied der Interpreter-Klasse dispatch_table_ speichert die Handleradresse für jeden Bytecode.

Beispielsweise lautet die dem ADD-Bytecode entsprechende Verarbeitungsfunktion (wenn der ADD-Bytecode ausgeführt wird, wird die Klasse InterpreterBinaryOpAssembler aufgerufen):

IGNITION_HANDLER(Hinzufügen, InterpreterBinaryOpAssembler) {
   BinaryOpWithFeedback(&BinaryOpAssembler::Generate_AddWithFeedback);
}
  
void BinaryOpWithFeedback(BinaryOpGenerator generator) {
    Knoten* reg_index = BytecodeOperandReg(0);
    Knoten* lhs = LoadRegister(reg_index);
    Knoten* rhs = GetAccumulator();
    Knoten* Kontext = GetContext();
    Knoten* Slot-Index = BytecodeOperandIdx(1);
    Knoten* Feedback-Vektor = LoadFeedbackVector();
    BinaryOpAssembler binop_asm(zustand());
    Knoten*Ergebnis = (binop_asm.*Generator)(Kontext, links, rechts, Slot-Index,                            
feedback_vector, false);
    SetAccumulator(Ergebnis); // Setze das Ergebnis der ADD-Berechnung in den Akkumulator Dispatch(); // Verarbeite den nächsten Bytecode }

Tatsächlich wurde der JS-Code an dieser Stelle ausgeführt. Wenn während des Ausführungsprozesses eine Hot-Funktion gefunden wird, aktiviert V8 Turbofan zur optimierten Kompilierung und generiert direkt Maschinencode. Im Folgenden wird der Turbofan-Optimierungscompiler erläutert:

9. Turbofan

Turbofan generiert optimierten Maschinencode basierend auf Bytecode und Hot Function Feedback-Typen. Viele der Optimierungsprozesse von Turbofan entsprechen im Wesentlichen den Prinzipien der Backend-Optimierung bei der Kompilierung und verwenden Sea-of-Node.

Funktionsoptimierung hinzufügen:

Funktion add(x, y) {
  gib x+y zurück;
}
hinzufügen (1, 2);
%OptimizeFunctionOnNextCall(Hinzufügen);
hinzufügen (1, 2);

V8 verfügt über eine Funktion, die direkt aufgerufen werden kann, um anzugeben, welche Funktion optimiert werden soll. Führen Sie %OptimizeFunctionOnNextCall aus, um Turbofan aktiv aufzurufen und die Add-Funktion zu optimieren. Die Add-Funktion wird basierend auf dem Parameter-Feedback des letzten Aufrufs optimiert. Offensichtlich ist das Feedback dieses Mal eine Ganzzahl, sodass Turbofan basierend auf dem Parameter, der eine Ganzzahl ist, optimiert und direkt Maschinencode generiert. Der nächste Funktionsaufruf ruft direkt den optimierten Maschinencode auf. (Beachten Sie, dass zum Ausführen von V8 die Syntax --allow-natives-syntax erforderlich ist. OptimizeFunctionOnNextCall ist eine integrierte Funktion. Nur mit der Syntax --allow-natives-syntax kann JS die integrierte Funktion aufrufen, andernfalls meldet die Ausführung einen Fehler.)

Der entsprechende von der Add-Funktion von JS generierte Maschinencode lautet wie folgt:

Dies beinhaltet das Konzept kleiner Ganzzahlen. Sie können diesen Artikel lesen: https://zhuanlan.zhihu.com/p/82854566

Wenn Sie den Eingabeparameter der Add-Funktion in ein Zeichen ändern

Funktion add(x, y) {
  gib x+y zurück;
}
hinzufügen (1, 2);
%OptimizeFunctionOnNextCall(Hinzufügen);
hinzufügen (1, 2);

Der entsprechende Maschinencode, der von der optimierten Additionsfunktion generiert wird, lautet wie folgt:

Vergleicht man die beiden obigen Abbildungen, übergibt die Add-Funktion unterschiedliche Parameter und generiert nach der Optimierung unterschiedliche Maschinencodes.

Wenn eine Ganzzahl übergeben wird, ruft sie im Wesentlichen direkt die Assembleranweisung add auf.

Wenn eine Zeichenfolge übergeben wird, ruft sie im Wesentlichen die integrierte Add-Funktion von V8 auf

An diesem Punkt endet der gesamte Ausführungsprozess von V8.

Oben finden Sie eine ausführliche Erläuterung des Ausführungsprozesses der JavaScript-Engine V8. Weitere Informationen zur JavaScript-Engine V8 finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • JavaScript Tauchen Sie ein in die V8-Engine und 5 Tipps zum Schreiben von optimiertem Code
  • Ein Leitfaden für Anfänger zum Erlernen der Funktionsweise von JavaScript-Engines
  • Detaillierte Erläuterung der Prinzipien und der Verwendung der JavaScript-Vorlagen-Engine
  • Detailliertes Beispiel für das Implementierungsprinzip der JavaScript-Vorlagen-Engine
  • Detaillierte Erklärung des Arbeitsmechanismus der Javascript-Engine
  • Detaillierte Erklärung der Js-Template-Engine (TrimPath)
  • Anwendungsbeispiele für die JavaScript-Vorlagen-Engine
  • Detaillierte Erläuterung des Implementierungsprinzips der leistungsstarken JavaScript-Vorlagen-Engine

<<:  So fügen Sie eine Nginx-Proxy-Konfiguration hinzu, um nur internen IP-Zugriff zuzulassen

>>:  Verbindung zum lokalen MySQL über Socket-Lösung „/tmp/mysql.sock“ nicht möglich

Artikel empfehlen

React DVA-Implementierungscode

Inhaltsverzeichnis dva Verwendung von dva Impleme...

Ausführliches Tutorial zur Installation von mysql 5.6.23 winx64.zip

Eine ausführliche Dokumentation zur Installation ...

Schritte zum Bereitstellen eines Spring Boot-Projekts mit Docker

Inhaltsverzeichnis Erstellen Sie ein einfaches Sp...

Miniprogramm zur Implementierung des Slider-Effekts

In diesem Artikelbeispiel wird der spezifische Co...

Eine kurze Erläuterung der HTML-Tabellen-Tags

Besprechen Sie hauptsächlich seine Struktur und ei...

So überwachen Sie Tomcat mit LambdaProbe

Einführung: Lambda Probe (früher bekannt als Tomc...

So erstellen, speichern und laden Sie Docker-Images

Es gibt drei Möglichkeiten, ein Image zu erstelle...

Tutorial zur Installation und Kennwortkonfiguration von MySQL 5.7.21

Tutorial zur Installation und Kennworteinstellung...

Verwendung von MySQL-Triggern

Trigger können dazu führen, dass vor oder nach de...

Mysql5.7.14 Linux-Version Passwort vergessen perfekte Lösung

Fügen Sie in der Datei /etc/my.conf unter [mysqld...

Zusammenfassung gängiger Docker-Befehle (empfohlen)

1. Zusammenfassung: Im Allgemeinen können sie in ...

Probleme und Lösungen beim Verbinden des Knotens mit der MySQL-Datenbank

Ich habe heute eine neue Version von MySQL (8.0.2...

Eine andere Art von „Abbrechen“-Button

Der „Abbrechen“-Button ist nicht Teil des notwend...