为什么绑定比闭包慢?

IT技术 javascript node.js performance v8
2021-02-02 13:06:53

之前有张贴问过Javascript中的Function.bind vs Closure:如何选择?

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

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

使用绑定,您正在调用具有现有作用域的函数,因此不会发生作用域遍历。

两个 jsperfs 表明 bind 实际上比closure慢得多

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

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

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

既然它不是更快并且闭包可以达到同样的目的,那么应该避免绑定吗?

2个回答

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

大多数时候这无关紧要。

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

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

是的,.bind比闭包慢得多——至少在 Chrome 中,至少在目前它在v8. 我个人有时不得不为性能问题切换到 Node.JS(更一般地说,闭包在性能密集的情况下有点慢)。

为什么?因为该.bind算法比用另一个函数包装一个函数并使用.callor复杂得多.apply(有趣的是,它还返回一个将 toString 设置为 [native function] 的函数)。

有两种方式来看待这一点,从规范的角度和从实现的角度来看。让我们观察两者。

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

  1. 让 Target 成为这个值。
  2. 如果 IsCallable(Target) 为 false,则抛出 TypeError 异常。
  3. 令 A 是一个新的(可能为空的)内部列表,其中包含在 thisArg(arg1、arg2 等)之后提供的所有参数值,按顺序排列。

...

(21.调用F的[[DefineOwnProperty]]内部方法,参数为“arguments”,PropertyDescriptor {[[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable] ]:假},假。

(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]] 内部特性”

@KubaWyrostek 1) 不,2) 不,因为 bind 的设计速度并不慢,它只是没有实现得那么快。箭头函数还没有在 V8 中登陆(它们登陆然后被恢复)我们会看到它们什么时候出现。
2021-03-20 13:06:53
如果 f = g.bind(stuff); f() 应该比 g(stuff) 慢吗?我可以很快发现这一点,我只是很好奇每次我们调用一个函数时是否都会发生同样的事情,无论该函数是什么实例化的,或者它是否取决于该函数的来源。
2021-03-29 13:06:53
以后对已经应用“绑定”的函数的调用会更慢吗?即 a:function(){}.bind(this)... 以后对 a() 的调用是否比我从不首先绑定时慢?
2021-03-29 13:06:53
@Paul 对我的回答持怀疑态度。所有这些都可能在未来版本的 Chrome (/V8) 中得到优化。我很少发现自己.bind在浏览器中避免使用,在大多数情况下,可读和可理解的代码更为重要。至于绑定函数的速度 -是的,绑定函数此时会保持较慢,尤其this是在部分中未使用您可以从基准、规范和/或独立的实现(基准)中看到这一点
2021-04-07 13:06:53
我想知道:1) 自 2013 年以来有什么变化(现在已经两年了)2)因为箭头函数有这个词法界限 - 箭头函数设计得更慢。
2021-04-09 13:06:53

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

请注意,虽然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 是否很慢,我关心一旦这些变量已经绑定到函数时被调用的代码的性能。