为什么绑定比闭包慢?

Pau*_*aul 76 javascript performance v8 node.js

之前的一张海报在Javascript中询问了Function.bind vs Closure:如何选择?

并且部分收到了这个答案,这似乎表明bind应该比闭包更快:

范围遍历意味着,当您要获取存在于不同范围内的值(变量,对象)时,会增加额外开销(代码执行速度变慢).

使用bind,您将调用具有现有范围的函数,因此不会进行范围遍历.

两个jsperfs表明bind实际上比闭包慢得多.

这是作为对上述评论发布的

并且,我决定编写自己的jsperf

那么为什么绑定这么慢(铬+ 70%)?

由于它不是更快并且闭包可以起到相同的作用,应该避免绑定吗?

Ben*_*aum 137

Chrome 59更新:正如我在下面的答案中预测的那样,使用新的优化编译器时,bind不再慢.以下是具有详细信息的代码:https://codereview.chromium.org/2916063002/

大多数时候没关系.

除非您创建的应用程序.bind是瓶颈,否则我不会打扰.在大多数情况下,可读性比纯粹的性能重要得多.我认为使用本机.bind通常提供更易读和可维护的代码 - 这是一个很大的优点.

但是,是的,重要的.bind是- 速度较慢

是的,.bind比封闭要慢得多 - 至少在Chrome中是这样,至少在目前的实施方式中是这样v8.我个人不得不在某些时候切换Node.JS以解决性能问题(更一般地说,在性能密集的情况下,闭包有点慢).

为什么?因为.bind算法比用另一个函数包装函数并使用.call或更复杂.apply.(有趣的是,它还返回一个函数,其中toString设置为[native function]).

从规范的角度和实现的角度来看,有两种方法可以看待这种情况.让我们观察两者.

首先,让我们看一下规范中定义的绑定算法:

  1. 设Target为此值.
  2. 如果IsCallable(Target)为false,则抛出TypeError异常.
  3. 设A是按此顺序在thisArg(arg1,arg2等)之后提供的所有参数值的新(可能为空)内部列表.

...

(21.使用参数"arguments"调用F的[[DefineOwnProperty]]内部方法,PropertyDescriptor {[[Get]]:thrower,[[Set]]:thrower,[[Enumerable]]:false,[[Configurable] ]:false},和false.

(22.返回F.

看起来很复杂,不仅仅是一个包装.

其次,让我们看看它是如何在Chrome中实现的.

我们来看看FunctionBindv8(chrome JavaScript引擎)源代码:

function FunctionBind(this_arg) { // Length is 1.
  if (!IS_SPEC_FUNCTION(this)) {
    throw new $TypeError('Bind must be called on a function');
  }
  var boundFunction = function () {
    // Poison .arguments and .caller, but is otherwise not detectable.
    "use strict";
    // This function must not use any object literals (Object, Array, RegExp),
    // since the literals-array is being used to store the bound data.
    if (%_IsConstructCall()) {
      return %NewObjectFromBound(boundFunction);
    }
    var bindings = %BoundFunctionGetBindings(boundFunction);

    var argc = %_ArgumentsLength();
    if (argc == 0) {
      return %Apply(bindings[0], bindings[1], bindings, 2, bindings.length - 2);
    }
    if (bindings.length === 2) {
      return %Apply(bindings[0], bindings[1], arguments, 0, argc);
    }
    var bound_argc = bindings.length - 2;
    var argv = new InternalArray(bound_argc + argc);
    for (var i = 0; i < bound_argc; i++) {
      argv[i] = bindings[i + 2];
    }
    for (var j = 0; j < argc; j++) {
      argv[i++] = %_Arguments(j);
    }
    return %Apply(bindings[0], bindings[1], argv, 0, bound_argc + argc);
  };

  %FunctionRemovePrototype(boundFunction);
  var new_length = 0;
  if (%_ClassOf(this) == "Function") {
    // Function or FunctionProxy.
    var old_length = this.length;
    // FunctionProxies might provide a non-UInt32 value. If so, ignore it.
    if ((typeof old_length === "number") &&
        ((old_length >>> 0) === old_length)) {
      var argc = %_ArgumentsLength();
      if (argc > 0) argc--;  // Don't count the thisArg as parameter.
      new_length = old_length - argc;
      if (new_length < 0) new_length = 0;
    }
  }
  // This runtime function finds any remaining arguments on the stack,
  // so we don't pass the arguments object.
  var result = %FunctionBindArguments(boundFunction, this,
                                      this_arg, new_length);

  // We already have caller and arguments properties on functions,
  // which are non-configurable. It therefore makes no sence to
  // try to redefine these as defined by the spec. The spec says
  // that bind should make these throw a TypeError if get or set
  // is called and make them non-enumerable and non-configurable.
  // To be consistent with our normal functions we leave this as it is.
  // TODO(lrn): Do set these to be thrower.
  return result;
Run Code Online (Sandbox Code Playgroud)

我们可以在实现中看到一堆昂贵的东西.即%_IsConstructCall().这当然需要遵守规范 - 但在许多情况下,它也比简单包装慢.


另一方面,调用.bind也略有不同,规范说明"使用Function.prototype.bind创建的函数对象没有原型属性或[[Code]],[[FormalParameters]]和[[Scope]]内部属性"

  • @Paul以一些怀疑态度回答我的回答.所有这些都可能在未来版本的Chrome(/ V8)中进行优化.我很少发现自己在浏览器中避免使用`.bind`,在大多数情况下,可读和可理解的代码更为重要.至于绑定函数的速度 - [是的,绑定函数此刻将保持较慢](http://jsperf.com/bind-vs-native-bind-run),尤其是当`this`值未用于部分的.您可以从基准,规范和/或独立实现[(基准)](http://jsperf.com/bind-vs-native-bind-run)中看到这一点. (4认同)

And*_*rew 6

我只想在这里提供一点观点:

请注意,虽然bind()ing很慢,但一旦绑定就调用函数则不然!

我在Linux上的Firefox 76.0中的测试代码:

//Set it up.
q = function(r, s) {

};
r = {};
s = {};
a = [];
for (let n = 0; n < 1000000; ++n) {
  //Tried all 3 of these.
  //a.push(q);
  //a.push(q.bind(r));
  a.push(q.bind(r, s));
}

//Performance-testing.
s = performance.now();
for (let x of a) {
  x();
}
e = performance.now();
document.body.innerHTML = (e - s);
Run Code Online (Sandbox Code Playgroud)

因此,虽然 ing 确实比.bind()不绑定慢大约 2 倍(我也测试过),但上面的代码对于所有 3 种情况(绑定 0、1 或 2 个变量)花费相同的时间。


就我个人而言,我并不关心.bind()当前用例中的 ing 是否很慢,我关心的是一旦这些变量已经绑定到函数,所调用的代码的性能。