web-dev-qa-db-fra.com

Pourquoi la mutation du [[prototype]] d'un objet est-elle mauvaise pour la performance?

Dans les documents MDN pour la fonction standardsetPrototypeOf ainsi que la propriété non standard __proto__ :

La mutation du [[prototype]] d'un objet, quel que soit le moyen utilisé, est fortement déconseillée car elle est très lente et ralentit inévitablement l'exécution ultérieure dans les implémentations JavaScript modernes.

Utiliser Function.prototype pour ajouter des propriétés est le le moyen d'ajouter des fonctions membres aux classes javascript. Puis comme le montre le suivant: 

function Foo(){}
function bar(){}

var foo = new Foo();

// This is bad: 
//foo.__proto__.bar = bar;

// But this is okay
Foo.prototype.bar = bar;

// Both cause this to be true: 
console.log(foo.__proto__.bar == bar); // true

Pourquoi foo.__proto__.bar = bar; est-il mauvais? Si c'est mauvais, Foo.prototype.bar = bar; n'est pas aussi mauvais? 

Alors pourquoi cet avertissement: il est très lent et ralentit inévitablement l'exécution ultérieure dans les implémentations JavaScript modernes. Foo.prototype.bar = bar; n'est sûrement pas si mal.

Mise à jour Peut-être par mutation, ils signifiaient une réaffectation. Voir la réponse acceptée. 

50
basarat
// This is bad: 
//foo.__proto__.bar = bar;

// But this is okay
Foo.prototype.bar = bar;

Non, les deux font la même chose (en tant que foo.__proto__ === Foo.prototype), et les deux vont bien. Ils créent juste une propriété bar sur l'objet Object.getPrototypeOf(foo).

La déclaration fait référence à l'affectation à la propriété __proto__ elle-même:

function Employee() {}
var fred = new Employee();

// Assign a new object to __proto__
fred.__proto__ = Object.prototype;
// Or equally:
Object.setPrototypeOf(fred, Object.prototype);

L’avertissement à la page Object.prototype va plus en détail:

La mutation du [[prototype]] d'un objet est, de par la nature de la façon dont les moteurs JavaScript modernes optimisent les accès aux propriétés}, une opération très lente

Ils indiquent simplement que changer la chaîne de prototype d'un objet déjà existant supprime les optimisations. Au lieu de cela, vous êtes censé créer un nouvel objet avec une chaîne de prototypes différente via Object.create().

Je ne pouvais pas trouver de référence explicite, mais si nous considérons comment les classes cachées de V8 sont implémentées, nous pouvons voir ce qui pourrait se passer ici. Lors du changement de la chaîne de prototypes d'un objet, son type interne change - il ne devient pas simplement une sous-classe comme lors de l'ajout d'une propriété, mais est complètement échangé. Cela signifie que toutes les optimisations de recherche de propriétés sont vidées et que le code précompilé devra être supprimé. Ou bien, il s'agit simplement d'un code non optimisé.

Quelques citations notables:

  • Brendan Eich (vous le connaissez) a dit

    L’inscription __proto__ est une tâche gigantesque à mettre en œuvre (elle doit être sérialisée pour un cycle de contrôle) et crée toutes sortes de risques de confusion de type.

  • Brian Hackett (Mozilla) a dit :

    Le fait de permettre aux scripts de transformer le prototype de pratiquement n'importe quel objet complique la tâche de raisonner sur le comportement d'un script et rend la mise en œuvre de VM, JIT et d'analyse plus complexe et complexe. L'inférence de type a eu plusieurs bogues dus à __proto__ mutable et ne peut pas maintenir plusieurs invariants souhaitables à cause de cette caractéristique (c'est-à-dire que "les jeux de types contiennent tous les objets de type possibles pouvant être réalisés pour une var/property" et "JSFunctions possède des types qui sont aussi des fonctions" ).

  • Jeff Walden a dit :

    Mutation prototype après la création, avec sa déstabilisation erratique des performances et son impact sur les mandataires et [[SetInheritance]]

  • Erik Corry (Google) a déclaré :

    Je ne m'attends pas à de gros gains de performances en rendant proto non écrasable. Dans le code non optimisé, vous devez vérifier la chaîne de prototypes si les objets de prototypes (et non leur identité) ont été modifiés. Dans le cas d'un code optimisé, vous pouvez utiliser un code non optimisé si quelqu'un écrit sur proto. Donc, cela ne ferait pas tellement de différence, du moins dans V8-Crankshaft.

  • Eric Faust (Mozilla) a déclaré

    Lorsque vous définissez __proto__, non seulement vous réduisez les chances d'optimisations futures d'Ion sur cet objet, mais vous forcez également le moteur à explorer tous les autres éléments d'inférence de type (informations sur les valeurs de retour de fonction, ou valeurs de propriété, peut-être) qui pensent connaître cet objet et leur dire de ne pas faire beaucoup d’hypothèses non plus, ce qui implique une déoptimisation plus poussée et peut-être une invalidation du jitcode existant.
    Changer le prototype d’un objet en cours d’exécution est vraiment un vilain marteau, et la seule façon de ne pas se tromper est de jouer prudemment, mais la sécurité est lente.

53
Bergi

__proto__/setPrototypeOf ne correspond pas à l'attribution au prototype d'objet. Par exemple, lorsque vous avez une fonction/un objet auquel des membres sont affectés:

function Constructor(){
    if (!(this instanceof Constructor)){
        return new Constructor();
    } 
}

Constructor.data = 1;

Constructor.staticMember = function(){
    return this.data;
}

Constructor.prototype.instanceMember = function(){
    return this.constructor.data;
}

Constructor.prototype.constructor = Constructor;

// By doing the following, you are almost doing the same as assigning to 
// __proto__, but actually not the same :P
var newObj = Object.create(Constructor);// BUT newObj is now an object and not a 
// function like !!!Constructor!!! 
// (typeof newObj === 'object' !== typeof Constructor === 'function'), and you 
// lost the ability to instantiate it, "new newObj" returns not a constructor, 
// you have .prototype but can't use it. 
newObj = Object.create(Constructor.prototype); 
// now you have access to newObj.instanceMember 
// but staticMember is not available. newObj instanceof Constructor is true

// we can use a function like the original constructor to retain 
// functionality, like self invoking it newObj(), accessing static 
// members, etc, which isn't possible with Object.create
var newObj = function(){
    if (!(this instanceof newObj)){   
        return new newObj();
    }
}; 
newObj.__proto__ = Constructor;
newObj.prototype.__proto__ = Constructor.prototype;
newObj.data = 2;

(new newObj()).instanceMember(); //2
newObj().instanceMember(); // 2
newObj.staticMember(); // 2
newObj() instanceof Constructor; // is true
Constructor.staticMember(); // 1

Tout le monde semble ne se concentrer que sur le prototype et oublier que les fonctions peuvent avoir des membres assignés et instanciées après mutation. Il n'y a actuellement aucun autre moyen de le faire sans utiliser __proto__/setPrototypeOf. Presque personne n'utilise un constructeur sans la possibilité d'hériter d'une fonction constructeur du parent, et Object.create ne parvient pas à servir.

De plus, il s'agit de deux appels Object.create qui, pour le moment, sont impuissants dans V8 (navigateur et noeud), ce qui fait de __proto__ un choix plus viable.

2
pocesar

Oui .prototype = est tout aussi mauvais, d’où le libellé "peu importe la façon dont il est accompli". prototype est un pseudo objet permettant d'étendre la fonctionnalité au niveau de la classe. Sa nature dynamique ralentit l'exécution du script. En revanche, l’ajout d’une fonction au niveau instance entraîne moins de frais généraux.

1
Schien

Voici un point de repère utilisant le noeud v6.11.1

NormalClass : Une classe normale, avec le prototype non édité

PrototypeEdited : Une classe avec le prototype édité (la fonction test() est ajoutée)

PrototypeReference : Une classe avec la fonction prototype ajoutée test() qui fait référence à une variable externe

Résultats :

NormalClass x 71,743,432 ops/sec ±2.28% (75 runs sampled)
PrototypeEdited x 73,433,637 ops/sec ±1.44% (75 runs sampled)
PrototypeReference x 71,337,583 ops/sec ±1.91% (74 runs sampled)

Comme vous pouvez le constater, la classe de prototype modifiée est un moyen plus rapide que la classe normale. Le prototype qui a une variable qui fait référence à une variable externe est la plus lente, mais c'est un moyen intéressant d'éditer des prototypes avec une variable déjà instanciée.

La source : 

const Benchmark = require('benchmark')
class NormalClass {
  constructor () {
    this.cat = 0
  }
  test () {
    this.cat = 1
  }
}
class PrototypeEdited {
  constructor () {
    this.cat = 0
  }
}
PrototypeEdited.prototype.test = function () {
  this.cat = 0
}

class PrototypeReference {
  constructor () {
    this.cat = 0
  }
}
var catRef = 5
PrototypeReference.prototype.test = function () {
  this.cat = catRef
}
function normalClass () {
  var tmp = new NormalClass()
  tmp.test()
}
function prototypeEdited () {
  var tmp = new PrototypeEdited()
  tmp.test()
}
function prototypeReference () {
  var tmp = new PrototypeReference()
  tmp.test()
}
var suite = new Benchmark.Suite()
suite.add('NormalClass', normalClass)
.add('PrototypeEdited', prototypeEdited)
.add('PrototypeReference', prototypeReference)
.on('cycle', function (event) {
  console.log(String(event.target))
})
.run()
0