Why is Function.prototype.bind slow?

2019-01-13 10:47发布

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.

3条回答
你好瞎i
2楼-- · 2019-01-13 10:57

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.

查看更多
劳资没心,怎么记你
3楼-- · 2019-01-13 11:09

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.

查看更多
Juvenile、少年°
4楼-- · 2019-01-13 11:10

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.

查看更多
登录 后发表回答