📊

結局JavaScriptの配列ループはどれが一番速いのか

2021/12/05に公開
4

配列(Array)に繰り返し処理をするにあたって、for文、while文、for...of文、forEachはどれが一番速いのか検証します。

検証するに至った理由として、日本で実行速度を検証した記事では、for文が最速という記事が多く見受けらます。例えば以下です。

これらの記事は2021年12月5日現在、ループ比較において日本版Google検索上位にあります。
しかし、コンパイラ言語ならともかくインタプリタ言語において、配列の要素に添字でアクセスしなければならないfor文が早いというのは直感的には納得できませんでした。さらに、パフォーマンス測定サイトのMeasureThat.netではforEachの方が早いという結果が出ていたのです。

ループの方法による速度差なんて大したことないんだから、好きなのを使ったら良いんだよというのは分かりますが、速度が求められるライブラリを開発するような場面では、ループが更に繰り返し実行されることも多く、少しでも速くしたいものです。
自分が納得できる形で検証してみようと思います。

実行環境

今回の検証では実行環境について細かく分けて測定しません。理由はサーバーかPCかスマホか、どのブラウザか、どのOSか、CPUは何を使っているか、キリが無いからです。その上、最後まで読んでいただければ分かりますが、実行環境で違いがありすぎてやるだけ無意味と考えています。
そのため、私が使っているSurface Pro X(16GB,64bit,ARMベースプロセッサ) Windows 10にて、Node.js(v17.1.0)とEdge(96.0.1054.43)でのみの測定とします。
測定方法は書きますので、他の環境が気になる場合は自身が納得できる形で実行されてみてください。

検証コード

1,000個の配列に対して以下の5つのパターンで繰り返し処理を行います。for文の場合、添字で配列の値へのアクセスが必要になるため、すべてのループの中に代入式を入れています。console.logを繰り返している記事も多いのですが誤差が大きくなりそうなので使用しませんでした。

for i++
for (var i = 0; i < arr.length; i++) {
  var a = arr[i]
}
for i--
for (var i = arr.length; i >= 0; i--) {
  var a = arr[i]
}
while i--
var i = arr.length
while (i--) {
  var a = arr[i]
}
for of
for (var v of arr) {
  var a = v
}
forEach
arr.forEach(v => {
  var a = v
})

パフォーマンス測定サイトで検証

まずはお手軽に測定サイトを使ってみます。単位はops/secで、数字が大きいほうが速くなります。

MeasureThat.netで測定

他の人が測定した結果でもforEachが最速と出ているMeasureThat.netで測定してみます。
※++が文字化けしています。

参照:Run results for: for vs while vs for of vs forEach

圧倒的にforEachが速いという結果です。for ofよりも速いというのは個人的に意外でした。

JSBEN.CHで測定

他のサイトでも測定してみます。

参照:for vs while vs for of vs forEach

またforEachが速いという結果になりました。for i--whileよりもfor i++の方が早いのは最適化されているのでしょうか。
しかしこのサイトではどれも差があまりなく、テストを繰り返すとたまにfor i++が一番速いという結果になることもあります。複数回実行しての平均は取っていないようです。

JSBench.Meで測定

このサイトではfor i++が一番速いという結果になりました。MeasureThat.netとは真逆の結果です。

Perflinkで測定

参照:for vs while vs for of vs forEach

このサイトも実行するたびにたまにfor i++forEachが逆転するのですが、forEachが最速になることが多いです。

何度実行しても変わらないのはfor ofが最も遅いということでした。

パフォーマンス測定サイトでの考察

4つのベンチマークサイトで測定した結果、forEachが速いことが多いようです。
実行するたびに結果が変わるというのは様々な要因があると思いますので理解できるのですが、平均を取っているMeasureThat.netJSBench.Meで結果が真逆になるというのが不思議です。
どのサイトを信じたらいいのか全く分かりません。

Benchmark.jsで検証

測定サイトの結果がそれぞれ異なるため、仕方なくライブラリBenchmark.jsを使って測定してみます。単位はops/secで、数字が大きいほうが速くなります。

Node.js結果

1回目

for i++ x 890,587 ops/sec ±1.98% (85 runs sampled)
for i-- x 676,696 ops/sec ±1.51% (88 runs sampled)
while i-- x 535,465 ops/sec ±1.66% (84 runs sampled)
for of x 637,016 ops/sec ±1.69% (85 runs sampled)
forEach x 493,769 ops/sec ±54.98% (84 runs sampled)

2回目

for i++ x 921,759 ops/sec ±0.34% (85 runs sampled)
for i-- x 694,308 ops/sec ±0.53% (85 runs sampled)
while i-- x 557,372 ops/sec ±0.35% (88 runs sampled)
for of x 659,756 ops/sec ±0.50% (89 runs sampled)
forEach x 729,502 ops/sec ±53.00% (91 runs sampled)

ソース:ittedev/my_measure/for_vs_foreach/node.js

for i++が最速でした。1回目と2回目の2つの結果を載せたのは、forEachの結果にばらつきがあり、2番手になったり最下位になったりしたためです。

Edge結果

for i++ x 1,486,420 ops/sec ±0.57% (66 runs sampled)
for i-- x 990,967 ops/sec ±1.14% (64 runs sampled)
while i-- x 748,789 ops/sec ±1.18% (66 runs sampled)
for of x 988,361 ops/sec ±0.30% (66 runs sampled)
forEach x 2,322,379 ops/sec ±2.02% (65 runs sampled)

ソース:ittedev/my_measure/for_vs_foreach/browser.html

forEachが圧倒的に速いです。その他の方法の中ではfor i++が速いです。

Benchmark.jsでの考察

またしても結果が異なりました。さらに、各検証サイトとも異なる結果です。
Node.jsではfor i++が早く、EdgeではforEachが早くなります。
実行環境で異なるという結果です。当然、私の環境以外では違う結果になるでしょう。
あとはBenchmark.jsが信用できるかというと、少なくともパフォーマンス測定サイトよりは、実行環境と測定方法が明確になるので信用できると思います。

考察

結果として1位になることがあったのはfor i++forEachです。どちらが上かというと、環境によって異なるため、はっきり言えませんし、今後変わる可能性もあります。
はっきり言えるのはfor i++が最速とは限らないということです。むしろ、MeasureThat.netと、Benchmark.jsを使ってEdgeで測った結果から、Chromium系ではforEachのほうが圧倒的に速いことが多かったのです。

C言語等でよく行なわれていたfor文とwhile文のi--を使ったテクニックは期待した結果が得られませんでした。これはfor i++を使ったときに最適化が行われているのではないでしょうか。あるいはインクリメントとデクリメントでは実行速度が異なるのかもしれません。

また、比較的新しい書き方のfor...of文は最速にはならないということが分かりました。
for...of文は配列だけでなくジェネレータやイテレータにも使え、for await...ofというのもあったりして、同期処理もできるため万能ですので、配列に繰り返し処理したい時とは用途が違うと考えたほうがいいですね。

結論

  • ループ速度の順序は実行環境で変わる。
  • Node.jsで速度を求めているときは自身の環境で測定した方が良いが、面倒ならfor i++文を使う。
  • ブラウザで速度を求めているときはforEachを使う。
  • それ以外のとき、for...of文とforEachのどちらか迷ったらforEachを使い、forEachが使えないときだけfor...of文を使う。

https://zenn.dev/rabee/articles/javascript-huge-array-for-each-is-very-slow

Discussion

simiraaaasimiraaaa

気になったので調べてみたのですが、要素数が大きくなるとforEachのパフォーマンスが著しく悪くなるようです。
自分の環境だと要素数が 40000000 ぐらいからパフォーマンスが悪くなりました。
ここまで大きい配列を扱うことはないとは思いますが、大規模なデータを使うときなどforEachを使わないようにしたほうが良さそうです。
以下自分の環境での実行結果です。

{
  const fill = n => Array.from({ length: n }, (_, i) => i);
  const arrs = [
    fill(100),
    fill(1000),
    fill(10000),
    fill(100000),
    fill(1000000),
    fill(10000000),
    fill(40000000),
    fill(100000000),
  ];


  arrs.forEach(arr => {
    console.log('-------');
    console.time(`for         : ${arr.length}`);
    for (let i = 0; i < arr.length; i++) {
    }
    console.timeEnd(`for         : ${arr.length}`);

    console.time(`for (cached): ${arr.length}`);
    const len = arr.length;
    for (let i = 0; i < len; i++) {
    }
    console.timeEnd(`for (cached): ${arr.length}`);

    console.time(`forEach     : ${arr.length}`);
    arr.forEach((v, i) => { });
    console.timeEnd(`forEach     : ${arr.length}`);

    console.time(`i--         : ${arr.length}`);
    for (let i = arr.length - 1; i >= 0; i--) {
    }
    console.timeEnd(`i--         : ${arr.length}`);
    console.log('-------');
  });
}
-------
for         : 10000000: 2.928ms
for (cached): 10000000: 3.318ms
forEach     : 10000000: 3.178ms
i--         : 10000000: 2.941ms
-------
-------
for         : 40000000: 26.631ms
for (cached): 40000000: 11.5ms
forEach     : 40000000: 359.65ms
i--         : 40000000: 22.601ms
-------
-------
for         : 100000000: 60.06ms
for (cached): 100000000: 27.932ms
forEach     : 100000000: 842.739ms
i--         : 100000000: 57.114ms
-------
NakamuraNakamura

コメントありがとうございます!

おお~
私の環境でも同じ結果になりました。
ブラウザは要素数によって実装を変えてるんでしょうか。急に悪くなりました。

要素数が速度にここまで影響してくるとは全く想像していませんでした。
貴重な情報をありがとうございます。

おのざわおのざわ

for++のテストコードですが、ループ条件式に i < arr.lengthが入っているのが気になりました。
私の環境では、事前にvar l = arr.lengthとしておくと、おおよそ倍くらいの速度になりました。
(「for++の速度、遅いじゃん!」と思った人が見てくれるといいなと思い、コメント残させていただきました)

NakamuraNakamura

コメントありがとうございます!
おっしゃる通りです!

事前に長さを変数に入れるかどうかで、最初に測定したサイトでfor++以外の方法との差を埋めるほどの影響が無かったので、そのまま変数に入れない書き方を採用してしまっていました。
プロパティへのアクセス1回とはいえ軽視するべきではありませんでした。