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.
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.cc
kö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.
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
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 spendTimeOptimizingThisObjectAssumingItsPropertiesWontChange
was 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 🙂
// 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