In che modo la funzione util.toFastProperties di Bluebird rende le proprietà di un oggetto "veloci"?


165

Nel util.jsfile di Bluebird , ha la seguente funzione:

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

Per qualche ragione, c'è una dichiarazione dopo la funzione di ritorno, che non sono sicuro del perché sia ​​lì.

Inoltre, sembra che sia intenzionale, poiché l'autore ha messo a tacere l'avvertimento JSHint su questo:

"Eval" irraggiungibile dopo "return". (W027)

Cosa fa esattamente questa funzione? Rende util.toFastPropertiesdavvero le proprietà di un oggetto "più veloci"?

Ho cercato nel repository GitHub di Bluebird eventuali commenti nel codice sorgente o una spiegazione nel loro elenco di problemi, ma non sono riuscito a trovarne nessuno.

Risposte:


314

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' deleteistruzione 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 .prototypeproprietà 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.ccpossiamo 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::MigrateSlowToFastprende 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- SetPrototypein 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 FuntionSetPrototypequale è ciò che otteniamo .prototype =.

Fare __proto__ =o .setPrototypeOfavrebbe 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 .setPrototypeOfpossiamo 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.


37
Questo è il post più interessante che ho letto da un po '. Molto rispetto e apprezzamento per te!
m59,

2
@timoxley Ho scritto quanto segue eval(nei commenti sul codice quando si spiega il codice OP pubblicato): "impedire che la funzione venga ottimizzata attraverso l'eliminazione del codice morto o ulteriori ottimizzazioni. Questo codice non viene mai raggiunto ma anche il codice non raggiungibile fa sì che v8 non ottimizzi funzioni." . Ecco una lettura correlata . Vorresti che approfondissi ulteriormente l'argomento?
Benjamin Gruenbaum,

3
@dherman a 1;non causerebbe una "deottimizzazione", debugger;probabilmente avrebbe funzionato ugualmente bene. La cosa bella è che quando evalviene passato qualcosa che non è una stringa non fa nulla con essa, quindi è piuttosto innocuo - un po 'comeif(false){ debugger; }
Benjamin Gruenbaum

6
A proposito, questo codice è stato aggiornato a causa di una modifica della recente v8, ora è necessario anche istanziare il costruttore. Così divenne più
pigro

4
@BenjaminGruenbaum Puoi approfondire il motivo per cui questa funzione NON deve essere ottimizzata? Nel codice minimizzato, eval non è comunque presente. Perché eval è utile qui nel codice non minimizzato?
Boopathi Rajaa,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.