51

Maybe not because the call is slow, but rather the lookup is; I'm not sure, but here is an example:

var foo = {};
foo.fn = function() {};

var bar = {};
bar.fn = function() {};

console.time('t');

for (var i = 0; i < 100000000; i++) {
    foo.fn();
}

console.timeEnd('t');

Tested on win8.1

  • firefox 35.01: ~240ms
  • chrome 40.0.2214.93 (V8 3.30.33.15): ~760ms
  • msie 11: 34 sec
  • nodejs 0.10.21 (V8 3.14.5.9): ~100ms
  • iojs 1.0.4 (V8 4.1.0.12): ~760ms

Now here is the interesting part, if i change bar.fn to bar.somethingelse:

  • chrome 40.0.2214.93 (V8 3.30.33.15): ~100ms
  • nodejs 0.10.21 (V8 3.14.5.9): ~100ms
  • iojs 1.0.4 (V8 4.1.0.12): ~100ms

Something went wrong in v8 lately? What causes this?

Totoro
  • 3,398
  • 1
  • 24
  • 39
Gábor Bokodi
  • 525
  • 1
  • 5
  • 10
  • I get the same results in Chrome. Didn't test in IE, but did it really take 34 seconds ? – adeneo Jan 28 '15 at 19:55
  • 17
    yes, but the first time it crashed. ie is adorable :) – Gábor Bokodi Jan 28 '15 at 19:57
  • It's also strange that io.js is so much slower than node.js using a newer version of the engine, and that chrome is so much slower than node.js using almost the same engine. – adeneo Jan 28 '15 at 19:58
  • 1
    My guess (totally arbitrary) is that this has to do with having two dynamic(ish) objects in the same scope with similar structures, but that aren't guaranteed to be the same, causing problems with [the caches used for monomorphism](http://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html) and causing one lookup to be deoptimized. That's totally off the wall, though. Very interested in the actual answer. – ssube Jan 28 '15 at 20:08
  • Doesn't matter how much it's nested either, `foo.one.fn` and `bar.two.fn` causes the same lag – adeneo Jan 28 '15 at 20:12
  • I think it's the implentation of V8's [hidden classes](https://developers.google.com/v8/design) in C++ that causes this issue, where the class diverges somehow when using the same property name in the same chain etc. – adeneo Jan 28 '15 at 20:16
  • hidden classes was my first thought, but what about V8 3.14.5.9? Hidden classes were there too. Also, if you set `bar.fn = foo.fn`, it's ~100ms again. – Gábor Bokodi Jan 28 '15 at 20:21
  • That's the strange part, how it differs between versions and implementations of V8, and the small changes in the code? Node.js doesn't seem to be affected, while io.js is? I think only someone intimately familiar with the V8 source knows this. Maybe retagging with `C++` gets you somewhere, or even `C#`, some of those guys work at Google, and probably knows this (where is Jon Skeet when you need him)! – adeneo Jan 28 '15 at 20:33

2 Answers2

65

First fundamentals.

V8 uses hidden classes connected with transitions to discover static structure in the fluffy shapeless JavaScript objects.

Hidden classes describe the structure of the object, transitions link hidden classes together describing which hidden class should be used if a certain action is performed on an object.

For example the code below would lead to the following chain of hidden classes:

var o1 = {};
o1.x = 0;
o1.y = 1;
var o2 = {};
o2.x = 0;
o2.y = 0;

enter image description here

This chain is created as you construct o1. When o2 is constructed V8 simply follows established transitions.

Now when a property fn is used to store a function V8 tries to give this property a special treatment: instead of just declaring in the hidden class that object contains a property fn V8 puts function into the hidden class.

var o = {};
o.fn = function fff() { };

enter image description here

Now there is an interesting consequence here: if you store different functions into the field with the same name V8 can no longer simply follow the transitions because the value of the function property does not match expected value:

var o1 = {};
o1.fn = function fff() { };
var o2 = {};
o2.fn = function ggg() { };

When evaluating o2.fn = ... assignment V8 will see that there is a transition labeled fn but it leads to a hidden class that does not suitable: it contains fff in fn property, while we are trying to store ggg. Note: I have given function names only for simplicity - V8 does not internally use their names but their identity.

Because V8 is unable to follow this transition V8 will decide that its decision to promote function to the hidden class was incorrect and wasteful. The picture will change

enter image description here

V8 will create a new hidden class where fn is just a simple property and not a constant function property anymore. It will reroute the transition and also mark old transition target deprecated. Remember that o1 is still using it. However next time code touches o1 e.g. when a property is loaded from it - runtime will migrate o1 off the deprecated hidden class. This is done to reduce polymorphism - we don't want o1 and o2 to have different hidden classes.

Why is it important to have functions on the hidden classes? Because this gives V8's optimizing compiler information it uses to inline method calls. It can only inline method call if call target is stored on the hidden class itself.

Now lets apply this knowledge to the example above.

Because there is a clash between transitions bar.fn and foo.fn become normal properties - with functions stored directly on those objects and V8 can't inline the call of foo.fn leading to a slower performance.

Could it inline the call before? Yes. Here is what changed: in older V8 there was no deprecation mechanism so even after we had a clash and rerouted fn transition, foo was not migrated to the hidden class where fn becomes a normal property. Instead foo still kept the hidden class where fn is a constant function property directly embedded into the hidden class allowing optimizing compiler to inline it.

If you try timing bar.fn on the older node you will see that it is slower:

for (var i = 0; i < 100000000; i++) {
    bar.fn();  // can't inline here
}       

precisely because it uses hidden class that does not allow optimizing compiler to inline bar.fn call.

Now the last thing to notice here is that this benchmark does not measure the performance of a function call, but rather it measures if optimizing compiler can reduce this loop to an empty loop by inlining the call inside it.

Vyacheslav Egorov
  • 10,302
  • 2
  • 43
  • 45
  • 5
    @Esailija I don't think you got anything wrong - on the high level your answer is accurate. When I saw you answered I wanted to stop typing - but I was already in the middle, so I decided it'd be a waste if I stop. (if we dig deeper into time: it used to be the case that V8 did not attach transitions to the root `Object` hidden class and both `foo.fn` and `bar.fn` would fast) – Vyacheslav Egorov Jan 28 '15 at 21:37
  • Would a polymorphism solution be able to inline the calls on multiple (both) classes, or would it not dare to inline them at all? – Bergi Jan 31 '15 at 15:54
  • Does this re-transition and class deprecation also happen if those functions are closures with the same source? – Bergi Jan 31 '15 at 15:57
  • 2
    @Bergi 1) it would inline if it can determine a target from the hidden class itself - in the examples above there are no polymorphic calls though, so I am not sure which particular code snippet you are referring to. 2) Yes, it does - as noted about function identity is used (think `===`) - this is somewhat a drawback of the current design. Ideally it shouldn't. – Vyacheslav Egorov Feb 01 '15 at 14:38
  • 2
    I live your sketches too... any chance this is from an app/website that others can use? – scunliffe Feb 03 '15 at 23:43
  • 2
    @scunliffe essentially it's the same library that I described in http://mrale.ph/blog/2012/11/25/shaky-diagramming.html – Vyacheslav Egorov Feb 04 '15 at 09:55
29

Object literals share hidden class ("map" in v8 internal terms) by structure I.E. same named keys in same order, while objects created from different constructors would have different hidden class even if the constructors initialized them to exactly the same fields.

When generating code for foo.fn(), in the compiler you don't generally have access to the specific foo object but only its hidden class. From the hidden class you could access the fn function but because the shared hidden class can actually have different function at the fn property, that is not possible. So because you don't know at compile time which function will be called, you cannot inline the call.

If you run the code with trace inlining flag:

$ /c/etc/iojs.exe --trace-inlining test.js
t: 651ms

However if you change anything so that either .fn is always the same function or foo and bar have different hidden class:

$ /c/etc/iojs.exe --trace-inlining test.js
Inlined foo.fn called from .
t: 88ms

(I did this by doing bar.asd = 3 before the bar.fn-assignment, but there are a ton of different ways to achieve it, such as constructors and prototypes which you surely know are the way to go for high performance javascript)

To see what changed between versions, run this code:

var foo = {};
foo.fn = function() {};

var bar = {};
bar.fn = function() {};

foo.fn();
console.log("foo and bare share hidden class: ", %HaveSameMap(foo, bar));

As you can see the results differs between node10 and iojs:

$ /c/etc/iojs.exe --allow-natives-syntax test.js
foo and bare share hidden class:  true

$ node --allow-natives-syntax test.js
foo and bare share hidden class:  false

I haven't followed development of v8 in details recently so I couldn't point the exact reason but these kinds of heuristics change all the time in general.

IE11 is closed source but from everything they have documented it actually seems like it's very similar to v8.

Esailija
  • 138,174
  • 23
  • 272
  • 326