【问题标题】:Why is Function.prototype.bind slow?为什么 Function.prototype.bind 慢?
【发布时间】:2011-12-28 12:49:01
【问题描述】:

this benchmark 与 chrome 16 与 Opera 11.6 进行比较时,我们发现

  • 在 chrome 中本机绑定几乎比绑定的模拟版本慢 5 倍
  • 在 Opera 中,本机绑定几乎比模拟版本的绑定快 4 倍

在这种情况下,bind 的模拟版本在哪里

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

存在这样的差异是否有充分的理由,或者这只是 v8 优化不足的问题?

注意:emulatebind 只实现了一个子集,但这并不真正相关。如果您有一个功能齐全且经过优化的模拟绑定,performance difference in the benchmark 仍然存在。

【问题讨论】:

  • @RobW 内联了我要与之比较的模拟版本的绑定。
  • 我想,这是由于代码的不同优化造成的。也许,带有本机绑定的包装器不允许某些优化。 FF10 演示了类似的行为。
  • 您的问题必须是 “为什么我的模拟 .bind() 在 Chrome、FireFox 中比本机更快,而在 Opera 和 IE 中则慢?”。为什么你认为那一定是另外一种情况?不同的代码优化。例如,您的模拟绑定不允许添加参数,而只能添加上下文。
  • @AndrewD。基本上是的。我想知道解释这种特殊差异的引擎特定优化。
  • 这是一个使用 es5-shim 版本的测试用例的补充:jsperf.com/bind-vs-emulate/6

标签: javascript google-chrome v8


【解决方案1】:

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

In the V8 source code,此检查(在boundFunction 内)显示为

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

Plaintext link to v8natives.js 表示 Google 代码搜索何时停止。)

有点令人费解的是,至少对于 Chrome 16,es5-shim 版本仍然比原生版本快。并且其他浏览器对于 es5-shim 与本机的结果相当不同。推测:可能%_IsConstructCall() 甚至比this instanceof bound 还要慢,可能是由于跨越了原生/JS 代码边界。也许其他浏览器有更快的方法来检查[[Construct]] 调用。

【讨论】:

  • added instanceof check to raynosBound。我认为开销主要是连接空数组。您是否认为值得为 args.length === 0 案例手动优化 ES5 绑定 shim 以返回仅执行 target.apply(that, arguments) 的函数?
  • 我个人认为这里的任何优化都是微优化,我会等到我的基准测试表明绑定函数超过了例如网络延迟会导致我的应用变慢,然后再考虑此类问题。
【解决方案2】:

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

特别是 15.3.4.5.1 似乎是一个可能的性能负担:简而言之,绑定函数具有不同的 [[Call]] 内部属性,因此调用它们可能会采用不寻常且可能更复杂的代码路径。

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

编辑这个答案是猜测,但我的另一个答案有更接近的证据。我仍然认为这是有效的推测,但它是一个单独的答案,所以我将保留它,只是将您推荐给另一个。

【讨论】:

    【解决方案3】:

    V8 source code for bind 是用 JS 实现的。

    OP 不模仿bind,因为它不像bind 那样对参数进行咖喱。这是一个功能齐全的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 周围的一些corner cases 参数强制。这可能是另一个开销来源。

    【讨论】:

    • 它没有完全模拟绑定。但是性能测试只使用被模拟的绑定部分的子集。你能解释一下为什么在性能测试中不使用参数柯里化会导致开销吗?你能用全功能的测试来编辑测试吗?
    • @Raynos,添加了来自v8natives.js的bind函数的JS源代码链接。
    • 嗯,为什么不var newArgs = [].slice.call( arguments ); 然后f.apply( context, curriedArgs.concat( newArgs ) );...
    • @Raynos。这个实现中有一个错误: var curriedArgs = Array.prototype.slice.call(arguments, 2);应该是 var curriedArgs = Array.prototype.slice.call(arguments, 1);
    • @ansiart,thisValue/context 是否应该是 curriedArgs 的一部分?
    猜你喜欢
    • 2013-09-24
    • 1970-01-01
    • 2021-09-03
    • 1970-01-01
    • 2014-06-15
    • 2011-03-23
    • 1970-01-01
    • 2016-09-28
    • 2020-02-08
    相关资源
    最近更新 更多