如果了解V8/spidermonkey/chakra的内部工作机制,是否在JavaScript中显式初始化未定义的对象成员是一种优化?

8
在JavaScript中,良好性能的普遍原则之一是避免改变对象的形状。
这让我想到,这是否真的有益呢?
class Foo {
  constructor() {
    this.bar = undefined;
  }

  baz(x) { this.bar = x; }
}

一个值得推荐的最佳实践,将比这个提供更好的性能。
class Foo {
  constructor() {
  }

  baz(x) { this.bar = x; }
}

这个说法有多少真实性?原因是什么?在各个JS引擎中,这个说法是否更或者更少正确?

1
首先,您可以尝试测量它,但不清楚为什么您认为第一个版本应该比第二个版本表现更好。 - pvg
你为什么认为第一个版本是有竞争力的? - Oliver Charlesworth
1
是的,这是一种通用的优化方式,但它是否真正起到作用取决于您对实例执行的操作类型。 - Bergi
1个回答

18

我是一名V8开发者。

是的,通常来说第一个版本是值得推荐的最佳实践。

原因并不是因为对象创建本身会更快。相反,很明显一个不执行任何操作的构造函数会比一个执行了一些操作的构造函数稍微快一点。

第一个版本被推荐的原因是它确保应用程序中所有的 Foo 对象都有相同的“形状”,而对于第二个版本,可能会发生一些对象具有 .bar 属性而其他对象没有的情况。属性有时出现有时不出现会迫使 JavaScript 引擎远离它可以使用的最快速的状态/代码路径;当存在多个这样的属性时效果会更加明显。

举个例子:

class Foo() {
  constructor() {}
  addBar(x) { this.bar = x; }
  addBaz(x) { this.baz = x; }
  addQux(x) { this.qux = x; }
}
var foo1 = new Foo(); foo1.addBar(1);
var foo2 = new Foo(); foo2.addBaz(10); foo2.addBar(2);
var foo3 = new Foo(); foo3.addQux(100); foo3.addBaz(20); foo3.addBar(3);

function hot_function(foo) {
  return foo.bar;  // [1]
}
hot_function(foo1);
hot_function(foo2);
hot_function(foo3);

在标有[1]的那一行,使用这个构造函数的版本,至少可以看到三种不同形状的对象。因此JavaScript引擎将在对象中至少找到三个不同位置的属性bar。根据它的内部实现细节,它可能需要每次搜索对象的所有属性,或者它可以缓存之前看到的对象形状,但是缓存多个比缓存一个更昂贵,并且缓存尝试会有限制。 但是,如果构造函数将所有属性初始化为undefined,则所有传入的foo对象都将具有相同的形状,并且bar属性将始终是它们的第一个属性,引擎可以使用非常快速的代码来处理这个非常简单的情况。

不仅如此,addBar()在底层执行的操作也将因为能否简单地覆盖现有属性(非常快)而有所不同,必须添加新属性(潜在更慢,可能需要分配并复制对象),或者必须在两种情况之间动态决定(最慢,当然)。

另一个影响是每个唯一的对象形状都需要一些内部元数据。因此,避免不必要的不同对象形状可以节省一些内存。

当然,对于这样一个小例子,任何影响都很小。但是一旦您有一个具有数千个对象和每个对象都有几十个属性的大型应用程序,它可能会产生真正的巨大差异。注意误导性的微基准测试!


1
这似乎忽略了更改类型可能会对性能造成重大影响的事实,因此在构造函数中初始化为undefined,至少在此问题(以及在Chrome 61左右)中所示的情况下,构造函数初始化为未定义变体比惰性初始化或在构造函数中初始化为与传递给setter相同类型的值要慢得多。 - pvg
3
在更改类型时避免各种性能问题,这正是建议在构造函数中初始化字段的原因。是的,如果后续类型已知(并且始终相同),那么初始化为相同类型的值会更好一些(特别是当它是数字时),但初始化为 undefined 完全可以,并且可以获得大部分效益。正如我所说,当构造函数为空时,仅通过初始化速度更快;但随后的程序执行受益于所有字段都已初始化,因此提前完成此操作是值得的。 - jmrk
对,我抱怨的不是在构造函数中进行初始化,这是一种明智的编程实践,也是一个相当知名的v8性能实践(我认为2012年有一次Google IO关于性能的演讲涵盖了它)。但将所有内容设置为“undefined”似乎既不好的编程实践,也不利于性能。 - pvg
这似乎是一个规模问题。如果一个类在被取消引用之前只会被使用一次(这本身可能是个问题),那么这种技术可能会带来更多的伤害而不是好处。我想知道什么是收支平衡点?对象需要改变形状的次数,函数需要被调用的次数等等,需要多少次才能使显式分配比负担更有益。我猜这个数字很低,甚至只有两个函数调用和一个形状改变,因为this.foo = undefined操作非常简单。 - M-Pixel
1
@jmrk 考虑一个将来将保存对象引用的属性的情况,用 null 还是用 undefined 来初始化它,在(引擎优化)方面有区别吗? - Bergi
1
@Bergi: 不对,对于V8来说,将变量初始化为nullundefined没有任何区别。 - jmrk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接