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
使い所がない。