Reputation: 169401
When comparing this benchmark with chrome 16 vs opera 11.6 we find that
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.
Upvotes: 32
Views: 5065
Reputation: 112827
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.
Upvotes: 7
Reputation: 112827
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.
Upvotes: 27
Reputation: 120516
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.
Upvotes: 3