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

Lesezeit: 11 Minuten

Benutzer-Avatar
Qantas 94 Schwer

Bei Bluebird util.js Dateies hat 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 eine Anweisung nach der Return-Funktion, von der ich nicht sicher bin, warum sie dort ist.

Außerdem scheint es so zu sein, dass der Autor die JSHint-Warnung dazu zum Schweigen gebracht hat:

Unerreichbares ‘eval’ nach ‘return’. (W027)

Was macht diese Funktion genau? Tut util.toFastProperties die Eigenschaften eines Objekts wirklich “schneller” machen?

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

Benutzer-Avatar
Benjamin Grünbaum

Update 2017: Zunächst einmal 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 – alles unten ist immer noch gültig.


Lassen Sie uns zuerst besprechen, was es tut und warum das schneller ist und warum es dann funktioniert.

Was es macht

Der V8-Motor verwendet zwei Objektdarstellungen:

  • Wörterbuchmodus – in dem Objekt als Schlüssel gespeichert sind – Wertkarten als a Hash-Karte.
  • Schneller Modus – in denen Gegenstände wie gespeichert werden Strukturenbei dem keine Berechnung für den Zugriff auf Eigenschaften erforderlich ist.

Hier ist eine einfache Demo das zeigt den Geschwindigkeitsunterschied. Hier verwenden wir die delete -Anweisung, um die Objekte in den langsamen Wörterbuchmodus zu zwingen.

Die Engine versucht wann immer möglich den schnellen Modus zu verwenden und im Allgemeinen immer dann, wenn viele Eigenschaftszugriffe durchgeführt werden – manchmal wird sie jedoch in den Wörterbuchmodus geworfen. Der Wörterbuchmodus hat eine große Leistungseinbuße, 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 schnellen Modus zwingen.

  • Bluebirds Petka höchstpersönlich spricht hier darüber.
  • Diese Folien (Wayback-Maschine) von Vyacheslav Egorov erwähnt es auch.
  • Die Frage „*https://stackoverflow.com/questions/23455678/pros-and-cons-of-dictionary-mode*“ und ihre akzeptierte Antwort sind ebenfalls verwandt.
  • Dieser etwas veraltete Artikel ist immer noch eine ziemlich gute Lektüre, die Ihnen eine gute Vorstellung davon geben kann, wie Objekte in v8 gespeichert werden.

Warum es schneller ist

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 schnellen Modus zu haben, um die zusätzliche Strafe bei jedem Aufruf einer Funktion zu vermeiden.

Dafür – v8 wird gerne Objekte setzen, die die sind .prototype Eigenschaft von Funktionen im schnellen Modus, da sie von jedem Objekt geteilt werden, das durch Aufrufen dieser Funktion als Konstruktor erstellt wird. 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, um zu behaupten, dass v8 diese Optimierung durchführt, können wir stattdessen Lesen Sie die v8-Einheitentests:

// 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 v8 funktioniert. Allerdings – es wäre schön zu sehen, wie.

Wenn wir nachsehen objects.cc finden wir die folgende Funktion (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);
    }
}

Jetzt, JSObject::MigrateSlowToFast Nimmt einfach explizit das Wörterbuch und konvertiert es in ein schnelles V8-Objekt. Es ist eine lohnende Lektüre und ein interessanter Einblick in die Interna von v8-Objekten – aber das ist hier nicht das Thema. Ich kann es immer noch wärmstens empfehlen dass du es hier liest da es eine gute Möglichkeit ist, etwas über v8-Objekte zu lernen.

Wenn wir auschecken SetPrototype in objects.cckönnen wir sehen, dass es in Zeile 12231 aufgerufen wird:

if (value->IsJSObject()) {
    JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}

Die wiederum heißt by FuntionSetPrototype das ist, was wir mit bekommen .prototype =.

Tun __proto__ = oder .setPrototypeOf hätte auch funktioniert, aber das sind ES6-Funktionen und Bluebird läuft auf allen Browsern seit Netscape 7, also kommt es nicht in Frage, den Code hier zu vereinfachen. Zum Beispiel, wenn wir überprüfen .setPrototypeOf 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;
}

Was direkt an ist Object:

InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));

Also – wir sind den Weg vom Code, den Petka geschrieben hat, bis zum Bare Metal gegangen. 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, also profitiert es stark von diesen Performance-Hacks – es ist nicht einfach, so schnell wie Callbacks zu sein. Du selten muss so etwas in Code tun, der keine Bibliothek antreibt.

  • Das ist der interessanteste Beitrag, den ich seit langem gelesen habe. Ihnen viel Respekt und Anerkennung!

    – m59

    28. Juli 2014 um 7:08 Uhr

  • @timoxley Ich habe Folgendes über die geschrieben eval (in den Code-Kommentaren bei der Erläuterung des von OP geposteten Codes): “Verhindern, dass die Funktion durch Eliminierung von totem Code oder weitere Optimierungen optimiert wird. Dieser Code wird nie erreicht, aber selbst unerreichbarer Code führt dazu, dass v8 Funktionen nicht optimiert.” . Hier ist eine verwandte Lektüre. Möchten Sie, dass ich das Thema näher erläutere?

    – Benjamin Grünbaum

    28. Juli 2014 um 14:51 Uhr

  • @dherman a 1; würde keine “Deoptimierung” bewirken, a debugger; hätte wahrscheinlich genauso gut funktioniert. Das Schöne ist, wann eval wird etwas übergeben, das kein String ist, es macht nichts damit, also ist es ziemlich harmlos – irgendwie so if(false){ debugger; }

    – Benjamin Grünbaum

    28. Juli 2014 um 15:27 Uhr

  • Übrigens wurde dieser Code aufgrund einer Änderung in der letzten v8 aktualisiert, jetzt müssen Sie auch den Konstruktor instanziieren. So wurde es fauler ;d

    – Esailija

    29. März 2015 um 2:09 Uhr

  • @BenjaminGruenbaum Können Sie erläutern, warum diese Funktion NICHT optimiert werden sollte? Im minimierten Code ist eval sowieso nicht vorhanden. Warum ist eval hier im nicht minifizierten Code nützlich?

    – Boopathie Rajaa

    3. August 2016 um 20:46 Uhr


Realität ab 2021 (NodeJS Version 12+). Scheint, als ob eine große Optimierung durchgeführt wurde, Objekte mit gelöschten Feldern und spärlichen Arrays werden nicht langsam. Oder übersehe ich etwas?

// run in Node with enabled flag
// node --allow-natives-syntax script.js

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var obj1 = new Point(1, 2);
var obj2 = new Point(3, 4);
delete obj2.y;

var arr = [1,2,3]
arr[100] = 100

console.log('obj1 has fast properties:', %HasFastProperties(obj1));
console.log('obj2 has fast properties:', %HasFastProperties(obj2));
console.log('arr has fast properties:', %HasFastProperties(arr));

beide zeigen wahr

obj1 has fast properties: true
obj2 has fast properties: true
arr has fast properties: true

  • Array-Elemente und -Eigenschaften waren unter der Haube schon immer unterschiedlich. Ein Array kann strukturähnliche Eigenschaften und Dictionary-Modus-Elemente haben oder umgekehrt. — Seit ein paar Jahren gibt es einen Sonderfall, bei dem das Löschen der letzte -Eigenschaft macht einfach ihre Hinzufügung rückgängig, ohne das Objekt in den Wörterbuchmodus zu überführen. Versuchen delete obj1.x und es wird in den Wörterbuchmodus gehen. (Das ist wirklich der „Schnell zum Hinzufügen/Löschen von Eigenschaften“-Modus.)

    – jmrk

    12. Juni um 13:44 Uhr

V8-Entwickler hier. Die akzeptierte Antwort ist eine großartige Erklärung, ich wollte nur eines hervorheben: Die sogenannten “schnellen” und “langsamen” Eigenschaftsmodi sind unglückliche Fehlbezeichnungen, sie haben jeweils ihre Vor- und Nachteile. Hier ist eine (leicht vereinfachte) Übersicht über die Leistung verschiedener Operationen:

strukturähnliche Eigenschaften Wörterbucheigenschaften
Hinzufügen einer Eigenschaft zu einem Objekt -- +
Löschen einer Eigenschaft --- +
Lesen/Schreiben einer Eigenschaft, zum ersten Mal - +
Lesen/Schreiben, zwischengespeichert, monomorph +++ +
Lesen/Schreiben, zwischengespeichert, wenige Formen ++ +
Lesen/Schreiben, zwischengespeichert, viele Formen -- +
umgangssprachliche Bezeichnung “schnell” “langsam”

Wie Sie also sehen können, sind die Wörterbucheigenschaften für die meisten Zeilen in dieser Tabelle tatsächlich schneller, weil es ihnen egal ist, was Sie tun, sie verarbeiten einfach alles mit solider (wenn auch nicht rekordverdächtiger) Leistung. Strukturähnliche Eigenschaften sind für eine bestimmte Situation blitzschnell (Lesen/Schreiben der Werte vorhandener Eigenschaften, wo jede einzelne Stelle im Code nur sehr wenige eindeutige Objektformen sieht), aber der Preis dafür ist, dass alle anderen Operationen, insbesondere solche, die Eigenschaften hinzufügen oder entfernen, werden viel Langsamer.

Zufällig hat der Spezialfall, wo strukturähnliche Eigenschaften ihren großen Vorteil haben (+++) ist besonders häufig und sehr wichtig für die Leistung vieler Apps, weshalb sie den Spitznamen “schnell” erhalten haben. Aber es ist wichtig, das zu erkennen, wenn Sie delete Eigenschaften und V8 schaltet die betroffenen Objekte in den Wörterbuchmodus, dann ist es nicht dumm oder versucht zu nerven, sondern versucht, Ihnen die bestmögliche Leistung für das zu bieten, was Sie tun. Wir haben in der Vergangenheit Patches gelandet, die eine beachtliche Leistung erzielt haben Verbesserungen indem mehr Objekte gegebenenfalls früher in den Wörterbuchmodus (“langsam”) wechseln.

Nun, es kann passieren, dass Ihre Objekte im Allgemeinen von strukturähnlichen Eigenschaften profitieren würden, aber etwas in Ihrem Code bewirkt, dass V8 sie in Wörterbucheigenschaften umwandelt, und Sie möchten das rückgängig machen; Bluebird hatte einen solchen Fall. Trotzdem der Name toFastProperties ist in seiner Einfachheit etwas irreführend; ein genauerer (wenn auch unhandlicher) Name wäre spendTimeOptimizingThisObjectAssumingItsPropertiesWontChangewas darauf hindeuten würde, dass die Operation selbst kostspielig ist und nur in bestimmten begrenzten Fällen sinnvoll ist. Wenn jemand hat die Schlussfolgerung mitgenommen “Oh, das ist großartig, also kann ich jetzt glücklich Eigenschaften löschen und einfach anrufen toFastProperties danach jedes Mal”, dann wäre das ein großes Missverständnis und würde zu einem ziemlich schlimmen Leistungsabfall führen.

Wenn Sie sich an ein paar einfache Faustregeln halten, werden Sie nie einen Grund haben, auch nur zu versuchen, Änderungen der internen Objektdarstellung zu erzwingen:

  • Verwenden Sie Konstruktoren und initialisieren Sie alle Eigenschaften im Konstruktor. (Dies hilft nicht nur Ihrer Engine, sondern auch der Verständlichkeit und Wartbarkeit Ihres Codes. Beachten Sie, dass TypeScript dies nicht unbedingt erzwingt, sondern stark dazu ermutigt, da es die Produktivität der Entwicklung fördert.)
  • Verwenden Sie Klassen oder Prototypen, um Methoden zu installieren, und legen Sie sie nicht einfach auf jede Objektinstanz. (Auch dies ist aus vielen Gründen eine gängige Best Practice, einer davon ist, dass es schneller ist.)
  • Vermeiden delete. Wenn Eigenschaften kommen und gehen, verwenden Sie lieber a Map über das „Object-as-Map“-Muster aus der ES5-Ära. Wenn ein Objekt in einen bestimmten Zustand ein- und ausschalten kann, bevorzugen Sie boolesche (oder äquivalente) Eigenschaften (z o.has_state = true; o.has_state = false;) über das Hinzufügen und Löschen einer Indikatoreigenschaft.
  • Wenn es um Leistung geht, messen, messen, messen. Bevor Sie anfangen, Zeit in Leistungsverbesserungen zu investieren, erstellen Sie ein Profil Ihrer App, um zu sehen, wo sich die Hotspots befinden. Wenn Sie eine Änderung implementieren, von der Sie hoffen, dass sie die Dinge beschleunigt, überprüfen Sie sie mit Ihrer echten App (oder etwas sehr Ähnliches; nicht nur ein 10-Linien-Mikrobenchmark!), dass es tatsächlich hilft.

Wenn Ihr Teamleiter Ihnen schließlich sagt: “Ich habe gehört, dass es ‘schnelle’ und ‘langsame’ Eigenschaften gibt, stellen Sie bitte sicher, dass alle unsere Eigenschaften ‘schnell’ sind”, dann verweisen Sie sie auf diesen Beitrag 🙂

  • Bevorzugen Sie boolesche Eigenschaften gegenüber dem Hinzufügen und Löschen einer Indikatoreigenschaft.” – oder setzen Sie die Eigenschaft des Indikators auf null/undefined für die Zustände, in denen es keinen normalen Wert haben sollte

    – Bergi

    20. Juni um 0:01

  • @Bergi: ja, genau das meinte ich mit “boolean (oder gleichwertig)” 🙂

    – jmrk

    20. Juni um 10:06 Uhr

// run in Node with enabled flag
// node --allow-natives-syntax script.js

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var obj2 = new Point(3, 4);
console.log('obj has fast properties:', %HasFastProperties(obj2)) // true
delete obj2.y;
console.log('obj2 has fast properties:', %HasFastProperties(obj2)); //true

var obj = {x : 1, y : 2};
console.log('obj has fast properties:', %HasFastProperties(obj))  //true
delete obj.x;

console.log('obj has fast properties:', %HasFastProperties(obj)); //fasle

Funktion und Objekt sehen unterschiedlich aus

1283760cookie-checkWie macht die Funktion util.toFastProperties von Bluebird die Eigenschaften eines Objekts “schnell”?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy