1. ホーム
  2. javascript

[解決済み] なぜbindはクロージャより遅いのか?

2023-07-02 07:47:29

質問

以前の投稿者からの質問 JavascriptのFunction.bindとClosureの比較:どのように選択するのですか?

で、一部このような回答がありましたが、これはbindの方がclosureより速いはずということらしいです。

スコープトラバーサルとは、異なるスコープに存在する値 (変数,オブジェクト) を取得するために手を伸ばしたとき、したがって オーバーヘッドが追加されます(コードの実行速度が遅くなります)。

bind を使うと、既存のスコープを持つ関数を呼び出すことになります。 スコープの走査は行われません。

2 つの jsperfs は、bind は実際には 閉鎖 .

上記へのコメントとして投稿されたものです

と、書くことにしました。 私自身のjsperf

では、なぜ bind はこんなに遅いのでしょうか (chromium では 70% 以上)?

速くなく、クロージャが同じ目的を果たすことができるので、bindは避けるべきなのでしょうか?

どのように解決するのですか?

Chrome 59 の更新: 以下の回答で予測したように、新しい最適化コンパイラでバインドが遅くなることはなくなりました。以下はそのコードです。 https://codereview.chromium.org/2916063002/

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

アプリケーションを作成しているのでなければ .bind がボトルネックになるようなアプリケーションを作るのでなければ、私は気にしないでしょう。ほとんどの場合、読みやすさの方がパフォーマンスよりもずっと重要です。私は、ネイティブの .bind を使用することは、通常、より読みやすく、保守しやすいコードを提供することになります。

しかし、そうです、重要なときには .bind はより遅いです。

はい。 .bind はクロージャよりもかなり遅いです - 少なくとも Chrome では、少なくとも現在の方法で実装されている v8 . 私は個人的に、パフォーマンスの問題でNode.JSに切り替えなければならなかったことがあります(より一般的には、クロージャはパフォーマンスを重視する状況ではちょっと遅いです)。

なぜかというと なぜなら .bind アルゴリズムは、関数を別の関数でラップして .call または .apply . (面白いことに、これは toString を [ネイティブ関数] に設定した関数も返します)。

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

まず 仕様で定義されているバインドアルゴリズムを見てみましょう。 :

  1. Targetをこの値とします。
  2. IsCallable(Target)がfalseの場合、TypeError例外を投げる。
  3. thisArgの後に提供されたすべての引数値(arg1、arg2など)を順番に並べた新しい(空の可能性がある)内部リストをAとする。

...

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

(22.Fを返す。

かなり複雑なようだ、ラップよりもずっと。

次に が Chrome でどのように実装されているか見てみましょう。 .

確認しましょう FunctionBind をv8(chrome JavaScript engine)のソースコードで確認してみましょう。

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を使って作られたFunctionオブジェクトはprototypeプロパティや[[Code]]、[[FormalParameters]]、[[Scope]]の内部プロパティを持っていない、と仕様書には書かれています"