9

I've had a long-standing assumption that deep prototype chains resulted in performance deterioration for property accessors. I was trying to explain that on hide the getter or add in the proto Object when a quick benchmark I threw together resulted in quite the opposite outcome from what I was expecting.

What is going on here? Am I missing something obvious, or does this outright demonstrate that my (and others') assumption about the performance of property accessors on the prototype chain was wrong?

Setup

const object1 = {
  foo: 'Hello, World!',
  get bar () { return this.foo }
};

const object2 = Object.assign(Object.create({
  get bar () { return this.foo }
}), {
  foo: 'Hello, World!'
});

let result;

Test 1

(control, without prototype)

result = object1.bar;

Test 2

(experiment, with prototype)

result = object2.bar;

Outcome

Test 1 ran 92.85% slower than Test 2, which means that placing get bar () {} in the prototype chain rather than in the object's own properties results in a 14x speed increase for the property accessor. See Object.create() to understand how the layout of the object is different.

Test 1

79,323,722 ops/s ±0.34%

Test 2

1,108,762,737 ops/s ±0.15%

Tested on Windows 10 Intel i7-7700K CPU @ 4.20GHz using Google Chrome 63.0.3239.132 (Official Build) (64-bit)

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • 1
    (FWIW, the benchmarks give no difference between the two in Firefox 60. 1.8 billion ops/s vs. Chrome 64’s 60 and 890 million ops/s) – Ry- Feb 20 '18 at 20:08
  • 2
    The runtime is free to collapse the chain if it wants to. – Pointy Feb 20 '18 at 20:08
  • I can also add more tests using `class` and ES5 inheritance pattern if that will help hone in on the anomaly here. – Patrick Roberts Feb 20 '18 at 20:09
  • 1
    @Pointy sure, but that doesn't explain why it's _faster_ rather than _comparable_. – Patrick Roberts Feb 20 '18 at 20:09
  • Just dont run performance tests in js. They are usually going to confuse you :) – Jonas Wilms Feb 20 '18 at 20:11
  • @JonasW. to be fair, the results are usually what I expect them to be. This is one of the few that have outright stumped me. – Patrick Roberts Feb 20 '18 at 20:11
  • Maybe cause the second object is somewhat immutable? – Jonas Wilms Feb 20 '18 at 20:11
  • @JonasW. I just added a case with `Object.freeze()` on the normal object, `class`, and ES5 inheritance. It's definitely not about immutability, it's because of the `prototype` chain. – Patrick Roberts Feb 20 '18 at 20:21
  • 2
    Probably related: https://stackoverflow.com/questions/36338289/object-descriptor-getter-setter-performance-in-recent-chrome-v8-versions – Ry- Feb 20 '18 at 20:21
  • @Ryan Just upvoted that answer. So this benchmark is due to a V8 implementation issue with getters and setters? Seems pretty conclusive. If you want to add that as an answer, or mark as a duplicate, I'm fine with that either way. – Patrick Roberts Feb 20 '18 at 20:24
  • Yeah, it changes the object’s representation, which makes sense. See what happens when you just read `foo`: https://jsbench.me/xkjdw3pitd/2 Not sure if the link has enough detail in its answer. – Ry- Feb 20 '18 at 20:32
  • I did a fork of your test, to see how this would be affected if you wanted to write to these variables too. https://jsbench.me/24jdw3sw6i/1 – Akxe Feb 20 '18 at 20:33
  • Also the related "bug" in chromium seems to be closed and therefor fixed... – Akxe Feb 20 '18 at 20:34
  • 1
    @Akxe: The link is to a patch that caused the change in performance, not a bug report about the hit. – Ry- Feb 20 '18 at 20:36
  • @Akxe clearly it's still a problem. In Ryan's [fork](https://jsbench.me/xkjdw3pitd/2), the presence of the `bar` getter in the object's own properties causes a performance hit to accessing `foo` when it shouldn't. – Patrick Roberts Feb 20 '18 at 20:36
  • 1
    Maybe also related: https://stackoverflow.com/questions/24987896/how-does-bluebirds-util-tofastproperties-function-work – Bergi Feb 20 '18 at 20:50
  • @Bergi I'm definitely going to use that trick in one of the libraries I wrote... thank you for the link, that was extremely educational. – Patrick Roberts Feb 20 '18 at 21:01

1 Answers1

3

To my knowledge, these details only apply to the V8 engine, I am not sure how directly this maps to the Firefox's implementation.

Without the prototype, V8 is creating hidden classes to support the properties of your object. For each new property, a new hidden class is created, and then a transition from the previous hidden class to the new one is created.

However, this does not happen with prototypes, and is kind of a little known fact from the conversations I have had with regards to the topic. In other words, yes, prototypes are faster.

To optimize prototypes, V8 keeps track of their shape differently from regular transitioning objects. Instead of keeping track of the transition tree, we tailor the hidden class to the prototype object, and always keep it fast -Toon Verwaest (V8 dev)

This setup all happens during Dynamic Machine Code Generation. The difference between the two setups that you are seeing is the difference between the more complex hidden class path versus the more custom path. Or, by name, the difference between the fastPropertiesWithPrototype object and the slowProperties object, the latter of which uses dictionary mode.

Travis J
  • 81,153
  • 41
  • 202
  • 273
  • I thought that dictionary mode and the hidden class transition table were two separate mechanics. Are you saying that the instantiation of `{ foo: 'Hello, World!', get bar () { return this.foo } }` configures the object in dictionary mode _and also_ creates a second hidden class transitioning from `{ foo: 'Hello, World!' }`? I thought hidden classes were used to _avoid_ switching to dictionary mode. – Patrick Roberts Feb 20 '18 at 22:20
  • The entry in the dictionary should be the path through the hidden classes. – Travis J Feb 20 '18 at 22:21
  • Ah, I totally misunderstood that. Thank you for explaining that vital detail. One more question. This implementation only applies to V8? Or does SpiderMonkey also implement slow objects using dictionary mode and hidden class transitions? – Patrick Roberts Feb 20 '18 at 22:22
  • 1
    @PatrickRoberts - It is possible that the complex path is using fastProperties (as opposed to slowProperties), which are only marginally faster. They are still slower than the fastPropertiesWithPrototype object. I am not sure of the exact triggers for when the property store becomes a lookup as opposed to linearly accessed during creation, but from what I read it seems that creating anonymous objects does create a property store with a lookup. – Travis J Feb 20 '18 at 22:28
  • @PatrickRoberts - I will make a note regarding the implementation in my answer, as I believe this is only V8 related. Sorry, from the benchmarks I thought that was relevant, as when I used the performance examples it showed very similar results in chrome. – Travis J Feb 20 '18 at 22:30