为什么绑定比关闭慢?


79

先前的发帖人在Javascript中Function.bind vs Closure:如何选择?

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

范围遍历意味着,当您要获取存在于另一个范围中的值(变量,对象)时,因此会增加额外的开销(代码执行起来会变慢)。

使用bind时,您正在使用现有范围调用函数,因此不会发生范围遍历。

两个jsperfs表示bind实际上比闭包要慢得多。

这是对以上内容的评论

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

那么,为什么结合速度这么慢(铬含量超过70%)?

由于速度不是更快,并且闭包可以达到相同的目的,是否应避免绑定?


10
“应该避免绑定”-除非您一次在页面上执行数千次-否则您都不必在意。
zerkms

1
从小块组装异步复杂任务可能需要在nodejs中看起来完全一样,因为回调需要以某种方式对齐。
保罗

我猜这是因为浏览器没有花太多精力来优化它。请参阅Mozilla的代码(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…),以手动实现它。浏览器很有可能只是在内部进行,而不是快速关闭。
戴夫

1
间接函数调用(apply/call/bind)通常比直接函数调用慢得多。
乔治,

@zerkms谁能说没有做过数千次呢?由于它提供的功能,我想您可能对它的普遍性感到惊讶。
安德鲁

Answers:


142

Chrome 59更新:正如我在下面的回答中所预测的那样,使用新的优化编译器后,绑定不再变慢。这是带有详细信息的代码: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.用参数“参数”调用F的[[DefineOwnProperty]]内部方法,PropertyDescriptor {[[Get]]:thrower,[[Set]]:thrower,[[Enumerable]]:false,[[Configurable] ]:false}和false。

(22.返回F。

看起来非常复杂,不只是包装。

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

让我们检查FunctionBind一下v8(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;

在执行过程中,我们可以看到很多昂贵的东西。即%_IsConstructCall()。当然,必须遵守规范-但在许多情况下,它也比简单包装慢。


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


如果f = g.bind(stuff); f()应该比g(stuff)慢吗?我很快就能发现这一点,我很好奇,无论每次实例化该函数时,每次调用函数时是否发生相同的事情,还是取决于该函数的来源。
Paul

4
@保罗对我的回答持怀疑态度。在将来的Chrome(/ V8)版本中可能会优化所有这些功能。我很少发现自己避免.bind在浏览器中使用,可读和可理解的代码在大多数情况下更为重要。至于绑定函数的速度-是的,此刻绑定函数将保持较慢的速度,特别是当this在局部函数中未使用该值时。您可以从基准,规范和/或实现中独立地看到这一点(基准)
本杰明·格林鲍姆

我想知道是否:1)自2013年以来已经发生了任何变化(到现在已经两年了)2)由于箭头函数具有这种词法约束-箭头函数在设计上是否变慢了?
Kuba Wyrostek

1
@KubaWyrostek 1)不,2)不,由于绑定在设计上并不慢,因此实现的速度并不快。当我们看到它们时,箭头功能尚未在V8中着陆(它们先着陆然后被还原)。
本杰明·格伦鲍姆

1
将来对已经应用“绑定”的函数的调用会变慢吗?即a:function(){}。bind(this)...将来对a()的调用会比我从未绑定时慢吗?
wayofthefuture

1

我只想在这里给出一些观点:

请注意,尽管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);

因此,尽管.bind()ing确实比不绑定慢大约2倍(我也测试了),但是上述代码在所有3种情况下(绑定0、1或2个变量)花费的时间相同。


就我个人而言,我不在乎.bind()在当前使用情况下ing是否缓慢,我关心的是一旦这些变量已经绑定到函数上就调用了代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.