web-dev-qa-db-ja.com

オブジェクトの[[prototype]]を変更するとパフォーマンスが低下するのはなぜですか?

standardsetPrototypeOf function および非標準の __proto__プロパティのMDNドキュメントから

オブジェクトの[[Prototype]]を変更することは、これがどのように達成されても、非常に遅く、現代のJavaScript実装での後続の実行が避けられないほど遅くなるため、強く推奨されません。

Function.prototypeを使用してプロパティを追加することは、メンバー関数をjavascriptクラスに追加する方法です。次に、次のように:

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

foo.__proto__.bar = bar;が悪いのはなぜですか?悪いことがFoo.prototype.bar = bar;と同じくらい悪いわけではないのですか?

それからなぜこの警告:それは非常に遅く、現代のJavaScript実装での後続の実行をやむを得ず遅くします。確かにFoo.prototype.bar = bar;はそれほど悪くはありません。

Updateおそらく突然変異によって、彼らは再割り当てを意味した。受け入れられた回答を参照してください。

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

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

いいえ。両方とも(_foo.__proto__ === Foo.prototype_として)同じことを行っており、両方とも問題ありません。 Object.getPrototypeOf(foo)オブジェクトにbarプロパティを作成しているだけです。

ステートメントが参照するのは、___proto___プロパティ自体に割り当てることです。

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

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

_Object.prototype_ページ の警告はさらに詳しく説明します。

オブジェクトの[[Prototype]]の変更は、現代のJavaScriptエンジンがプロパティアクセスを最適化する方法の性質により、非常に遅い操作です。

彼らは単に、既存のオブジェクトのプロトタイプチェーンを変更すると最適化を殺すと述べています。代わりに、Object.create()を介して異なるプロトタイプチェーンを持つ新しいオブジェクトを作成することになっています。

明示的な参照は見つかりませんでしたが、 V8の隠しクラス の実装方法を検討すると、ここで何が起こるのかがわかります。オブジェクトのプロトタイプチェーンを変更すると、その内部タイプが変更されます。プロパティを追加するときのように単純にサブクラスになるのではなく、完全に交換されます。これは、すべてのプロパティルックアップの最適化がフラッシュされ、プリコンパイルされたコードを破棄する必要があることを意味します。または、最適化されていないコードに単純にフォールバックします。

いくつかの注目すべき引用符:

  • ブレンダン・エイチ(あなたは彼を知っている)が言った

    書き込み可能な__proto__は、実装するのが非常に苦痛であり(サイクルチェックにシリアライズする必要があります)、あらゆる種類のタイプの混乱の危険を生み出します。

  • Brian Hackett(Mozilla)が言った

    スクリプトがほとんどすべてのオブジェクトのプロトタイプを変更できるようにすることで、スクリプトの動作について推論することが難しくなり、VM、JIT、および分析の実装がより複雑でバグが増えます。型推論には、可変__proto__によるいくつかのバグがあり、この機能のためにいくつかの望ましい不変式を維持できません(つまり、「型セットには、var/propertyで実現できるすべての可能な型オブジェクトが含まれます」および「JSFunctionsには関数でもある型があります」 )。

  • ジェフ・ウォルデンが言った

    不安定なパフォーマンスの不安定化、およびプロキシと[[SetInheritance]]への影響を伴う、作成後のプロトタイプ突然変異

  • Erik Corry(Google)が言った

    プロトタイプを上書き不可にすることでパフォーマンスが大幅に向上することは期待していません。最適化されていないコードでは、プロトタイプオブジェクト(IDではない)が変更された場合にプロトタイプチェーンをチェックする必要があります。最適化されたコードの場合、誰かがプロトに書き込むと、最適化されていないコードにフォールバックできます。そのため、少なくともV8クランクシャフトではそれほど違いはありません。

  • Eric Faust(Mozilla)が言った

    __proto__を設定すると、そのオブジェクトに対するIonからの将来の最適化の可能性を台無しにするだけでなく、他のすべての型推論(関数の戻り値に関する情報、またはプロパティ値、おそらく)彼らはこのオブジェクトについて知っていると考えており、多くの仮定を行わないように指示します。これは、既存のjitcodeのさらなる最適化解除とおそらく無効化を伴います。
    実行の途中でオブジェクトのプロトタイプを変更するのは本当に厄介なことであり、間違いを防ぐ唯一の方法は安全にプレイすることですが、安全は遅いです。

55
Bergi

__proto__/setPrototypeOfは、オブジェクトプロトタイプへの割り当てと同じではありません。たとえば、メンバーが割り当てられた関数/オブジェクトがある場合:

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

誰もがプロトタイプにのみ焦点を合わせているようであり、関数がメンバーを割り当てて、突然変異後にインスタンス化できることを忘れています。現在、__proto__/setPrototypeOfを使用せずにこれを行う他の方法はありません。ほとんどの場合、親コンストラクター関数から継承する機能を持たないコンストラクターを使用し、Object.createは機能しません。

さらに、これは2つのObject.create呼び出しであり、現時点ではV8(ブラウザーとノードの両方)で非常に遅いため、__proto__がより実行可能な選択肢になります。

2
pocesar

はい。prototype=は同じくらい悪いので、「どのように達成されても」という言葉遣いです。 prototypeは、クラスレベルで機能を拡張するための擬似オブジェクトです。その動的な性質により、スクリプトの実行が遅くなります。一方、インスタンスレベルで関数を追加すると、オーバーヘッドがはるかに少なくなります。

1
Schien

これは、ノード_v6.11.1_を使用したベンチマークです

NormalClass:プロトタイプが編集されていない通常のクラス

PrototypeEdited:プロトタイプが編集されたクラス(test()関数が追加されます)

PrototypeReference:外部変数を参照するプロトタイプ関数test()が追加されたクラス

結果:

_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)
_

ご覧のとおり、プロトタイプ編集クラスは通常のクラスよりも高速です。外部のものを参照する変数を持つプロトタイプは最も遅いですが、それは既にインスタンス化された変数を持つプロトタイプを編集する興味深い方法です

ソース:

_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