ループ速度比較 〜デカい配列のforEachが約10倍遅い〜 【JavaScript】

2023/04/18に公開1

はじめに

https://zenn.dev/itte/articles/737a3ca709aad0

こちらの記事を読んで、 forEach が速いことが意外だったので、自分でも検証してみました。

検証結果

コード

検証コード
{
  const fill = n => Array.from({ length: n }, (_, i) => i);
  const arrays = [
    fill(100000),
    fill(10000000),
    fill(2 ** 25),
    fill(2 ** 25 + 1),
    fill(2 ** 26),
  ];
  arrays.forEach(arr => {
    console.log('-------');
    console.time(`for         : ${arr.length}`);
    for (let i = 0; i < arr.length; i++) { const v = arr[i]; }
    console.timeEnd(`for         : ${arr.length}`);

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

    console.time(`for of      : ${arr.length}`);
    for (const v of arr) { }
    console.timeEnd(`for of      : ${arr.length}`);

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

実行結果


表

グラフ
グラフ

実行結果(画像の表と同じです)
-------
for         : 100000: 2.464ms
for (cached): 100000: 2.212ms
for of      : 100000: 4.849ms
forEach     : 100000: 1.801ms
-------
for         : 10000000: 13.398ms
for (cached): 10000000: 2.842ms
for of      : 10000000: 5.92ms
forEach     : 10000000: 3.019ms
-------
for         : 20000000: 5.608ms
for (cached): 20000000: 5.674ms
for of      : 20000000: 11.461ms
forEach     : 20000000: 5.732ms
-------
for         : 33554432: 9.434ms
for (cached): 33554432: 9.397ms
for of      : 33554432: 19.094ms
forEach     : 33554432: 10.009ms
-------
for         : 33554433: 33.497ms
for (cached): 33554433: 26.946ms
for of      : 33554433: 642.979ms
forEach     : 33554433: 283.816ms
-------
for         : 67108864: 59.199ms
for (cached): 67108864: 49.539ms
for of      : 67108864: 847.556ms
forEach     : 67108864: 537.941ms

要素数が33,554,432を超えると突然遅くなる

確かに要素数が少ないうちは forEach は速いのですが、要素数が 33,554,432 から 33,554,433 になった瞬間、明らかに遅くなってます。
どのループ方法でも、突然遅くなっていますが、特に著しくパフォーマンスが悪化したのは、 for offorEach約30倍に増加 しています。
for と比べても約10倍で、一緒に走っていたはずなのに突然見捨てられてしまいました。

for 文の変化がわかりにくいので拡大すると、
グラフ拡大
for 文であっても、突然3倍ぐらいの時間がかかっています。

ただ、 for 文の速度であれば、遅くなったとはいえ許容範囲内だと思います。
ちなみに for in は遅すぎたので検証から外しました。

なぜ遅くなるのか

なぜ遅くなるのかという点については、調べていません。
メモリを連続的に確保できなくて最適化できなくなる…とかそんな感じだと勝手に思ってます。

その他のメソッドでの検証

コード

検証コード
{
  const fill = n => Array.from({ length: n }, (_, i) => i);
  const arrays = [
    fill(100000),
    fill(10000000),
    fill(2 ** 25),
    fill(2 ** 25 + 1),
    fill(2 ** 26),
  ];
  arrays.forEach(arr => {
    console.log('-------');
    console.time(`for         : ${arr.length}`);
    for (let i = 0; i < arr.length; i++) { const v = arr[i]; }
    console.timeEnd(`for         : ${arr.length}`);

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

    console.time(`for of      : ${arr.length}`);
    for (const v of arr) { }
    console.timeEnd(`for of      : ${arr.length}`);

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

    // every
    label = `every       : ${arr.length}`;
    console.time(label);
    arr.every((v, i) => true);
    console.timeEnd(label);

    // reduce
    label = `reduce      : ${arr.length}`;
    console.time(label);
    arr.reduce((acc, v, i) => { }, 0);
    console.timeEnd(label);

    // filter
    label = `filter      : ${arr.length}`;
    console.time(label);
    arr.filter((v, i) => true);
    console.timeEnd(label);

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

    // find
    label = `find        : ${arr.length}`;
    console.time(label);
    arr.find((v, i) => false);
    console.timeEnd(label);

    // indexOf
    label = `indexOf     : ${arr.length}`;
    console.time(label);
    arr.indexOf(Infinity);
    console.timeEnd(label);

    // slice
    label = `slice       : ${arr.length}`;
    console.time(label);
    arr.slice();
    console.timeEnd(label);
  });
}

検証結果


その他のメソッド表

グラフ
その他のメソッドグラフ

検証結果(表と同じです)
-------
for         : 100000: 1.917ms
for (cached): 100000: 2.196ms
for of      : 100000: 5.423ms
forEach     : 100000: 1.906ms
some        : 100000: 2.12ms
every       : 100000: 1.765ms
reduce      : 100000: 1.598ms
filter      : 100000: 55.721ms
map         : 100000: 2.207ms
find        : 100000: 1.68ms
indexOf     : 100000: 0.151ms
slice       : 100000: 0.697ms
-------
for         : 10000000: 20.499ms
for (cached): 10000000: 2.878ms
for of      : 10000000: 5.614ms
forEach     : 10000000: 2.827ms
some        : 10000000: 2.806ms
every       : 10000000: 3.048ms
reduce      : 10000000: 10.281ms
filter      : 10000000: 249.985ms
map         : 10000000: 65.648ms
find        : 10000000: 3.047ms
indexOf     : 10000000: 0.002ms
slice       : 10000000: 41.558ms
-------
for         : 33554432: 9.721ms
for (cached): 33554432: 9.502ms
for of      : 33554432: 19.155ms
forEach     : 33554432: 9.626ms
some        : 33554432: 9.41ms
every       : 33554432: 19.198ms
reduce      : 33554432: 24.619ms
filter      : 33554432: 785.836ms
map         : 33554432: 499.55ms
find        : 33554432: 310.616ms
indexOf     : 33554432: 42.842ms
slice       : 33554432: 249.602ms
-------
for         : 33554433: 69.343ms
for (cached): 33554433: 34.429ms
for of      : 33554433: 427.38ms
forEach     : 33554433: 267.396ms
some        : 33554433: 326.967ms
every       : 33554433: 338.672ms
reduce      : 33554433: 298.681ms
filter      : 33554433: 1.219s
map         : 33554433: 501.959ms
find        : 33554433: 307.183ms
indexOf     : 33554433: 42.954ms
slice       : 33554433: 142.439ms
-------
for         : 67108864: 68.643ms
for (cached): 67108864: 55.465ms
for of      : 67108864: 836.494ms
forEach     : 67108864: 530.984ms
some        : 67108864: 643.041ms
every       : 67108864: 668.004ms
reduce      : 67108864: 587.057ms
filter      : 67108864: 2.236s
map         : 67108864: 978.112ms
find        : 67108864: 610.472ms
indexOf     : 67108864: 84.211ms
slice       : 67108864: 285.489ms

その他のメソッドでも、似たような傾向ですが、一部要素数に比例しているものもあります。
わかりにくいので遅いグループと速いグループに分けてみます。

速いグループ
その他のメソッド速いグループ

速いグループでは、 indexOf が直線的に増加しているのがわかります。
なぜなのかわかりませんが、 indexOf は要素数に関係なく速度を維持できるようです。

遅いグループ
その他のメソッド遅いグループ

遅いグループでは、 filter 以外 が直線的に増加しています。
filter の場合は、最初から速度が遅いのに、 33554433 から1.5倍ほど時間がかかっています。
新しい配列を生成するからと言うのであれば、 mapslice も同様の現象が発生しそうですが、そうではありません。
find も条件を見て要素を返すか返さないかという点ではほとんど同じですが、パフォーマンスが明らかに違います。
もしかすると、 mapslice は関数呼び出しの時点で、確保する要素数が確定しているため、最適化しやすいのかもしれません。
filter は、コールバック関数の戻り値によって、配列のlengthが可変なので、動的にメモリ確保することになり最適化しづらいのかと思いました。

結論

結論としては、大きな配列を扱う可能性がある場合、極力純粋な for 文でループを回すか、大きな配列を作らないように分割して処理するなどの注意が必要になるということですね。

あとがき

おそらくですが、この現象はJavaScriptに限った話ではないと思います。
今後は少ないデータを扱う場合と、大量のデータを扱う場合とではセオリーが変わるかもしれない、ということを念頭に置いておこうと思いました。

Discussion