Einführung
Daniel Clifford hat bei der Google I/O einen hervorragenden Vortrag über Tipps und Tricks zur Verbesserung der JavaScript-Leistung in V8 gehalten. Daniel ermutigt uns dazu, „schnellere Nachfrage“ – um Leistungsunterschiede zwischen C++ und JavaScript sorgfältig zu analysieren und Code über die Funktionsweise von JavaScript zu schreiben. In diesem Artikel finden Sie eine Zusammenfassung der wichtigsten Punkte von Daniels Vortrag. Dieser Artikel wird aktualisiert, wenn sich die Leistungsrichtlinien ändern.
Der wichtigste Ratschlag
Es ist wichtig, Tipps zur Leistungsoptimierung in einen Kontext zu stellen. Tipps zur Leistungsoptimierung sind süchtig. Manchmal lenken Sie schon viel von den eigentlichen Problemen ab, wenn Sie sich zuerst auf tief greifende Ratschläge konzentrieren. Sie müssen sich einen umfassenden Überblick über die Leistung Ihrer Webanwendung verschaffen. Bevor Sie sich auf diesen Leistungstipp konzentrieren, sollten Sie Ihren Code am besten mit Tools wie PageSpeed analysieren und eine Verbesserung erzielen. So können Sie eine vorzeitige Optimierung vermeiden.
Der beste grundlegende Rat für eine gute Leistung in Webanwendungen lautet:
- Seien Sie vorbereitet, bevor Sie ein Problem haben (oder bemerken).
- Identifizieren und verstehen Sie dann den Kern des Problems.
- Abschließend die wichtigen Probleme beheben
Um diese Schritte durchzuführen, sollten Sie wissen, wie V8 JavaScript optimiert, damit Sie Code unter Berücksichtigung des JS-Laufzeitdesigns schreiben können. Es ist auch wichtig, die verfügbaren Tools zu kennen und zu erfahren, wie sie Ihnen helfen können. Daniel erklärt in seinem Vortrag ausführlicher, wie die Entwicklertools verwendet werden. werden in diesem Dokument nur einige der wichtigsten Punkte des V8-Motordesigns behandelt.
Weiter zu den V8-Tipps!
Ausgeblendete Klassen
JavaScript verfügt über begrenzte Informationen zur Kompilierungszeit: Typen können während der Laufzeit geändert werden, daher ist es normal, zu erwarten, dass die Erläuterung der JS-Typen zum Zeitpunkt der Kompilierung kostspielig ist. Daher fragen Sie sich vielleicht, wie die JavaScript-Leistung jemals nahe an C++ kommen könnte. V8 enthält jedoch versteckte Typen, die intern für Objekte zur Laufzeit erstellt werden. -Objekte mit derselben verborgenen Klasse können dann denselben optimierten generierten Code verwenden.
Beispiel:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!```
Bis die Objektinstanz p2 das zusätzliche Mitglied „.z“ hat hinzugefügt haben, haben p1 und p2 intern dieselbe verborgene Klasse, sodass V8 eine einzelne Version einer optimierten Assembly für JavaScript-Code generieren kann, die entweder p1 oder p2 manipuliert. Je mehr Sie vermeiden können, dass die versteckten Klassen voneinander abweichen, desto besser ist die Leistung.
Dementsprechend
- Alle Objektmitglieder in Konstruktorfunktionen initialisieren (damit die Instanzen den Typ später nicht ändern)
- Objektmitglieder immer in der gleichen Reihenfolge initialisieren
iWork Numbers
V8 nutzt Tagging, um Werte effizient darzustellen, wenn sich Typen ändern können. V8 leitet von den von Ihnen verwendeten Werten ab, mit welchem Zahlentyp Sie es zu tun haben. Sobald V8 diese Inferenz erstellt hat, nutzt es Tagging, um Werte effizient darzustellen, da sich diese Typen dynamisch ändern können. Allerdings sind beim Ändern dieser Typ-Tags manchmal Kosten anfallen. Daher ist es am besten, Zahlentypen einheitlich zu verwenden. Im Allgemeinen ist es am besten, 31-Bit-Ganzzahlen mit Vorzeichen zu verwenden.
Beispiel:
var i = 42; // this is a 31-bit signed integer
var j = 4.2; // this is a double-precision floating point number```
Dementsprechend
- Bevorzugen Sie numerische Werte, die als vorzeichenbehaftete 31-Bit-Ganzzahlen dargestellt werden können.
Arrays
Um große und dünnbesetzte Arrays zu verarbeiten, gibt es intern zwei Arten von Arrayspeichern:
- Fast Elements: Linearer Speicher für kompakte Schlüsselsätze
- Wörterbuchelemente: andernfalls Hash-Tabellenspeicherung
Der Array-Speicher sollte nicht von einem Typ zum anderen gewechselt werden.
Dementsprechend
- Für Arrays zusammenhängende Schlüssel verwenden, die bei 0 beginnen
- Weisen Sie große Arrays (z. B. mit mehr als 64.000 Elementen) nicht vorab ihrer maximalen Größe zu, sondern wachsen Sie im Laufe der Zeit.
- Löschen Sie keine Elemente in Arrays, insbesondere keine numerischen Arrays.
- Nicht initialisierte oder gelöschte Elemente laden:
for (var b = 0; b < 10; b++) {
a[0] |= b; // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
a[0] |= b; // Much better! 2x faster.
}
Außerdem sind Arrays mit Double-Werten schneller. Die verborgenen Klassen des Arrays verfolgen Elementtypen und Arrays, die nur Double-Werte enthalten, werden entfernt, was zu einer verborgenen Klassenänderung führt. Unachtsame Manipulation von Arrays kann jedoch zusätzliche Arbeit aufgrund von Boxing und Unboxing verursachen, z. B.
var a = new Array();
a[0] = 77; // Allocates
a[1] = 88;
a[2] = 0.5; // Allocates, converts
a[3] = true; // Allocates, converts```
ist weniger effizient als:
var a = [77, 88, 0.5, true];
da im ersten Beispiel die einzelnen Zuweisungen nacheinander ausgeführt werden und die Zuweisung von a[2]
dazu führt, dass das Array in ein Array von Unboxed-Doubles konvertiert wird. Die Zuweisung von a[3]
führt jedoch dazu, dass es wieder in ein Array konvertiert wird, das beliebige Werte (Zahlen oder Objekte) enthalten kann. Im zweiten Fall kennt der Compiler die Typen aller Elemente im Literal und die verborgene Klasse kann im Voraus bestimmt werden.
- Mit Arrayliteralen für kleine Arrays mit fester Größe initialisieren
- Weisen Sie kleine Arrays (< 64.000) vorab der korrekten Größe zu, bevor Sie sie verwenden.
- Nicht numerische Werte (Objekte) nicht in numerischen Arrays speichern
- Achten Sie darauf, dass kleine Arrays nicht neu konvertiert werden, wenn Sie ohne Literale initialisieren.
JavaScript-Kompilierung
Obwohl JavaScript eine sehr dynamische Sprache ist und die ursprünglichen Implementierungen davon Interpreter waren, verwenden moderne JavaScript-Laufzeit-Engines die Kompilierung. V8 (das JavaScript von Chrome) verfügt über zwei verschiedene Just-In-Time-Compiler (JIT):
- Die „vollständige“ -Compiler, der guten Code für JavaScript-Code generieren kann,
- Der Optimizer-Compiler, der für die meisten JavaScript-Codes hervorragenden Code produziert, aber länger dauert, bis er kompiliert ist.
Der vollständige Compiler
In V8 wird der Full-Compiler für den gesamten Code ausgeführt und beginnt so bald wie möglich mit der Ausführung von Code. Dabei wird schnell guter, aber nicht guter Code generiert. Dieser Compiler geht bei der Kompilierung so gut wie nichts über Typen aus. Er erwartet, dass sich Variablentypen zur Laufzeit ändern können und werden. Der vom Full-Compiler generierte Code nutzt Inline-Caches (ICs), um das Wissen über Typen während der Programmausführung zu verfeinern und so die Effizienz im laufenden Betrieb zu verbessern.
Das Ziel von Inline-Caches besteht darin, Typen effizient zu verarbeiten, indem typabhängiger Code für Vorgänge im Cache gespeichert wird. Wenn der Code ausgeführt wird, validiert er zuerst die Typannahmen und verwendet dann den Inline-Cache, um eine Verknüpfung für den Vorgang zu erstellen. Dies bedeutet jedoch, dass Vorgänge, die mehrere Typen akzeptieren, weniger leistungsfähig sind.
Dementsprechend
- Die monomorphe Verwendung von Operationen wird gegenüber polymorphen Operationen bevorzugt
Operationen sind monomorph, wenn die verborgenen Klassen von Eingaben immer identisch sind. Andernfalls sind sie polymorph, was bedeutet, dass sich der Typ einiger Argumente bei verschiedenen Aufrufen des Vorgangs ändern kann. Der zweite add()-Aufruf in diesem Beispiel führt beispielsweise zu Polymorphie:
function add(x, y) {
return x + y;
}
add(1, 2); // + in add is monomorphic
add("a", "b"); // + in add becomes polymorphic```
Der optimierende Compiler
Parallel zum Full-Compiler kompiliert V8 "hot" (d. h. Funktionen, die häufig ausgeführt werden) mit einem Optimierungscompiler zusammen. Dieser Compiler verwendet Typfeedback, um den kompilierten Code zu beschleunigen. Tatsächlich verwendet er die Typen von ICs, über die wir gerade gesprochen haben.
Im Optimierungs-Compiler werden Vorgänge spekulativ Inline-Vorgänge ausführen (direkt an der Stelle platziert, an der sie aufgerufen werden). Dies beschleunigt die Ausführung (auf Kosten des Speicherbedarfs), ermöglicht aber auch andere Optimierungen. Monomorphe Funktionen und Konstruktoren können vollständig inline eingefügt werden (dies ist ein weiterer Grund, warum Monomorphismus in V8 eine gute Idee ist).
Mit dem eigenständigen „d8“ kannst du protokollieren, was optimiert wird Version der V8-Engine:
d8 --trace-opt primes.js
(Hiermit werden die Namen optimierter Funktionen in stdout protokolliert.)
Nicht alle Funktionen können jedoch optimiert werden. Einige Funktionen verhindern, dass der optimierende Compiler für eine bestimmte Funktion ausgeführt wird. Dies wird als „Bailout“ bezeichnet. Insbesondere der Optimierungs-Compiler greift derzeit auf Funktionen mit dem Konstruktion {} Catch {}-Blöcken aus!
Dementsprechend
- Fügen Sie sicherheitsrelevanten Code in eine verschachtelte Funktion ein, wenn Sie versuchen, {}-Catch-{}-Blöcke zu verwenden: ```js function perf_sensitive() { // Hier leistungsabhängige Aktionen ausführen }
versuchen { perf_sensitive() } catch (e) { // Ausnahmen hier behandeln }
Diese Anleitung wird sich in Zukunft wahrscheinlich ändern, da wir Try/Catch-Blöcke im Optimize-Compiler aktivieren. Sie können untersuchen, wie der optimierende Compiler Funktionen ausschaltet, indem Sie den "--trace-opt" verwenden. mit d8 wie oben angegeben, wodurch Sie mehr Informationen darüber erhalten, welche Funktionen aussortiert wurden:
d8 --trace-opt primes.js
De-Optimierung
Schließlich ist die von diesem Compiler durchgeführte Optimierung spekulativ. Manchmal funktioniert sie nicht und wir machen den Vorgang zurück. Der Prozess der „Deoptimierung“ Optimierten Code wird weggeworfen und die Ausführung wird „vollständig“ an der richtigen Stelle fortgesetzt. Compiler-Code. Die erneute Optimierung kann später noch einmal ausgelöst werden, aber kurzfristig verlangsamt sich die Ausführung. Insbesondere wenn Änderungen an den verborgenen Klassen von Variablen nach der Optimierung der Funktionen vorgenommen werden, kommt es zu dieser Deoptimierung.
Dementsprechend
- Verborgene Klassenänderungen in Funktionen nach der Optimierung vermeiden
Wie bei anderen Optimierungen können Sie mit einem Logging-Flag ein Log der Funktionen abrufen, die von V8 entfernt werden mussten:
d8 --trace-deopt primes.js
Andere V8-Tools
Übrigens, du kannst beim Start auch V8-Tracing-Optionen an Chrome übergeben:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --js-flags="--trace-opt --trace-deopt"```
Neben der Profilerstellung mit den Entwicklertools können Sie auch d8 für die Profilerstellung verwenden:
% out/ia32.release/d8 primes.js --prof
Dabei wird der integrierte Sampling-Profiler verwendet, der jede Millisekunde eine Stichprobe abruft und v8.log schreibt.
Zusammenfassung
Es ist wichtig, dass Sie genau wissen, wie die V8-Engine mit Ihrem Code arbeitet, um die Entwicklung von leistungsstarkem JavaScript vorzubereiten. Hier noch einmal der grundlegende Rat:
- Seien Sie vorbereitet, bevor Sie ein Problem haben (oder bemerken).
- Identifizieren und verstehen Sie dann den Kern des Problems.
- Abschließend die wichtigen Probleme beheben
Sie sollten also zuerst mithilfe anderer Tools wie PageSpeed prüfen, ob das Problem in Ihrem JavaScript-Code liegt. vor dem Erfassen von Messwerten auf reines JavaScript (kein DOM) reduzieren und diese Messwerte dann verwenden, um Engpässe zu finden und wichtige zu beseitigen. Hoffentlich werden Daniels Vortrag (und dieser Artikel) Ihnen helfen, besser zu verstehen, wie V8 JavaScript ausführt. Konzentrieren Sie sich aber auch auf die Optimierung Ihrer eigenen Algorithmen!