JavaScriptの配列周りの処理ってどうするのが一番速いの?
JavaScriptは結構優秀な言語なので一般的なWebアプリケーションを開発する上では処理速度を気にする必要はあまりないと思います。
とはいえ、エンジニアとしてパフォーマンスの高い実装を心がけたいので、よく扱う配列周りの処理について速度を調べてみることにしました。
配列内の全要素に対するループ処理
まずはシンプルによく使う配列の全件ループについて検証してみます。
個人的には可読性を重視して for-of
を使うことが多いですが、純粋な for
文で添え字参照した方が速そう。
実行した処理
1000万個の要素を持つ配列に対してループ処理を実行したときの経過時間を計測します。
平等に計測するため、いずれの場合においてもループ処理内で配列内の各要素を参照するようにしました。
ループの回し方は
-
Array.prototype.forEach
メソッド -
for
を用いたインデックス参照 for-of
for-in
の4パターンを比較します。
const LOOP_LENGTH = 10**7
const data = Array.from({ length: LOOP_LENGTH }, (_, i) => ({
id: i + 1
}))
const forEachStart = Date.now()
data.forEach((el) => {
el
})
const forEachDuration = Date.now() - forEachStart
console.log('forEach', forEachDuration)
const forStart = Date.now()
for (let i = 0; i < data.length; i++) {
const el = data[i]
}
const forDuration = Date.now() - forStart
console.log('for', forDuration)
const forOfStart = Date.now()
for (const el of data) {
el
}
const forOfDuration = Date.now() - forOfStart
console.log('for of', forOfDuration)
const forInStart = Date.now()
for (const el in data) {
el
}
const forInDuration = Date.now() - forInStart
console.log('for in', forInDuration)
結果
forEach |
for |
for-of |
for-in |
|
---|---|---|---|---|
1回目 | 59ms | 5ms | 104ms | 1717ms |
2回目 | 56ms | 8ms | 106ms | 1756ms |
3回目 | 58ms | 8ms | 97ms | 2480ms |
予想通り、シンプルな for
文が一番速く、次いで forEach
、 for-of
、 ぶっちぎりで遅かったのは for-in
という結果。
forEach
と for-of
に倍近い差が出たのは意外でした。
インデックス番号が取得できることを踏まえても forEach
の方が利便性は高いですが、可読性を考慮すると for-of
を使った方がチーム開発には向いていそうなのでどちらを使うかケースバイケース。
for-in
については enumerable の考慮が必要で管理が複雑になりやすいですし、チーム開発には向いていなさそうです。個人開発で enumerable を使いこなせるのであれば一考してもいいかもしれませんが、かなり遅いことも踏まえて他のパターンを採用した方が無難な気がします。
いろんなリストの全要素に対するループ処理
リストを扱う場合、必要に応じて配列以外の手法を採用するケースがあります。
それぞれ用途は異なりますが、ループ処理を行う場合はどれが優れているのでしょうか?
実行した処理
1000万個の要素を持つリストに対してそれぞれ全件ループを実行した場合の速度を比較します。
公平を期すため、ループの手法はすべて for-of
で統一しておきましょう。
比較対象のリストは
- 配列
- Map
- Set
- 連想配列(オブジェクト)
の4種を採用しました。
const LOOP_LENGTH = 10**7
const data = Array.from({ length: LOOP_LENGTH }, (_, i) => ({
id: i + 1
}))
let v = null
const arr = [...data]
const arrStartTime = Date.now()
for (const el of arr) {
el
}
const arrDuration = Date.now() - arrStartTime
console.log('arr', arrDuration)
const map = new Map(data.map((el) => [el.id, el]))
const mapStartTime = Date.now()
for (const el of map.values()) {
el
}
const mapDuration = Date.now() - mapStartTime
console.log('map', mapDuration)
const _set = new Set(data)
const setStartTime = Date.now()
for (const el of _set.values()) {
el
}
const setDuration = Date.now() - setStartTime
console.log('set', setDuration)
const associativeArr = Object.fromEntries(data.map((el) => [el.id, el]))
const associativeArrStartTime = Date.now()
for (const el of Object.values(associativeArr)) {
el
}
const associativeArrDuration = Date.now() - associativeArrStartTime
console.log('associativeArr', associativeArrDuration)
結果
配列 | Map | Set | 連想配列 | |
---|---|---|---|---|
1回目 | 106ms | 129ms | 109ms | 146ms |
2回目 | 128ms | 125ms | 108ms | 140ms |
3回目 | 96ms | 146ms | 119ms | 144ms |
配列とSetが若干速い気もしますが、有意な差とも言い難い結果になりました……。
ただ、この後も何回か実行したところ、配列は大体同じ結果になりましたが、他の3つは2~5倍ほどの時間がかかったり逆に数十msで完了したりと速度にばらつきがありました。
詳しい原因はわかりませんが、Map、Set、Objectの values
メソッドは実行状況によってパフォーマンスの差が大きくなるのかもしれません。
とはいえ、平均するとどのリスト形式も速度に大きな差はなさそうなのでループ処理の観点からはどれを選んでもよさそうです。
正直、 values
メソッドを使用するともっと遅くなると思っていたので意外な結果でした。
いろいろなリストからIDに紐づく要素を検索する処理
ループについては各リストで大きな速度差は見られませんでしたが、要素を検索する処理ではどうでしょうか?
検索した要素を取得するという目的からずれるSetを除いて、その他のリスト形式での速度を調べてみます。
実行した処理
1~1000までの自然数でIDが振られた1000個の要素を持つリストに対して、IDをキーとして要素を検索する処理を1000万回繰り返したときの結果を比較します。
比較対象は下記の4パターンです。
- IDとインデックス番号を合わせた配列(添え字による参照)
- IDとインデックス番号を合わせていない配列(
find
メソッドによる検索) - IDをキーとしたMap
- IDをキーとした連想配列
const LOOP_LENGTH = 10**7
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1
}))
const indexArr = [null, ...data]
const indexArrStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
const id = data[i % data.length].id
indexArr[id]
}
const indexArrDuration = Date.now() - indexArrStartTime
console.log('indexArr', indexArrDuration)
const findArr = [...data]
const findArrStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
const id = data[i % data.length].id
findArr.find(({ id: _id }) => _id === id)
}
const findArrDuration = Date.now() - findArrStartTime
console.log('findArr', findArrDuration)
const map = new Map(data.map((el) => [el.id, el]))
const mapStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
const id = data[i % data.length].id
map.get(id)
}
const mapDuration = Date.now() - mapStartTime
console.log('map', mapDuration)
const associativeArr = Object.fromEntries(data.map((el) => [el.id, el]))
const associativeArrStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
const id = data[i % data.length].id
associativeArr[id]
}
const associativeArrDuration = Date.now() - associativeArrStartTime
console.log('associativeArr', associativeArrDuration)
結果
配列から添え字で検索 | 配列からfindで検索 | Mapから検索 | 連想配列から検索 | |
---|---|---|---|---|
1回目 | 42ms | 7260ms | 176ms | 39ms |
2回目 | 45ms | 7334ms | 173ms | 40ms |
3回目 | 45ms | 7359ms | 176ms | 43ms |
添え字による参照を行う配列と連想配列が最も早く、次いでMap、断トツで遅いのが find
による配列の検索でした。
ちなみに、配列に対して at
メソッドを使用した場合も検証しましたが、添え字で参照した場合と速度差はないようです。
IDが自然数ではない場合や抜け番あるような場合では配列のインデックスとIDを一致させる手法は使えないので、ID検索という点で言うと連想配列が最も使い勝手の良いリスト形式といえそうです。
とはいえ、連想配列の場合、キーを削除するには delete
演算子が必要だったり、要素数の計算が1つのメソッドで完結しないなどのデメリットもあるので、Mapの方が使い勝手の良いケースは多いように思います。
少なくとも自然数でないIDによって要素を検索することが多いのであれば配列は速度面で他の手法よりも劣ることがわかりました。(find
はもっと早いと思っていた……)
配列を空にする処理
配列の初期化をする際、配列のインスタンスを残す必要があるか否かで空配列を作る手法は変わりますが、単純にどちらが速度的に優れているのかが気になったので調べてみます。
空配列に対する処理
まずは、元々空の配列に対して空にする処理を実行した場合の純粋な処理速度を比較してみました。
実行した処理
const LOOP_LENGTH = 10**7
const data = Array.from({ length: 100 }, (_, i) => i)
let arr = []
const spliceStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
arr.splice(0)
}
const spliceDuration = Date.now() - spliceStartTime
console.log('splice', spliceDuration)
const lengthStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
arr.length = 0
}
const lengthDuration = Date.now() - lengthStartTime
console.log('length', lengthDuration)
const newStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
arr = []
}
const newDuration = Date.now() - newStartTime
console.log('new', newDuration)
結果
splice を使用 |
length に0を代入 |
新規の空配列を再代入 | |
---|---|---|---|
1回目 | 209ms | 268ms | 63ms |
2回目 | 199ms | 277ms | 67ms |
3回目 | 192ms | 272ms | 61ms |
要素数100個の配列に対する処理
実行した処理
100個の要素をもつ配列を空にする処理を500万回繰り返したときの結果を比較します。
(1000万回実行するとメモリ不足でクラッシュしたため実行回数を減らしました)
const LOOP_LENGTH = 10**6 * 5
const data = Array.from({ length: 100 }, (_, i) => i)
function createArrays() {
return Array.from({ length: LOOP_LENGTH }, () => Array.from(data))
}
const spliceArrays = createArrays()
const spliceStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
spliceArrays[i].splice(0)
}
const spliceDuration = Date.now() - spliceStartTime
console.log('splice', spliceDuration)
const lengthArrays = createArrays()
const lengthStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
lengthArrays[i].length = 0
}
const lengthDuration = Date.now() - lengthStartTime
console.log('length', lengthDuration)
const newArrays = createArrays()
const newStartTime = Date.now()
for (let i = 0; i < LOOP_LENGTH; i++) {
newArrays[i] = []
}
const newDuration = Date.now() - newStartTime
console.log('new', newDuration)
結果
splice を使用 |
length に0を代入 |
新規の空配列を再代入 | |
---|---|---|---|
1回目 | 735ms | 306ms | 53ms |
2回目 | 659ms | 320ms | 64ms |
3回目 | 692ms | 329ms | 46ms |
条件を変えて検証したところ、要素数が5つを超えたところで splice(0)
よりも length = 0
の方が速くなりました。
Discussion