为什么Function.prototype.bind很慢?

Why is Function.prototype.bind slow?

本文关键字:很慢 bind prototype Function 为什么      更新时间:2023-09-26

当将这个基准测试与chrome 16和opera 11.6进行比较时,我们发现

  • 在chrome本地绑定几乎比模拟版本的bind
  • 慢5倍。opera原生bind中的
  • 几乎比模拟bind
  • 快4倍。

这里bind的模拟版本是

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

有很好的理由为什么会有这样的差异,或者这只是v8没有充分优化的问题?

注意:emulatebind只实现了一个子集,但这并不真正相关。如果您有一个功能齐全且经过优化的模拟绑定,那么基准测试中的性能差异仍然存在。

基于http://jsperf.com/bind-vs-emulate/6,它添加了es5-shim版本进行比较,看起来罪魁祸首是绑定版本必须执行的额外分支和instanceof,以测试它是否被作为构造函数调用。

每次绑定版本运行时,执行的代码本质上是:

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.
}

在V8源代码中,这个检查(在boundFunction中)显示为

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

(纯文本链接到v8natives.js当Google代码搜索停止时)

让人有点困惑的是,至少对于Chrome 16来说,es5-shim版本仍然比原生版本快。其他浏览器对es5-shim和native的结果也不尽相同。猜测:也许%_IsConstructCall()甚至比this instanceof bound慢,也许是由于跨越本地/JS代码边界。也许其他浏览器有更快的方法来检查[[Construct]]调用。

仅在ES5中实现功能齐全的bind是不可能的。特别是规范的15.3.4.5.1到15.3.4.5.3节不能被模拟。

特别是

15.3.4.5.1,看起来像是一个可能的性能负担:在短界函数中有不同的[[Call]]内部属性,因此调用它们可能需要一个不寻常的,可能更复杂的代码路径。

绑定函数的各种其他特定的不可仿真特性(例如arguments/caller中毒,以及可能独立于原始签名的自定义length)可能会给每个调用增加开销,尽管我承认这有点不太可能。虽然看起来V8现在甚至没有实现中毒。

EDIT这个答案是猜测,但我的另一个答案有更接近的证据。我仍然认为这是一个有效的推测,但这是一个单独的答案,所以我就把它放在这里,只是让你参考另一个

bind的V8源代码是用JS实现的。

OP不模仿bind,因为它不像bind那样curry参数。这是一个功能齐全的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);
  };
};

显然,一个快速的优化是

return f.apply(context, arguments);

代替curriedArgs.length == 0,因为否则你会有两个不必要的数组创建,和一个不必要的复制,但也许本地版本真的是在JS中实现的,并且没有做那个优化。

警告:这个功能完整的bind不能正确处理严格模式下this参数强制转换的一些极端情况。这可能是开销的另一个来源。