Comment la fonction util.toFastProperties de Bluebird rend-elle les propriétés d'un objet «rapides»?

165

Dans le util.jsfichier de Bluebird , 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 instruction 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 de JSHint à ce sujet:

'Eval' inaccessible après 'return'. (W027)

Que fait exactement cette fonction? Rend-il util.toFastPropertiesvraiment les propriétés d'un objet "plus rapides"?

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.

Qantas 94 lourd
la source

Réponses:

314

Mise à jour 2017: Tout d'abord, pour les lecteurs à venir 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 les objets sont stockés sous forme de cartes clé-valeur sous forme de carte de hachage .
  • Mode rapide - dans lequel les objets sont stockés comme des structures , dans lequel aucun calcul n'est impliqué dans l'accès aux propriétés.

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

Le moteur essaie d'utiliser le mode rapide chaque fois que possible et généralement chaque fois qu'un grand nombre d'accès aux propriétés est effectué - cependant, il est parfois jeté en mode dictionnaire. Être en mode dictionnaire a une grande pénalité en termes de performances, il est donc généralement souhaitable de mettre les objets en mode rapide.

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

Pourquoi c'est plus rapide

Dans les prototypes JavaScript, les fonctions sont généralement partagées entre de nombreuses instances et changent rarement beaucoup 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 les objets qui sont la .prototypepropriété des fonctions en mode rapide car ils seront partagés par chaque objet créé en invoquant cette fonction en tant que constructeur. Il s'agit généralement d'une optimisation intelligente et souhaitable.

Comment ça fonctionne

Passons d'abord en revue 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 besoin de trouver le code nous-mêmes pour affirmer que la v8 effectue cette optimisation, nous pouvons plutôt lire les tests unitaires de la 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.ccnous pouvons 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);
  }
}

Maintenant, JSObject::MigrateSlowToFastprend 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 éléments internes des objets v8 - mais ce n'est pas le sujet ici. Je vous recommande toujours vivement de le lire ici car c'est un bon moyen d'en apprendre davantage sur les objets v8.

Si nous vérifions SetPrototypedans objects.cc, nous pouvons voir qu'il est appelé en ligne 12231:

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

Qui à son tour est appelé par FuntionSetPrototypece que nous obtenons .prototype =.

Faire __proto__ =ou .setPrototypeOfaurait également fonctionné mais ce sont des fonctions ES6 et Bluebird fonctionne sur tous les navigateurs depuis Netscape 7, il n'est donc pas question de simplifier le code ici. Par exemple, si nous vérifions, .setPrototypeOfnous 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 est directement sur Object:

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

Donc - nous avons parcouru le chemin du code écrit par Petka au bare metal. C'était sympa.

Avertissement:

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

Benjamin Gruenbaum
la source
37
C'est le post le plus intéressant que j'ai lu depuis un moment. Beaucoup de respect et d'appréciation à vous!
m59
2
@timoxley J'ai écrit ce qui suit à propos du eval(dans les commentaires de code pour expliquer le code OP publié): "empêcher l'optimisation de la fonction par l'élimination du code mort ou d'autres optimisations. Ce code n'est jamais atteint mais même le code inaccessible empêche la v8 d'optimiser les fonctions." . Voici une lecture connexe . Souhaitez-vous que je développe davantage sur le sujet?
Benjamin Gruenbaum
3
@dherman a 1;ne provoquerait pas de "désoptimisation", a debugger;aurait probablement aussi bien fonctionné. Ce qui est bien, c'est que quand evalon passe quelque chose qui n'est pas une corde, ça ne fait rien avec donc c'est plutôt inoffensif - un peu commeif(false){ debugger; }
Benjamin Gruenbaum
6
Btw ce code a été mis à jour en raison d'un changement dans la récente v8, vous devez maintenant également instancier le constructeur. Donc, il est devenu plus
paresseux
4
@BenjaminGruenbaum Pouvez-vous expliquer pourquoi cette fonction ne devrait PAS être optimisée? Dans le code minifié, eval n'est de toute façon pas présent. Pourquoi eval est-il utile ici dans le code non minifié?
Boopathi Rajaa