32

When comparing this benchmark with chrome 16 vs opera 11.6 we find that

  • in chrome native bind is almost 5 times slower then an emulated version of bind
  • in opera native bind is almost 4 times faster then an emulated version of bind

Where an emulated version of bind in this case is

var emulatebind = function (f, context) {
    return function () {
        f.apply(context, arguments);
    };
};

Are there good reasons why there is such a difference or is this just a matter of v8 not optimizing enough?

Note: that emulatebind only implements a subset but that isn't really relevant. If you have a fully featured and optimised emulated bind the performance difference in the benchmark still exists.

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • @RobW inlined the emulated version of bind I'm comparing it to. – Raynos Dec 28 '11 at 12:58
  • I suppose, this is due to different optimization of code. Maybe, wrapper with native bind doesn't allow some certain optimizations. FF10 demonstrates the similar behavior. – kirilloid Dec 28 '11 at 13:24
  • 3
    Your Q. must be _"Why my emulated .bind() is faster than an native in Chrome, FireFox and slower in Opera and IE?"_. And why you think that must be otherwise? Diferent code optimization. Your emulated bind does not allow to adding parameters, but only context, for example. – Andrew D. Dec 28 '11 at 13:30
  • @AndrewD. basically yes. I want to know the engine specific optimisations that explain away this particular difference. – Raynos Dec 28 '11 at 14:20
  • Here's an addition to your test case that uses the es5-shim version: http://jsperf.com/bind-vs-emulate/6 – Domenic Jan 06 '12 at 20:39

3 Answers3

27

Based on http://jsperf.com/bind-vs-emulate/6, which adds the es5-shim version for comparison, it looks like the culprit is the extra branch and instanceof that the bound version has to perform to test if it's being called as a constructor.

Each time the bound version is run, the code that gets executed is essentially:

if (this instanceof bound) {
    // Never reached, but the `instanceof` check and branch presumably has a cost
} else {
    return target.apply(
     that,
     args.concat(slice.call(arguments))
    );

    // args is [] in your case.
    // So the cost is:
    // * Converting (empty) Arguments object to (empty) array.
    // * Concating two empty arrays.
}

In the V8 source code, this check appears (inside boundFunction) as

if (%_IsConstructCall()) {
    return %NewObjectFromBound(boundFunction);
}

(Plaintext link to v8natives.js for when Google Code Search dies.)

It is a bit puzzling that, for Chrome 16 at least, the es5-shim version is still faster than the native version. And that other browsers have rather varying results for es5-shim vs. native. Speculation: maybe %_IsConstructCall() is even slower than this instanceof bound, perhaps due to crossing native/JS code boundaries. And perhaps other browsers have a much faster way of checking for a [[Construct]] call.

Domenic
  • 110,262
  • 41
  • 219
  • 271
  • 1
    [added instanceof check to raynosBound](http://jsperf.com/bind-vs-emulate/7). I think the overhead is mainly concating empty arrays. Do you think its worthwhile to hand optimise the ES5 bind shim for the `args.length === 0` case to return a function that just does `target.apply(that, arguments)` ? – Raynos Jan 07 '12 at 03:23
  • 3
    Personally I think any optimization here is microoptimization, and I would wait until my benchmarks showed that bound functions were outpacing e.g. network latency in causing my app to be slow before considering anything of the sort. – Domenic Jan 07 '12 at 07:14
7

It's impossible to implement a fully-featured bind in ES5 alone. In particular sections 15.3.4.5.1 through 15.3.4.5.3 of the spec cannot be emulated.

15.3.4.5.1, in particular, seems like a possible performance burden: in short bound functions have different [[Call]] internal properties, so calling them is likely to take an unusual and possibly more complicated code path.

Various other specific un-emulatable features of a bound function (such as arguments/caller poisoning, and possibly the custom length independent of original signature) could possibly add overhead to each call, although I admit it's a bit unlikely. Although it looks like V8 doesn't even implement the poisoning at the moment.

EDIT this answer is speculation, but my other answer has something more approaching evidence. I still think this is valid speculation, but it's a separate answer, so I'll leave it as such and just refer you to the other one.

John Weisz
  • 30,137
  • 13
  • 89
  • 132
Domenic
  • 110,262
  • 41
  • 219
  • 271
3

The V8 source code for bind is implemented in JS.

The OP doesn't emulate bind because it doesn't curry arguments the way bind does. Here is a fully featured bind:

var emulatebind = function (f, context) {
  var curriedArgs = Array.prototype.slice.call(arguments, 2);
  return function () {
    var allArgs = curriedArgs.slice(0);
    for (var i = 0, n = arguments.length; i < n; ++i) {
      allArgs.push(arguments[i]);
    }
    return f.apply(context, allArgs);
  };
};

Obviously, a quick optimization would be to do

return f.apply(context, arguments);

instead if curriedArgs.length == 0, because otherwise you have two unnecessary array creations, and an unnecessary copy, but perhaps the native version is really implemented in JS and does not do that optimization.

Caveat: This fully featured bind does not correctly handle some corner cases around this argument coercion in strict mode. That might be another source of overhead.

Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
  • It doesn't emulate bind fully. But the performance tests only use the subset of the parts of bind that are emulated. Can you explain why the parameter currying causes an overhead when it's not used in the performance tests? Can you edit the tests with the fully featured one? – Raynos Jan 06 '12 at 19:36
  • @Raynos, added a link to the JS source code for the bind function from `v8natives.js`. – Mike Samuel Jan 06 '12 at 19:49
  • Hm, why not `var newArgs = [].slice.call( arguments );` and then `f.apply( context, curriedArgs.concat( newArgs ) );`... – Šime Vidas Jan 06 '12 at 19:52
  • You linked unit tests rather then implementation source code. Its useful to link both. – Raynos Jan 06 '12 at 19:54
  • @Raynos, the second link is to unittests. The first is to implementation : http://www.google.com/codesearch#W9JxUuHYyMg/trunk/src/v8natives.js&type=cs&l=1559 – Mike Samuel Jan 07 '12 at 01:05
  • 1
    @Raynos. There's a bug in this implementation: var curriedArgs = Array.prototype.slice.call(arguments, 2); should be var curriedArgs = Array.prototype.slice.call(arguments, 1); – ansiart Jan 13 '14 at 23:47
  • 1
    @ansiart, Should the thisValue/context be part of the curriedArgs? – Mike Samuel Jan 14 '14 at 00:13
  • I was just referring to the that one specific line in the link he posted: http://jsperf.com/bind-vs-emulate/4 – ansiart Jan 14 '14 at 00:34
  • Btw, thank you @Raynos, this is the fastest implementation I've seen to this point. – ansiart Jan 14 '14 at 00:37