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

1

8

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?

Veracity answered 9/6, 2017 at 21:27 Comment(3)
For one thing, you can try measuring it but it's not clear why you think the first version should somehow perform better than the second version.Cassiodorus
Why do you believe the first version is a contender?Tague
Yes, it is a general optimisation, but whether it actually makes a difference depends a lot on the kinds of operations you're doing with the instances.Sync
F
18

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!

Faviolafavonian answered 9/6, 2017 at 23:19 Comment(6)
This seems to omit the fact that changing types can be a substantial performance hit so initializing to undefined in the constructor, at least in the case shown in this question (and in Chrome 61-ish), the constructor initialization to undefined variant is much slower than either the lazy initialization or initializing in the constructor to a value of the same type as passed to the setter.Cassiodorus
Avoiding the various kinds of performance hits that arise from changing types is precisely why it's recommended to initialize fields in the constructor. Yes, if the later type is known (and always the same), then initializing to a value of the same type would be a bit better (especially when it's a number), but initializing to undefined is perfectly fine and gets you most of the benefit. And as I said, of course initialization alone is faster when the constructor is empty; but subsequent program execution benefits from having all fields initialized, so doing that up front is worth it.Faviolafavonian
Right, what I'm whining about is not the initialization-in-constructor which is both a sensible programming practice and a reasonably well-known v8 performance practice (I think there was a Google IO talk on performance that covered it back in 2012). But setting everything to undefined seems like both a lousy programming and performance practice.Cassiodorus
It sounds like a matter of scale. If a class is only going to be used once before being dereferenced (which might itself be a problem), then this technique may do more harm than good. I wonder what the break-even metric is? How many times does the object need to change shape, functions need to be called, etc., before the explicit allocation is more of a benefit than a burden. I would guess that it's pretty low, maybe even just two function calls and one shape change, because of how small and simple an operation this.foo = undefined is.Veracity
@Faviolafavonian Considering the case of a property that will later hold an object reference, is there any (engine optimisation) difference between initialising it with null or initialising it with undefined?Sync
@Bergi: nope, initializing to null or undefined doesn't make any difference for V8.Faviolafavonian

© 2022 - 2024 — McMap. All rights reserved.