abcdefGets

ゲッツ!

Function.prototype.bind のパフォーマンスについて

ふとパフォーマンスが気になったので調査した。
記憶が正しければ、callよりも遅いはず。

というわけでレッツ検証

事前準備

package.json

{
  "name": "bench",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "brn",
  "license": "MIT",
  "devDependencies": {
    "benchmark": "^2.1.3"
  }
}

bench.js

/**
 * @fileoverview
 * @author Taketoshi Aono
 */

const Benchmark = require('benchmark');
const suite = new Benchmark.Suite;

const bind = (() => {}).bind({});
const bindWithArgs = ((a, b, c) => {}).bind({}, 1, 2, 3);
const call = () => {};
const callargs = {};
const callcall = () => call.call(callargs)
// add tests
suite
  .add('bind', () => {
    bind();
  })
  .add('bind with args', () => {
    bindWithArgs();
  })
  .add('call', () => {
    callcall();
  })
  .on('cycle', function(event) {
    console.log(String(event.target));
  })
  .on('complete', function() {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({ 'async': true });

node bench.js

結果は V8限定ですが,

bind x 43,404,436 ops/sec ±2.07% (83 runs sampled)
bind with args x 35,140,882 ops/sec ±2.48% (84 runs sampled)
call x 59,048,983 ops/sec ±1.13% (85 runs sampled)
Fastest is call

となりました。 予想通り、callが最速。

ただ、気になるのはbindに引数を束縛した場合さらに遅くなる点。

気になるので調べました。

V8のコミットID
bdf32cf1bc96982ff5a22195d874617ffae03e79
時点のものです。

コード検証

色々さがして、とりあえずx87のbuiltin-x87.ccを見る。
x64でもいいけど、とりあえずx87を調べましょう。

でこれが、bindで生成した関数を呼び出すアセンブラコード生成関数。

void Builtins::Generate_CallBoundFunctionImpl(MacroAssembler* masm,
                                              TailCallMode tail_call_mode) {
  // ----------- S t a t e -------------
  //  -- eax : the number of arguments (not including the receiver)
  //  -- edi : the function to call (checked to be a JSBoundFunction)
  // -----------------------------------
  __ AssertBoundFunction(edi);

  if (tail_call_mode == TailCallMode::kAllow) {
    PrepareForTailCall(masm, eax, ebx, ecx, edx);
  }

  // Patch the receiver to [[BoundThis]].
  __ mov(ebx, FieldOperand(edi, JSBoundFunction::kBoundThisOffset));
  __ mov(Operand(esp, eax, times_pointer_size, kPointerSize), ebx);

  // Push the [[BoundArguments]] onto the stack.
  Generate_PushBoundArguments(masm);

  // Call the [[BoundTargetFunction]] via the Call builtin.
  __ mov(edi, FieldOperand(edi, JSBoundFunction::kBoundTargetFunctionOffset));
  __ mov(ecx, Operand::StaticVariable(ExternalReference(
                  Builtins::kCall_ReceiverIsAny, masm->isolate())));
  __ lea(ecx, FieldOperand(ecx, Code::kHeaderSize));
  __ jmp(ecx);
}

ここで注目したいのが、
Generate_PushBoundArguments(masm);
の部分
束縛した引数をPushするコードだと想像できる。

Generate_PushBoundArgumentsがこちら。

void Generate_PushBoundArguments(MacroAssembler* masm) {
  // ----------- S t a t e -------------
  //  -- eax : the number of arguments (not including the receiver)
  //  -- edx : new.target (only in case of [[Construct]])
  //  -- edi : target (checked to be a JSBoundFunction)
  // -----------------------------------

  // Load [[BoundArguments]] into ecx and length of that into ebx.
  Label no_bound_arguments;
  __ mov(ecx, FieldOperand(edi, JSBoundFunction::kBoundArgumentsOffset));
  __ mov(ebx, FieldOperand(ecx, FixedArray::kLengthOffset));
  __ SmiUntag(ebx);
  __ test(ebx, ebx);
  __ j(zero, &no_bound_arguments);
  {
    // ----------- S t a t e -------------
    //  -- eax : the number of arguments (not including the receiver)
    //  -- edx : new.target (only in case of [[Construct]])
    //  -- edi : target (checked to be a JSBoundFunction)
    //  -- ecx : the [[BoundArguments]] (implemented as FixedArray)
    //  -- ebx : the number of [[BoundArguments]]
    // -----------------------------------

    // Reserve stack space for the [[BoundArguments]].
    {
      Label done;
      __ lea(ecx, Operand(ebx, times_pointer_size, 0));
      __ sub(esp, ecx);
      // Check the stack for overflow. We are not trying to catch interruptions
      // (i.e. debug break and preemption) here, so check the "real stack
      // limit".
      __ CompareRoot(esp, ecx, Heap::kRealStackLimitRootIndex);
      __ j(greater, &done, Label::kNear);  // Signed comparison.
      // Restore the stack pointer.
      __ lea(esp, Operand(esp, ebx, times_pointer_size, 0));
      {
        FrameScope scope(masm, StackFrame::MANUAL);
        __ EnterFrame(StackFrame::INTERNAL);
        __ CallRuntime(Runtime::kThrowStackOverflow);
      }
      __ bind(&done);
    }

    // Adjust effective number of arguments to include return address.
    __ inc(eax);

    // Relocate arguments and return address down the stack.
    {
      Label loop;
      __ Set(ecx, 0);
      __ lea(ebx, Operand(esp, ebx, times_pointer_size, 0));
      __ bind(&loop);
      __ fld_s(Operand(ebx, ecx, times_pointer_size, 0));
      __ fstp_s(Operand(esp, ecx, times_pointer_size, 0));
      __ inc(ecx);
      __ cmp(ecx, eax);
      __ j(less, &loop);
    }

    // Copy [[BoundArguments]] to the stack (below the arguments).
    {
      Label loop;
      __ mov(ecx, FieldOperand(edi, JSBoundFunction::kBoundArgumentsOffset));
      __ mov(ebx, FieldOperand(ecx, FixedArray::kLengthOffset));
      __ SmiUntag(ebx);
      __ bind(&loop);
      __ dec(ebx);
      __ fld_s(
          FieldOperand(ecx, ebx, times_pointer_size, FixedArray::kHeaderSize));
      __ fstp_s(Operand(esp, eax, times_pointer_size, 0));
      __ lea(eax, Operand(eax, 1));
      __ j(greater, &loop);
    }

    // Adjust effective number of arguments (eax contains the number of
    // arguments from the call plus return address plus the number of
    // [[BoundArguments]]), so we need to subtract one for the return address.
    __ dec(eax);
  }
  __ bind(&no_bound_arguments);
}

想像よりなが~いーーー

__ j(zero, &no_bound_arguments);で引数束縛がなければjmpするコードを生成

その後は、BoundArgumentsを保存するstack領域を確保、リターンアドレスをstackに押し込んで、
BoundArgumentsをstackに押し込む。
これをループで行っているのでだいぶ遅そう。

Function.prototype.bindが引数束縛つきで遅くなるのはこれが理由っぽい。

まとめ

  • 最速は() => fn.call(this)形式
  • 次点でfn.bind(this)
  • 引数の束縛はかなり遅くなるので、() => fn.call(this, ...)のがおすすめ。

アロー関数のおかげでFunction.prototype.bind使い所がない。