Update 2017: Erstens für Leser, die heute kommen - hier ist eine Version, die mit Node 7 (4+) funktioniert:
function enforceFastProperties(o) {
function Sub() {}
Sub.prototype = o;
var receiver = new Sub(); // create an instance
function ic() { return typeof receiver.foo; } // perform access
ic();
ic();
return o;
eval("o" + o); // ensure no dead code elimination
}
Ohne ein oder zwei kleine Optimierungen - alle folgenden Punkte sind weiterhin gültig.
Lassen Sie uns zuerst diskutieren, was es tut und warum das schneller ist und warum es dann funktioniert.
Was es macht
Die V8-Engine verwendet zwei Objektdarstellungen:
- Wörterbuchmodus - in dem Objekte als Schlüsselwertkarten als Hash-Karte gespeichert werden .
- Schneller Modus - in dem Objekte wie Strukturen gespeichert werden , in denen keine Berechnung für den Eigenschaftszugriff erforderlich ist.
Hier ist eine einfache Demo , die den Geschwindigkeitsunterschied demonstriert. Hier verwenden wir die delete
Anweisung, um die Objekte in den langsamen Wörterbuchmodus zu zwingen.
Die Engine versucht, wann immer möglich und im Allgemeinen immer dann, wenn viel Zugriff auf Eigenschaften ausgeführt wird, den Schnellmodus zu verwenden. Manchmal wird sie jedoch in den Wörterbuchmodus versetzt. Im Wörterbuchmodus zu sein hat einen großen Leistungsverlust, daher ist es im Allgemeinen wünschenswert, Objekte in den schnellen Modus zu versetzen.
Dieser Hack soll das Objekt aus dem Wörterbuchmodus in den Schnellmodus zwingen.
Warum ist es schneller
In JavaScript speichern Prototypen normalerweise Funktionen, die von vielen Instanzen gemeinsam genutzt werden, und ändern sich selten dynamisch. Aus diesem Grund ist es sehr wünschenswert, sie im Schnellmodus zu haben, um die zusätzliche Strafe bei jedem Aufruf einer Funktion zu vermeiden.
Zu diesem Zweck versetzt v8 Objekte, die .prototype
Eigentum von Funktionen sind, gerne in den Schnellmodus, da sie von jedem Objekt gemeinsam genutzt werden, das durch Aufrufen dieser Funktion als Konstruktor erstellt wurde. Dies ist im Allgemeinen eine clevere und wünschenswerte Optimierung.
Wie es funktioniert
Lassen Sie uns zuerst den Code durchgehen und herausfinden, was jede Zeile tut:
function toFastProperties(obj) {
/*jshint -W027*/ // suppress the "unreachable code" error
function f() {} // declare a new function
f.prototype = obj; // assign obj as its prototype to trigger the optimization
// assert the optimization passes to prevent the code from breaking in the
// future in case this optimization breaks:
ASSERT("%HasFastProperties", true, obj); // requires the "native syntax" flag
return f; // return it
eval(obj); // prevent the function from being optimized through dead code
// elimination or further optimizations. This code is never
// reached but even using eval in unreachable code causes v8
// to not optimize functions.
}
Wir nicht haben , um den Code selbst zu finden , dass v8 zu behaupten , diese Optimierung ist, können wir stattdessen die v8 Unit - Tests lesen :
// Adding this many properties makes it slow.
assertFalse(%HasFastProperties(proto));
DoProtoMagic(proto, set__proto__);
// Making it a prototype makes it fast again.
assertTrue(%HasFastProperties(proto));
Das Lesen und Ausführen dieses Tests zeigt uns, dass diese Optimierung tatsächlich in Version 8 funktioniert. Es wäre jedoch schön zu sehen, wie.
Wenn wir überprüfen objects.cc
, können wir die folgende Funktion finden (L9925):
void JSObject::OptimizeAsPrototype(Handle<JSObject> object) {
if (object->IsGlobalObject()) return;
// Make sure prototypes are fast objects and their maps have the bit set
// so they remain fast.
if (!object->HasFastProperties()) {
MigrateSlowToFast(object, 0);
}
}
JSObject::MigrateSlowToFast
Nehmen Sie das Wörterbuch jetzt nur explizit und konvertieren Sie es in ein schnelles V8-Objekt. Es ist eine lohnende Lektüre und ein interessanter Einblick in die Interna von v8-Objekten - aber hier geht es nicht darum. Ich empfehle Ihnen immer noch wärmstens , es hier zu lesen, da dies eine gute Möglichkeit ist, mehr über v8-Objekte zu erfahren.
Wenn wir überprüfen SetPrototype
in objects.cc
, können wir sehen , dass es in Zeile 12231 genannt wird:
if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}
Was wiederum heißt, von FuntionSetPrototype
welchem bekommen wir .prototype =
.
Dies zu tun __proto__ =
oder .setPrototypeOf
hätte auch funktioniert, aber dies sind ES6-Funktionen und Bluebird läuft seit Netscape 7 auf allen Browsern. Daher kommt es nicht in Frage, den Code hier zu vereinfachen. Wenn wir zum Beispiel überprüfen .setPrototypeOf
, können wir sehen:
// ES6 section 19.1.2.19.
function ObjectSetPrototypeOf(obj, proto) {
CHECK_OBJECT_COERCIBLE(obj, "Object.setPrototypeOf");
if (proto !== null && !IS_SPEC_OBJECT(proto)) {
throw MakeTypeError("proto_object_or_null", [proto]);
}
if (IS_SPEC_OBJECT(obj)) {
%SetPrototype(obj, proto); // MAKE IT FAST
}
return obj;
}
Welches direkt auf ist Object
:
InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));
Also - wir sind den Weg von dem Code gegangen, den Petka zum Bare Metal geschrieben hat. Das war schön
Haftungsausschluss:
Denken Sie daran, dass dies alles Implementierungsdetails sind. Leute wie Petka sind Optimierungsfreaks. Denken Sie immer daran, dass vorzeitige Optimierung in 97% der Fälle die Wurzel allen Übels ist. Bluebird macht sehr oft etwas sehr Grundlegendes, so dass es viel von diesen Performance-Hacks profitiert - so schnell wie Rückrufe zu sein ist nicht einfach. Sie müssen selten so etwas in Code tun, der eine Bibliothek nicht mit Strom versorgt.
eval
(in den Codekommentaren bei der Erläuterung des veröffentlichten Code-OP) geschrieben: "Verhindern Sie, dass die Funktion durch Eliminierung von totem Code oder weitere Optimierungen optimiert wird. Dieser Code wird nie erreicht, aber selbst nicht erreichbarer Code führt dazu, dass v8 nicht optimiert wird Funktionen. " . Hier ist eine verwandte Lektüre . Möchten Sie, dass ich das Thema weiter erläutere?1;
würde keine "Deoptimierung" verursachen, adebugger;
hätte wahrscheinlich genauso gut funktioniert. Das Schöne ist, dass wenneval
etwas übergeben wird, das keine Saite ist, es nichts damit zu tun hat, so dass es ziemlich harmlos ist - irgendwie wieif(false){ debugger; }