Aggiornamento 2017: in primo luogo, per i lettori che arrivano oggi - ecco una versione che funziona con Nodo 7 (4+):
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
}
Senza una o due piccole ottimizzazioni, tutto quanto sotto è ancora valido.
Discutiamo prima cosa fa e perché è più veloce e quindi perché funziona.
Cosa fa
Il motore V8 utilizza due rappresentazioni di oggetti:
- Modalità dizionario - in cui gli oggetti sono memorizzati come mappe valore-chiave come mappa hash .
- Modalità veloce : in cui gli oggetti sono memorizzati come strutture , in cui non è previsto alcun calcolo nell'accesso alle proprietà.
Ecco una semplice demo che dimostra la differenza di velocità. Qui usiamo l' delete
istruzione per forzare gli oggetti in modalità dizionario lento.
Il motore tenta di utilizzare la modalità veloce ogni volta che è possibile e generalmente ogni volta che viene eseguito un sacco di accesso alle proprietà, tuttavia a volte viene lanciato in modalità dizionario. Essere in modalità dizionario ha una forte penalizzazione delle prestazioni, quindi generalmente è preferibile mettere gli oggetti in modalità veloce.
Questo hack ha lo scopo di forzare l'oggetto in modalità veloce dalla modalità dizionario.
Perché è più veloce
In JavaScript i prototipi in genere memorizzano funzioni condivise tra più istanze e raramente cambiano molto dinamicamente. Per questo motivo è molto desiderabile averli in modalità veloce per evitare la penalità aggiuntiva ogni volta che viene chiamata una funzione.
Per questo - v8 metterà volentieri gli oggetti che sono .prototype
proprietà delle funzioni in modalità veloce poiché saranno condivisi da ogni oggetto creato invocando quella funzione come costruttore. Questa è generalmente un'ottimizzazione intelligente e desiderabile.
Come funziona
Analizziamo innanzitutto il codice e calcoliamo cosa fa ogni riga:
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.
}
Non dobbiamo trovare noi stessi il codice per affermare che v8 esegue questa ottimizzazione, possiamo invece leggere i test unitari v8 :
// 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));
La lettura e l'esecuzione di questo test ci mostrano che questa ottimizzazione funziona davvero in v8. Tuttavia, sarebbe bello vedere come.
Se controlliamo objects.cc
possiamo trovare la seguente funzione (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);
}
}
Ora, JSObject::MigrateSlowToFast
prende esplicitamente il dizionario e lo converte in un oggetto V8 veloce. È una lettura utile e una visione interessante degli interni degli oggetti v8 - ma non è l'argomento qui. Consiglio ancora vivamente di leggerlo qui in quanto è un buon modo per conoscere gli oggetti v8.
Se effettuiamo il check- SetPrototype
in objects.cc
, possiamo vedere che si chiama nella riga 12231:
if (value->IsJSObject()) {
JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
}
Che a sua volta viene chiamato da FuntionSetPrototype
quale è ciò che otteniamo .prototype =
.
Fare __proto__ =
o .setPrototypeOf
avrebbe anche funzionato, ma queste sono funzioni ES6 e Bluebird funziona su tutti i browser da Netscape 7, quindi è fuori discussione per semplificare il codice qui. Ad esempio, se controlliamo .setPrototypeOf
possiamo vedere:
// 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;
}
Che è direttamente su Object
:
InstallFunctions($Object, DONT_ENUM, $Array(
...
"setPrototypeOf", ObjectSetPrototypeOf,
...
));
Quindi - abbiamo percorso il sentiero dal codice che Petka ha scritto al bare metal. È stato bello
Disclaimer:
Ricorda che si tratta di tutti i dettagli di implementazione. Persone come Petka sono patiti dell'ottimizzazione. Ricorda sempre che l'ottimizzazione prematura è la radice di tutti i mali del 97% delle volte. Bluebird fa molto spesso qualcosa di molto semplice, quindi guadagna molto da questi hack di prestazioni - essere veloci come i callback non è facile. È raro che dovete fare qualcosa di simile nel codice che non si accende una biblioteca.