web-dev-qa-db-ja.com

バインドがクロージャーより遅いのはなぜですか?

以前のポスターは尋ねました JavascriptでのFunction.bindとClosure:選択方法

そして、バインドがクロージャーよりも高速であることを示すように思われる部分的にこの答えを受け取りました:

スコープトラバーサルとは、異なるスコープに存在する値(変数、オブジェクト)を取得しようとしているときに、追加のオーバーヘッドが追加されることを意味します(コードの実行が遅くなります)。

バインドを使用して、既存のスコープで関数を呼び出しているため、スコープのトラバーサルは行われません。

2つのjsperfsは、バインドが closure よりも実際にはずっと遅いことを示唆しています。

これは上記に対するコメントとして投稿されました

そして、私は 自分のjsperf と書くことにしました

では、なぜバインドがこれほど遅くなるのか(クロムで70 +%)?

それは高速ではなく、クロージャは同じ目的を果たすことができるので、バインドは避けるべきですか?

75
Paul

Chrome 59の更新:下の答えで予測したように、新しい最適化コンパイラを使用すると、バインドが遅くなりません。コードの詳細は次のとおりです。 https://codereview.chromium.org/2916063002/

ほとんどの場合、それは重要ではありません。

_.bind_がボトルネックであるアプリケーションを作成している場合を除き、気にしません。ほとんどの場合、完全なパフォーマンスよりも読みやすさがはるかに重要です。ネイティブの_.bind_を使用すると、通常、読みやすく保守しやすいコードが得られると思います-これは大きなプラスです。

しかし、はい、それが重要な場合-_.bind_は遅いです

はい、_.bind_はクロージャーよりもかなり遅くなります-少なくともChromeでは、少なくとも_v8_で実装されている現在の方法では。私は、パフォーマンスの問題のためにNode.JSで何度か切り替える必要がありました(より一般的には、パフォーマンスが集中する状況ではクロージャーは少し遅くなります)。

どうして? _.bind_アルゴリズムは、関数を別の関数でラップして_.call_または_.apply_を使用するよりもはるかに複雑だからです。 (面白いことに、toStringが[native function]に設定された関数も返します)。

これを見るには、仕様の観点と実装の観点の2つの方法があります。両方を観察しましょう。

まず、 仕様で定義されているバインドアルゴリズムを見てください

  1. Targetをthis値とします。
  2. IsCallable(Target)がfalseの場合、TypeError例外をスローします。
  3. Aを、thisArg(arg1、arg2など)の後に指定されたすべての引数値の新しい(場合によっては空の)内部リストにします。

...

(21.引数 "arguments"、PropertyDescriptor {[[Get]]:thrower、[[Set]]:thrower、[[Enumerable]]:false、[[Configurable]を使用して、Fの[[DefineOwnProperty]]内部メソッドを呼び出す]:false}、およびfalse。

(22. Fを返します。

単なるラップ以上の、かなり複雑なようです。

次に、 Chromeでの実装方法 を見てみましょう。

V8(クロムJavaScriptエンジン)のソースコードで FunctionBind を確認しましょう。

_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]]、および[ [スコープ]]内部プロパティ "

135