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.
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
}
Voyons d'abord ce qu'il fait et pourquoi c'est plus rapide, puis pourquoi cela fonctionne.
Le moteur V8 utilise deux représentations d'objets:
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.
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.
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.
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.