Comment les fonctionnalités util.toFastProperties de Bluebird rendent les propriétés d'un objet «rapides»?

Dans le fichier util.js 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 déclaration après la fonction de retour, que je ne sais pas pourquoi c'est là.

De plus, il semble que ce soit délibéré, car l'auteur a fait taire l'avertissement de JSHint à propos de ceci:

«Eval» inaccessible après «retour». (W027)

Que fait exactement cette fonction? util.toFastProperties vraiment les propriétés d'un objet "plus rapide"?

J'ai cherché dans le dépôt GitHub de Bluebird pour tous les commentaires du code source ou une explication dans leur liste de problèmes, mais je n'ai pas pu en trouver.

Mise à jour 2017: d'abord, pour les lecteurs qui arrivent 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 précède est toujours valide.

Examinons d'abord ce qu'il fait et pourquoi c'est plus rapide et ensuite pourquoi ça fonctionne.

Ce qu'il fait

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

  • Mode Dictionnaire – dans lequel l'objet est stocké en tant que carte à valeurs clés comme carte hash .
  • Mode rapide – dans lequel les objets sont stockés comme des structures , dans lesquelles il n'y a aucun calcul impliqué dans l'accès à la propriété.

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

Le moteur essaie d'utiliser le mode rapide chaque fois que possible et, en général, chaque fois qu'il y a beaucoup d'accès à la propriété, mais parfois il est jeté dans le mode dictionnaire. Être en mode dictionnaire a une pénalité de grande performance, donc il est généralement souhaitable de mettre des objets en mode rapide.

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

  • Blueka Petka lui-même en parle ici .
  • Ces diapositives de Vyacheslav Egorov le mentionnent également.
  • Cette question et sa réponse acceptée sont également liées.
  • Cet article légèrement dépassé est encore une lecture assez bonne qui peut vous donner une bonne idée de la façon dont les objets sont stockés dans v8.

Pourquoi c'est plus rapide

Dans les prototypes JavaScript, les fonctions sont souvent stockées dans de nombreux cas et changent beaucoup de manière dynamique. Pour cette raison, il est très souhaitable de les faire en mode rapide pour éviter les pénalités supplémentaires chaque fois qu'une fonction est appelée.

Pour cela – v8 mettra volontiers des objets qui sont la propriété .prototype des fonctions en mode rapide car elles seront partagées 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 marche

Examinons d'abord le code et indiquons 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 ne devons pas trouver le code nous-mêmes pour affirmer que v8 fait cette optimisation, nous pouvons à la place lire les tests de l'unité 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)); 

Lire et exécuter ce test nous montre que cette optimisation fonctionne bien en v8. Cependant – il serait agréable de voir comment.

Si nous vérifions objects.cc nous 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::MigrateSlowToFast prend juste explicitement le dictionnaire et le convertit en un objet V8 rapide. C'est une lecture valable et un aperçu intéressant des objets internes de l'objet v8 – mais ce n'est pas le sujet ici. Je recommande vivement que vous le lisez ici car c'est un bon moyen d'apprendre à connaître les objets v8.

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

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

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

Faire __proto__ = ou .setPrototypeOf auraient également fonctionné, mais ce sont des fonctions ES6 et Bluebird s'exécute sur tous les navigateurs depuis Netscape 7, ce qui n'est pas posé pour simplifier le code ici. Par exemple, si nous vérifions .setPrototypeOf on peut 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, ... )); 

Alors – nous avons parcouru le chemin du code que Petka a écrit sur le métal nu. C'était sympa.

Avertissement:

Rappelez-vous que ce sont tous les détails de la mise en œuvre Les gens comme Petka sont des freaks d'optimisation. Rappelez-vous toujours que l'optimisation prématurée est la racine de tout le mal 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 ne gère pas une bibliothèque.