Rustでアルゴリズムの問題を解いていた時に、計算量的には通るはずのコードが通らず…配列の操作が怪しそうだったので配列の追加・削除の速度について調査しました。
前提として、今取り組んでいるアルゴリズムの問題は実行時間を1sに収める必要があるかつ計算回数が約350万回です。
100万回操作を行うコードを書いて処理にかかった時間に3.5をかけ、1000msを超えてるケースがないか確認しました。
環境
IntelliJ IDEA 2021.1.3
rustc 1.46.0 (04488afe3 2020-08-24)
固定長配列の要素追加
実行したコードは下記になります。
1 | let mut array = [0; 1_000_000]; |
計測結果は
1回目→42ms
2回目→51ms
3回目→43ms
で平均は45.3msでした。
45.3 * 3.5 = 約160msなので原因にはなりづらそうです。
可変長配列の要素追加
実行したコードは下記になります。
1 | let mut array = Vec::new(); |
計算結果は
1回目→62ms
2回目→63ms
3回目→63ms
で平均は62.7msでした
62.7 * 3.5 = 約220msでこれも原因ではなさそうです。
可変長配列の要素削除(先頭)
実行したコードは下記になります。
1 | let mut array = Vec::new(); |
計算結果は
1回目→124906ms
2回目→107748ms
3回目→99213ms
で平均は110622.3msでした。
完全にこれが原因でした。
VectorでQueueのような挙動を実現しようとしてremove()
を使っていたのですが悪かったようです。
【おまけ】可変長配列の要素削除(末尾)
Vectorはスタックとして扱うことができpush()
と対となるpop()
が準備されています。
それを使ったときの速度がremove()
と同じになるか気になったので試してみました。
実行したコードは下記になります。
1 | let mut array = Vec::new(); |
計算結果は
1回目→82ms
2回目→72ms
3回目→72ms
で平均は75.3msでした。
この速度の違いはいったい…
Rustのコードを見てみたら納得しました。pop()
は単純に今持っている配列のサイズ-1番目の要素を削除するだけですが、remove()
はindexで指定した位置の要素を削除した後に、指定した位置より後ろにある要素たちをコピーして削除した要素を埋める形でずらしているようです。
今回の場合、indexの指定が常に0なのでremoveとの相性がかなり悪かったです。