Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it an optimization to explicitly initialize undefined object members in JavaScript, given knowledge of the innerworkings of V8/spidermonkey/chakra?

In JavaScript, a commonly touted principle for good performance is to avoid changing the shape of an object.

This makes me wonder, is this

class Foo {
  constructor() {
    this.bar = undefined;
  }

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

a worthwhile best-practice that will give better performance than this

class Foo {
  constructor() {
  }

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

How true or false is this? Why? And is it any more or less true in one JS engine over the others?

like image 294
M-Pixel Avatar asked Jun 09 '17 21:06

M-Pixel


1 Answers

V8 developer here.

Yes, the first version is, in general, a worthwhile best practice.

The reason for that is not that object creation itself would be faster. On the contrary, it's pretty obvious that a constructor that doesn't do any work is going to be at least a little bit faster than a constructor that does do some work.

The reason the first version is recommended is because it ensures that all Foo objects in the application will have the same "shape", whereas with the second version, it could happen that some of them have a .bar property and others don't. Properties that are sometimes present and sometimes not tend to force the JavaScript engine away from the fastest possible states/code paths it can use; the effects will be much bigger when there is more than one such property.

As an example:

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

At the line marked [1], with this version of the constructor, objects of at least three different shapes are seen. So the JavaScript engine will find property bar in at least three different places within the object. Depending on its internal implementation details, it might have to search all the object's properties every time, or maybe it can cache object shapes it has seen before, but caching several is more expensive than caching one, and there will be limits to the caching attempts. However, if the constructor had initialized all properties to undefined, then all incoming foo objects here would have the same shape, and the bar property would always be their first property, and the engine could use really fast code to handle this very simple case.

It's not just such loads: also what addBar() does under the hood will be different depending on whether it can simply overwrite an existing property (very fast), has to add a new property (potentially much slower, might require allocation and copying the object around), or must dynamically decide between both cases (slowest, of course).

Another effect is that each unique object shape is going to require some amount of internal metadata. So avoiding unnecessarily distinct object shapes will save some memory.

Of course for such a small example, any effect will be small. But once you have a big app with thousands of objects with dozens of properties each, it can make a really big difference. Beware of misleading microbenchmarks!

like image 131
jmrk Avatar answered Nov 17 '22 23:11

jmrk