31

The benchmark:

JsPerf

The invariants:

var f = function() { };

var g = function() { return this; }

The tests:

Below in order of expected speed

  • new f;
  • g.call(Object.create(Object.prototype));
  • new (function() { })
  • (function() { return this; }).call(Object.create(Object.prototype));

Actual speed :

  1. new f;
  2. g.call(Object.create(Object.prototype));
  3. (function() { return this; }).call(Object.create(Object.prototype));
  4. new (function() { })

The question:

  1. When you swap f and g for inline anonymous functions. Why is the new (test 4.) test slower?

Update:

What specifically causes the new to be slower when f and g are inlined.

I'm interested in references to the ES5 specification or references to JagerMonkey or V8 source code. (Feel free to link JSC and Carakan source code too. Oh and the IE team can leak Chakra source if they want to).

If you link any JS engine source, please explain it.

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • 3
    Out of curiosity, what is the point of using `Object.create(Object.prototype)` rather than using an object literal `({})`? Aren't they exactly the same? Could that be the source of some performance difference? – maerics Jun 22 '11 at 14:29
  • @maerics I felt `Object.create(object.prototype)` was more in the "spirit" of `new`. It seems like `{}` is faster in Chrome, slower in FF and about the same in IE. – Raynos Jun 22 '11 at 14:38
  • Although I can kind of vaguely see why `Object.create(Object.prototype)` is more in the spirit of `new`, I'd be interested to see an example that was trying to construct more than just an empty object in order to confirm this. – Domenic Jun 22 '11 at 14:43
  • @Domenic [Benchmark for {} vs Object.create](http://jsperf.com/literal-vs-create/2) Apart from the simple case in firefox, literals are faster. – Raynos Jun 22 '11 at 14:58
  • But... what about `new`? I.e. what does the code look like to accomplish this same thing with `new`? (I can kind of guess, but having you clarify it might make it clearer where the differences lie.) – Domenic Jun 22 '11 at 16:07
  • @Domenic [Benchmark another one](http://jsperf.com/literal-vs-create/3). Seems like `new` is faster for anything non-trivial. I'm just curious what the costs are that make it slower for trivial cases. – Raynos Jun 22 '11 at 18:41
  • The last sentence about C++ is confusing and easy to to mistake for irony, a sentence where the word "not" is missing or an exaggeration. Would you prefer an explanation that includes C++ or not? – Alexander Jun 24 '11 at 21:37
  • 1
    @Alexander thanks for that. I've removed it to be more clear. – Raynos Jun 24 '11 at 21:52

5 Answers5

18

The main difference between #4 and all other cases is that the first time when you use a closure as a constructor is always quite expensive.

  1. It is always handled in V8 runtime (not in generated code) and transition between compiled JS code and C++ runtime is quite expensive. Subsequent allocations usually are handled in generated code. You can take a look at Generate_JSConstructStubHelper in builtins-ia32.cc and notice that is falls through to the Runtime_NewObject when closure has no initial map. (see http://code.google.com/p/v8/source/browse/trunk/src/ia32/builtins-ia32.cc#138)

  2. When closure is used as a constructor for the first time V8 has to create a new map (aka hidden class) and assign it as an initial map for that closure. See http://code.google.com/p/v8/source/browse/trunk/src/heap.cc#3266. Important thing here is that maps are allocated in a separate memory space. This space can't be cleaned by a fast partial scavenge collector. When map space overflows V8 has to perform relatively expensive full mark-sweep GC.

There are couple of other things happening when you use closure as a constructor for the first time but 1 and 2 are the main contributors to the slowness of test case #4.

If we compare expressions #1 and #4 then differences are:

  • #1 does not allocate a new closure every time;
  • #1 does not enter runtime every time: after closure gets initial map construction is handled in the fast path of generated code. Handling the whole construction in generated code is much faster then going back and forth between runtime and generated code;
  • #1 does not allocate a new initial map for each new closure every time;
  • #1 does not cause a mark-sweeps by overflowing map space (only cheap scavenges).

If we compare #3 and #4 then differences are:

  • #3 does not allocate a new initial map for each new closure every time;
  • #3 does not cause a mark-sweeps by overflowing map space (only cheap scavenges);
  • #4 does less on JS side (no Function.prototype.call, no Object.create, no Object.prototype lookup etc) more on C++ side (#3 also enters runtime every time you do Object.create but does very little there).

Bottom line here is that the first time you use closure as a constructor is expensive compared to subsequent construction calls of the same closure because V8 has to setup some plumbing. If we immediately discard the closure we basically throw away all the work V8 has done to speedup subsequent constructor calls.

trincot
  • 317,000
  • 35
  • 244
  • 286
Vyacheslav Egorov
  • 10,302
  • 2
  • 43
  • 45
5

The problem is that you can inspect the current source code of various engines, but it won't help you much. Don't try to outsmart the compiler. They'll try to optimize for the most common usage anyway. I don't think (function() { return this; }).call(Object.create(Object.prototype)) called 1,000 times has a real use-case at all.

"Programs should be written for people to read, and only incidentally for machines to execute."

Abelson & Sussman, SICP, preface to the first edition

Community
  • 1
  • 1
gblazex
  • 49,155
  • 12
  • 98
  • 91
  • I know `new` is faster then `Object.create` in almost all cases. I'm curious why it's not for that particular case. I'm not suggestion I'll will use new any more or less dependant on the answer. I write readable maintainable code. – Raynos Jun 24 '11 at 21:04
  • Yes but if you don't have a real use-case or a performance problem, what gives? I mean you shouldn't create 1,000 function on-the-fly with `new (function(){})` either. Comparing usable patterns with unusable ones has no real benefit. – gblazex Jun 24 '11 at 21:15
  • 2
    Curiosity always get's the better of me. – Raynos Jun 24 '11 at 21:52
3

I guess the following expansions explain what is going on in V8:

  1. t(exp1) : t(Object Creation)
  2. t(exp2) : t(Object Creation by Object.create())
  3. t(exp3) : t(Object Creation by Object.create()) + t(Function Object Creation)
  4. t(exp4) : t(Object Creation) + t(Function Object Creation) + t(Class Object Creation)[In Chrome]

    • For hidden Classes in Chrome look at : http://code.google.com/apis/v8/design.html.
    • When a new Object is created by Object.create no new Class object has to be created. There is already one which is used for Object literals and no need for a new Class.
Raynos
  • 166,823
  • 56
  • 351
  • 396
salman.mirghasemi
  • 1,009
  • 2
  • 7
  • 15
  • There is no `Class` in JavaScript. I understand all that logic. The only thing that confuses me is given that t(exp1) < t(exp2) why is t(exp4) NOT < t(exp3) – Raynos Jun 24 '11 at 10:03
  • Chrome makes Classes (hidden to the user, in their JS engine), I refer you to their documents. I updated my answer. – salman.mirghasemi Jun 24 '11 at 11:08
  • @salman.mirghasemi your going to have to back up `Class Object creation` with snippets from the V8 source. Your also going to have to backup why `new` has Class object creation and `Object.create()` doesn't. Your also going to have to backup why `new` is slower in _this_ specific case (3 & 4) compared to _every_ other case. – Raynos Jun 24 '11 at 11:12
  • For hidden Classes in Chrome look at : http://code.google.com/apis/v8/design.html. When a new Object is created by Object.create no new Class object has to be created. There is already one which is used for Object literals and no need for a new Class. – salman.mirghasemi Jun 24 '11 at 11:27
  • @salman.mirghasemi I wanted to see some C++ source code and a detailed explanation :P – Raynos Jun 24 '11 at 14:13
  • For the list of predefined types(Classes) in v8 source code look at here:http://www.google.com/codesearch#W9JxUuHYyMg/trunk/src/objects.h&q=object%20create%20type&exact_package=http://v8.googlecode.com/svn&l=43 . If you want to know more about the theory behind this implementation you can look at this paper: http://www.brics.dk/~hosc/local/LaSC-4-3-pp243-281.pdf – salman.mirghasemi Jun 24 '11 at 22:34
  • 2
    Yes, these observations are correct. The code you are looking for is here: http://code.google.com/p/v8/source/browse/trunk/src/heap.cc#3269 another important thing to know here is that hidden classes (called maps in V8 sources) are allocated in a separate memory space. This space can't be cleaned by a partial (scavenge) collector. So when you allocate a lot of maps you start getting expensive full collections (mark-sweeps) instead of cheap scavenges. That is why there is such a big difference between t(exp1) and t(exp4). You pay a lot more for GC than in other cases. – Vyacheslav Egorov Jun 24 '11 at 23:13
  • @salman.mirghasemi I've updated the question to be a lot more specific @VyacheslavEgorov Is the speed difference because t(exp4) has no initial map and t(exp3) does? Or is the speed difference due to different GC techniques whilst the performance tester is running in a large loop? – Raynos Jun 25 '11 at 00:32
  • Creating classes is insanely cheap. – gsnedders Jun 25 '11 at 01:09
  • @gsnedders bite the bullet, write the explanation :D – Raynos Jun 25 '11 at 07:55
  • @Raynos: function gets initial map lazyly, when you first try to construct an object with the given function. So in exp3 your closures do not get initial map and in exp4 they get them (each closure get a new distinctive one). Creation of initial map is indeed pretty cheap, but overflows of map space (which happen when you execute this piece of code hundreds thousands times) cause expensive (compared to scavenges) mark sweep collections. This is where difference comes from. All other test cases never overflow map space and never cause marksweep GC. – Vyacheslav Egorov Jun 25 '11 at 11:43
  • @VyacheslavEgorov Please make that an answer so I can accept it:) – Raynos Jun 25 '11 at 12:08
  • @Raynos: I've turned my answer into answer and extended it with an additional info. – Vyacheslav Egorov Jun 25 '11 at 14:49
0

Well, those two calls don't do exactly the same thing. Consider this case:

var Thing = function () {
  this.hasMass = true;
};
Thing.prototype = { holy: 'object', batman: '!' };
Thing.prototype.constructor = Thing;

var Rock = function () {
  this.hard = 'very';
};
Rock.prototype = new Thing();
Rock.constructor = Rock;

var newRock = new Rock();
var otherRock = Object.create(Object.prototype);
Rock.call(otherRock);

newRock.hard // => 'very'
otherRock.hard // => 'very'
newRock.hasMass // => true
otherRock.hasMass // => undefined
newRock.holy // => 'object'
otherRock.holy // => undefined
newRock instanceof Thing // => true
otherRock instanceof Thing // => false

So we can see that calling Rock.call(otherRock) does not cause otherRock to inherit from the prototype. This has to account for at least some of the added slowness. Although in my tests, the new construct is almost 30 times slower, even in this simple example.

benekastah
  • 5,651
  • 1
  • 35
  • 50
0
new f;
  1. take local function 'f' (access by index in local frame) - cheap.
  2. execute bytecode BC_NEW_OBJECT (or something like that) - cheap.
  3. Execute the function - cheap here.

Now this:

g.call(Object.create(Object.prototype));
  1. Find global var Object - cheap?
  2. Find property prototype in Object - so-so
  3. Find property create in Object - so-so
  4. Find local var g; - cheap
  5. Find property call - so-so
  6. Invoke create function - so-so
  7. Invoke call function - so-so

And this:

new (function() { })
  1. create new function object (that anonymous function) - relatively expensive.
  2. execute bytecode BC_NEW_OBJECT - cheap
  3. Execute the function - cheap here.

As you see case #1 is least consuming.

c-smile
  • 26,734
  • 7
  • 59
  • 86
  • The real interesting bit is how #1 is faster then #2. But swapping f & g out for anonymous functions makes #3 faster then #4 which I don't understand. – Raynos Jun 24 '11 at 21:54
  • No surprise, `g.call(Object.create(Object.prototype));` is quite a lot of operations. Two function invocations instead of two. Two additional property lookups + global Object look up. You can try caching result of Object.prototype in local var. – c-smile Jun 24 '11 at 23:53