【问题标题】:Why is mutating the [[prototype]] of an object bad for performance?为什么改变对象的 [[prototype]] 对性能不利?
【发布时间】:2014-07-11 13:10:13
【问题描述】:

来自标准 setPrototypeOf function 以及非标准 __proto__ property 的 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; 肯定没那么糟糕。

更新也许他们所说的突变意味着重新分配。请参阅已接受的答案。

【问题讨论】:

  • __proto__ 是一个 deprecated 非标准属性 .. 所以顺便说一句,很高兴看到直接解决性能问题的答案:为什么“..在现代 JavaScript 实现中非常缓慢且不可避免地会减慢后续执行”?
  • @user2864740 谢谢。那是我的意图。通过提及新的 standard 方式更新了问题以更加明确。
  • @basarat 我想他们都有同样的问题。似乎 JS 引擎需要为所有链接(派生)对象“刷新”任何缓存的属性解析或其他编译/中间 IL。
  • @basarat 也许吧。虽然我不同意这个答案,因为它似乎避开了与显式突变相关的任何问题(如图所示,没有__proto__ 的人可以突变)并暗示没有发生这样的优化(这会对性能产生任何影响)。突变不存在)。
  • 我找到了我正在寻找的问题和相应的答案:Should I put default values of attributes on the prototype to save space? 虽然不完全相关,但我认为这是您不应该这样做的原因之一。

标签: javascript performance prototype prototype-chain


【解决方案1】:
// 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 page 的警告更详细:

根据现代 JavaScript 引擎如何优化属性访问的本质,改变对象的 [[Prototype]] 是一个非常缓慢的操作

他们只是声明更改现有对象的原型链会扼杀优化。相反,您应该通过Object.create() 创建一个具有不同原型链的新对象。

我找不到明确的参考,但如果我们考虑V8's hidden classes 是如何实现的,我们可以看到这里可能会发生什么。当改变一个对象的原型链时,它的内部类型也会改变——它不像添加属性那样简单地变成一个子类,而是完全交换了。这意味着所有属性查找优化都被刷新,并且需要丢弃预编译的代码。或者它只是退回到未优化的代码。

一些值得注意的引语:

  • Brendan Eich (you know him) said

    可写的 __proto__ 实现起来非常痛苦(必须序列化以进行循环检查),并且会产生各种类型混淆的危险。

  • Brian Hackett (Mozilla) said:

    允许脚本对几乎任何对象的原型进行变异会使得对脚本行为的推理变得更加困难,并使 VM、JIT 和分析实现更加复杂和错误。由于可变 __proto__ 导致类型推断有几个错误,并且由于此特性而无法维护几个理想的不变量(即“类型集包含可以为 var/property 实现的所有可能的类型对象”和“JSFunctions 的类型也是函数” )。

  • Jeff Walden said:

    创建后的原型突变,不稳定的性能不稳定,以及对代理和[[SetInheritance]]的影响

  • Erik Corry (Google) said:

    我不希望通过使 proto 不可覆盖来获得巨大的性能提升。在未优化的代码中,您必须检查原型链,以防原型对象(而不是它们的身份)已更改。在优化代码的情况下,如果有人写入 proto,您可以回退到未优化的代码。所以它不会有太大的不同,至少在 V8-Crankshaft 中是这样。

  • Eric Faust (Mozilla) said

    当您设置 __proto__ 时,您不仅会破坏您在 Ion 对该对象进行未来优化的任何机会,而且还会迫使引擎爬到所有其他类型推断(有关函数的信息返回值或属性值,可能)认为他们知道这个对象,并告诉他们也不要做太多假设,这涉及进一步的去优化和现有 jitcode 的可能失效。
    在执行过程中更改对象的原型确实是一个令人讨厌的大锤,我们必须避免出错的唯一方法是安全行事,但安全是缓慢的。

【讨论】:

  • 我想我们都读过 OP 链接到的页面。这些特定的优化是什么
  • 据此,mutating 他们的意思是 reassigning。在这种情况下,使用fred = Object.create(Object.prototype) 应该同样糟糕。但他们特别说它的 goodInstead, create the object with the desired [[Prototype]] using Object.create.。我认为你是对的。他们可能已经优化了Object.create
  • object.create 和 proto 之间存在显着的性能差异:jsperf.com/proto-vs-object-create2 感谢您的宝贵时间
  • @BT 谢谢,已修复
  • @OliverSieweke 虽然我没有任何真正的洞察力,但我预计不会有任何问题,并认为const child = Object.setPrototypeOf({ method() { super.method() } }, parent) 模式很好。引擎应该能够优化这一点,如果他们不这样做,我会提出功能请求。正如您所说,这是使方法在对象文字中工作的唯一方法,对于具有自定义原型链的数组或函数也是必要的。只需在这些情况下使用它。
【解决方案2】:

__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 无法提供服务。

另外,这是两个 Object.create 调用,目前在 V8(浏览器和节点)中速度非常慢,这使得 __proto__ 成为更可行的选择

【讨论】:

    【解决方案3】:

    是的 .prototype= 一样糟糕,因此措辞“不管它是如何完成的”。原型是用于在类级别扩展功能的伪对象。它的动态特性减慢了脚本的执行速度。另一方面,在实例级别添加函数会产生更少的开销。

    【讨论】:

    • Adding a function on the instance level...incurs far less overhead. - 直到你有很多实例。
    • 需要更多上下文。根据我对链接资源的理解,它与 [prototype] 对象的 mutation 密切相关。因此,分配给Fn.prototype 并没有“同样糟糕”,因为它是在创建时复制的。 (问题的重点是 mutating 原型对象。)
    【解决方案4】:

    这是一个使用节点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()
    

    【讨论】:

    • 这些示例都不涉及更改任何对象的 [[prototype]] 槽(通过写信给.__proto__ 或调用Object.setProtottypeOf(),因此基准测试虽然很有趣,但与提出的问题无关.
    猜你喜欢
    • 1970-01-01
    • 2021-09-29
    • 2012-03-14
    • 2015-12-15
    • 1970-01-01
    • 2021-04-18
    • 1970-01-01
    • 2016-08-20
    • 2020-10-26
    相关资源
    最近更新 更多