◆ apply のほうが速い

ES2015 で ... 演算子で関数呼び出しの時に apply のように配列を展開できて パフォーマンスの悪い apply を使わなくて済む と聞いた覚えがあります

使えるようになった当時では JavaScript エンジンの実装面のチューニングもそこまでされていなくて早くないかもしれません
でも もうそろそろ 次の ES2017 が 2 ヶ月もすれば出る頃で ES2015 が使えるようになってけっこう時間も経ちましたしどっちが速いのか調べてみようかと思いました

apply と bind + ...

比較するのは 4 つ

  • bind して ... で関数実行
  • apply で関数実行
  • call で配列を手動で展開して実行
  • bind して配列を手動で展開して実行

手動での展開は可変個数だと無理があるので 今回は3つの要素の配列にしてます
それぞれ 3 の数字が入った配列が10万個ある配列を作って足し算するものです

4 種類の呼び出し方はこうなります
fn.bind(obj)(...a[i])
fn.apply(obj, a[i])
fn.call(obj, a[i][0], a[i][1], a[i][2])
fn.bind(obj)(a[i][0], a[i][1], a[i][2])

くわしくは全体を
var a = []

for(var i=0;i<100000;i++){
var k = i * 3
a.push([k, k+1, k+2])
}

var fn = function(x, y, z){ this.sum += x + y + z }

for(var i=0;i<5;i++){
t1("bind...")
t2("apply")
t3("call")
t4("bind")
}

function t1(name){
var obj = {sum: 0}

console.time(name)

for(var i=0;i<a.length;i++){
fn.bind(obj)(...a[i])
}

console.timeEnd(name)

console.log(obj.sum)
}

function t2(name){
var obj = {sum: 0}

console.time(name)

for(var i=0;i<a.length;i++){
fn.apply(obj, a[i])
}

console.timeEnd(name)

console.log(obj.sum)
}

function t3(name){
var obj = {sum: 0}

console.time(name)

for(var i=0;i<a.length;i++){
fn.call(obj, a[i][0], a[i][1], a[i][2])
}

console.timeEnd(name)

console.log(obj.sum)
}

function t4(name){
var obj = {sum: 0}

console.time(name)

for(var i=0;i<a.length;i++){
fn.bind(obj)(a[i][0], a[i][1], a[i][2])
}

console.timeEnd(name)

console.log(obj.sum)
}


さて 結果は

[Chrome]
bind...: 13.3ms
apply:    4.84ms
call:     2.67ms
bind:     8.76ms

bind...:  7.96ms
apply:    4.11ms
call:     2.53ms
bind:     5.80ms

bind...:  6.09ms
apply:    2.47ms
call:     0.676ms
bind:     3.88ms

bind...:  6.81ms
apply:    2.24ms
call:     0.460ms
bind:     2.76ms

bind...:  6.34ms
apply:    2.43ms
call:     0.509ms
bind:     3.44ms

[Firefox]
bind...: タイマー開始
bind...: 110.84ms
apply: タイマー開始
apply: 3.86ms
call: タイマー開始
call: 3.26ms
bind: タイマー開始
bind: 26.78ms

bind...: タイマー開始
bind...: 104.48ms
apply: タイマー開始
apply: 3.54ms
call: タイマー開始
call: 2.56ms
bind: タイマー開始
bind: 28.71ms

bind...: タイマー開始
bind...: 104.53ms
apply: タイマー開始
apply: 3ms
call: タイマー開始
call: 1.63ms
bind: タイマー開始
bind: 26.91ms

bind...: タイマー開始
bind...: 105.31ms
apply: タイマー開始
apply: 3.63ms
call: タイマー開始
call: 1.62ms
bind: タイマー開始
bind: 25.08ms

bind...: タイマー開始
bind...: 103.53ms
apply: タイマー開始
apply: 2.96ms
call: タイマー開始
call: 1.66ms
bind: タイマー開始
bind: 23.42ms

いつもどおり Chrome は for 文の 2 回目以降がキャッシュのためか高速化してます

一番早いのは call で一番遅いのが bind + ... と言う結果でした

bind してる 2 つは遅いです
新しく関数を作ってるので仕方ないともいえます

call と apply だと call のほうが速く 速度を求めるときで配列の個数が固定なら手動で要素を分けて call にしたほうが良さそうです

call と apply のように bind した関数に手動で要素を分けて呼び出すのと ... 演算子で自動で展開させるのだと ... 演算子のほうが遅くなってます

結果 apply と bind + ... では bind で関数作る分 apply のほうが速いと言う結果でした

普通の関数呼び出しと ...

this を bind することってそこまで多くもないので 通常の関数呼び出しもやってみます
sum(a[i][0], a[i][1], a[i][2])
sum(...a[i])

この 2 種類
bind と bind + ... と同じ結果な気がします
var a = []

for(var i=0;i<100000;i++){
var k = i * 3
a.push([k, k+1, k+2])
}

var sum = function(a, b, c){ return a + b + c }

for(var i=0;i<5;i++){
t5("standard")
t6("...")
}

function t5(name){
var total = 0
console.time(name)

for(var i=0;i<a.length;i++){
total += sum(a[i][0], a[i][1], a[i][2])
}

console.timeEnd(name)
}

function t6(name){
var total = 0
console.time(name)

for(var i=0;i<a.length;i++){
total += sum(...a[i])
}

console.timeEnd(name)
}

[Chrome]
standard:  2.13ms
...:       5.03ms

standard:  0.402ms
...:       4.34ms

standard:  0.465ms
...:       3.93ms

standard:  0.400ms
...:       3.84ms

standard:  0.399ms
...:       3.91ms

[Firefox]
standard: タイマー開始
standard: 2.77ms
...: タイマー開始
...: 58.6ms

standard: タイマー開始
standard: 2.09ms
...: タイマー開始
...: 56.62ms

standard: タイマー開始
standard: 1.77ms
...: タイマー開始
...: 56.33ms

standard: タイマー開始
standard: 1.54ms
...: タイマー開始
...: 55.72ms

standard: タイマー開始
standard: 1.51ms
...: タイマー開始
...: 56.36ms

同じようなものですね
通常の呼び出しにしたほうが速いです

Firefox の ... の遅さはちょっと遅すぎかも
30 倍くらいって

apply と ...

考えてみると通常呼び出しと ... って同じことをしてるわけですが 通常呼び出しにできない場合もありますよね
配列の要素が 2 つだったり 50 だったり可変だと  if 文で全部個数分書かないといけなくなります

そのための apply なので bind したものと比較というより
sum.apply(null, a[i])
sum(...a[i])

この 2 つを比べるのが重要な気がしました

(というのにブログ書き始めてから気づいたので ここは計測マシンが異なります)
var a = []

for(var i=0;i<100000;i++){
var k = i * 3
a.push([k, k+1, k+2])
}

var sum = function(a, b, c){ return a + b + c }

for(var i=0;i<5;i++){
t7("apply")
t8("...")
}

function t7(name){
var total = 0
console.time(name)

for(var i=0;i<a.length;i++){
total += sum.apply(null, a[i])
}

console.timeEnd(name)
}

function t8(name){
var total = 0
console.time(name)

for(var i=0;i<a.length;i++){
total += sum(...a[i])
}

console.timeEnd(name)
}

[Chrome]
apply:  5.71ms
...:    7.30ms

apply:  2.29ms
...:    3.75ms

apply:  2.41ms
...:    4.17ms

apply:  2.70ms
...:    3.89ms

apply:  2.17ms
...:    4.03ms

[Firefox]
apply: タイマー開始
apply: 5.81ms
...: タイマー開始
...: 44.53ms

apply: タイマー開始
apply: 2.66ms
...: タイマー開始
...: 41.42ms

apply: タイマー開始
apply: 2.34ms
...: タイマー開始
...: 41.23ms

apply: タイマー開始
apply: 2.63ms
...: タイマー開始
...: 41.96ms

apply: タイマー開始
apply: 2.41ms
...: タイマー開始
...: 41.32ms


Chrome はこれまでのほど差はないですが apply のほうが速いです
1.2倍~1.5倍くらい

Firefox は ... が異常に遅いので 10 ~ 20 倍くらいの差


まだまだ改善されていきそうではありますが しばらくは速度面を気にするなら call/apply のほうがよさそうです