web-dev-qa-db-fra.com

Comment la fonction util.toFastProperties de Bluebird accélère-t-elle les propriétés d'un objet?

Dans Bluebird's util.js fichier , il a la fonction suivante:

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

Pour une raison quelconque, il y a une déclaration après la fonction de retour, dont je ne sais pas pourquoi elle est là.

De plus, il semble que ce soit délibéré, car l'auteur avait fait taire l'avertissement JSHint à ce sujet:

"Eval" inaccessible après "retour". (W027)

Que fait exactement cette fonction? Est-ce que util.toFastProperties accélère-t-il vraiment les propriétés d'un objet?

J'ai recherché dans le référentiel GitHub de Bluebird des commentaires dans le code source ou une explication dans leur liste de problèmes, mais je n'en ai trouvé aucun.

165
Qantas 94 Heavy

Mise à jour 2017: Tout d'abord, pour les lecteurs qui viennent aujourd'hui - voici une version qui fonctionne avec Node 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
}

Sans une ou deux petites optimisations - tout ce qui suit est toujours valable.

Voyons d'abord ce qu'il fait et pourquoi c'est plus rapide, puis pourquoi cela fonctionne.

Ce qu'il fait

Le moteur V8 utilise deux représentations d'objets:

  • Mode dictionnaire - dans lequel l'objet est stocké en tant que mappage de valeurs-clés sous la forme d'un mappage de hachage .
  • Mode rapide - dans lequel les objets sont stockés comme structs , dans lequel aucun calcul n'est impliqué dans l'accès aux propriétés.

Voici ne démo simple qui montre la différence de vitesse. Ici, nous utilisons l'instruction delete pour forcer les objets en mode dictionnaire lent.

Le moteur essaie d'utiliser le mode rapide dans la mesure du possible et généralement chaque fois qu'un grand nombre d'accès à la propriété est effectué - mais parfois il est jeté en mode dictionnaire. Être en mode dictionnaire a une grosse pénalité de performance, donc généralement il est souhaitable de mettre des objets en mode rapide.

Ce hack est destiné à forcer l'objet en mode rapide à partir du mode dictionnaire.

Pourquoi c'est plus rapide

En JavaScript, les prototypes stockent généralement des fonctions partagées entre de nombreuses instances et changent rarement de manière dynamique. Pour cette raison, il est très souhaitable de les avoir en mode rapide pour éviter la pénalité supplémentaire chaque fois qu'une fonction est appelée.

Pour cela, la v8 mettra volontiers des objets qui sont les .prototype propriété des fonctions en mode rapide car elles seront partagées par chaque objet créé en appelant cette fonction en tant que constructeur. Il s'agit généralement d'une optimisation intelligente et souhaitable.

Comment ça marche

Voyons d'abord le code et voyons ce que fait chaque ligne:

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.
}

Nous n'avons pas avons pour trouver le code nous-mêmes pour affirmer que v8 fait cette optimisation, nous pouvons à la place lire les tests unitaires 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 lecture et l'exécution de ce test nous montrent que cette optimisation fonctionne bien en v8. Cependant - ce serait bien de voir comment.

Si nous vérifions objects.cc on peut trouver la fonction suivante (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);
  }
}

À présent, JSObject::MigrateSlowToFast prend simplement explicitement le dictionnaire et le convertit en un objet V8 rapide. C'est une lecture intéressante et un aperçu intéressant des composants internes de l'objet v8 - mais ce n'est pas le sujet ici. Je recommande toujours chaleureusement que vous le lisiez ici car c'est un bon moyen d'en apprendre davantage sur les objets v8.

Si nous vérifions SetPrototype dans objects.cc, nous pouvons voir qu'il est appelé à la ligne 12231:

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

Qui à son tour est appelé par FuntionSetPrototype qui est ce que nous obtenons avec .prototype =.

Faire __proto__ = ou .setPrototypeOf aurait aussi fonctionné mais ce sont des fonctions ES6 et Bluebird fonctionne sur tous les navigateurs depuis Netscape 7 donc il n'est pas question de simplifier le code ici. Par exemple, si nous vérifions .setPrototypeOf nous pouvons voir:

// 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;
}

Qui se trouve directement sur Object:

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

Donc - nous avons parcouru le chemin du code écrit par Petka au métal nu. C'était sympa.

Avertissement:

N'oubliez pas que ce sont tous les détails de la mise en œuvre. Des gens comme Petka sont des fous d'optimisation. Rappelez-vous toujours que l'optimisation prématurée est à l'origine de tous les maux 97% du temps. Bluebird fait très souvent quelque chose de très basique, donc il gagne beaucoup de ces hacks de performance - être aussi rapide que les rappels n'est pas facile. Vous rarement devez faire quelque chose comme ça dans du code qui n'alimente pas une bibliothèque.

313
Benjamin Gruenbaum