Wie macht die Funktion util.toFastProperties von Bluebird die Eigenschaften eines Objekts „schnell“?


165

In Bluebirds util.jsDatei hat es die folgende Funktion:

function toFastProperties(obj) {
    /*jshint -W027*/
    function f() {}
    f.prototype = obj;
    ASSERT("%HasFastProperties", true, obj);
    return f;
    eval(obj);
}

Aus irgendeinem Grund gibt es nach der Rückgabefunktion eine Anweisung, bei der ich nicht sicher bin, warum sie vorhanden ist.

Es scheint auch absichtlich zu sein, da der Autor die JSHint-Warnung dazu zum Schweigen gebracht hat:

Nicht erreichbare 'Bewertung' nach 'Rückkehr'. (W027)

Was genau macht diese Funktion? Macht es util.toFastPropertiesdie Eigenschaften eines Objekts wirklich "schneller"?

Ich habe das GitHub-Repository von Bluebird nach Kommentaren im Quellcode oder einer Erklärung in der Liste der Probleme durchsucht, aber keine gefunden.

Antworten:


314

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 deleteAnweisung, 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 .prototypeEigentum 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::MigrateSlowToFastNehmen 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 SetPrototypein 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 FuntionSetPrototypewelchem ​​bekommen wir .prototype =.

Dies zu tun __proto__ =oder .setPrototypeOfhä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.


37
Dies ist der interessanteste Beitrag, den ich seit einiger Zeit gelesen habe. Viel Respekt und Wertschätzung für Sie!
M59

2
@timoxley Ich habe Folgendes über 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?
Benjamin Gruenbaum

3
@dherman a 1;würde keine "Deoptimierung" verursachen, a debugger;hätte wahrscheinlich genauso gut funktioniert. Das Schöne ist, dass wenn evaletwas übergeben wird, das keine Saite ist, es nichts damit zu tun hat, so dass es ziemlich harmlos ist - irgendwie wieif(false){ debugger; }
Benjamin Gruenbaum

6
Übrigens wurde dieser Code aufgrund einer Änderung in Version 8 aktualisiert. Jetzt müssen Sie auch den Konstruktor instanziieren. So wurde es fauler; d
Esailija

4
@BenjaminGruenbaum Können Sie erläutern, warum diese Funktion NICHT optimiert werden sollte? Im minimierten Code ist eval ohnehin nicht vorhanden. Warum ist eval hier im nicht minimierten Code nützlich?
Boopathi Rajaa
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.