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' delete
instruction 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 .prototype
proprié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.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 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 SetPrototype
dans 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 FuntionSetPrototype
ce que nous obtenons .prototype =
.
Faire __proto__ =
ou .setPrototypeOf
aurait é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, .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 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.