見出し画像

Rubyの配列繰り返し処理の速度を調べてみる

こんにちは。スペースマーケット バックエンドエンジニアの北島です。

ゴールデンウィークはいかがお過ごしでしたでしょうか。私は「ついさっきも日の出を見た気がする」と毎日時間が過ぎる速度に驚いていました。休日の時間と平日の時間って、明らかに進む速度が違いますよね。どうしてでしょうね、速度って不思議ですよね。そうですね、改めてRubyの繰り返し処理の速度を知りたいですよね。

なるべく純粋な繰り返しで測ってみる

Rubyには配列繰り返し処理に使えるメソッドや制御構造がいくつかあり、代表的なもので言えばeachやfor,whileなどが挙げられます。内部実装的には他の処理と同様で、シンタックスシュガーとして提供されているものもあります。

よくみるものからあまり見ないものまで、なるべく何もしない純粋な繰り返しのみのベンチマークをとってみました。

require 'benchmark'

max_num = 10**7

Benchmark.bm 7 do |r|
 r.report 'each' do
   (1..max_num).each do
   end
 end

 r.report 'for' do
   for v in (1..max_num) do
   end
 end

 r.report 'map' do
   (1..max_num).map {}
 end

 r.report 'while' do
   v = 0
   v += 1 while v <= max_num
 end

 r.report 'loop' do
   v = 1
   loop do
     v += 1
     break if v >= max_num
   end
 end

 r.report 'times' do
   max_num.times do
   end
 end

 r.report 'upto' do
   1.upto max_num do
   end
 end

 r.report 'step' do
   1.step(max_num, 1) do
   end
 end
end

Ruby2.6系での実行です。Rubyには標準ライブラリにベンチマーク用のライブラリがあるので使っています。繰り返し回数は一千万回としました。手元のPCで時間的にさくっと実行完了できる回数という基準です。

手元のローカル環境で試しており、一度の実行だけでは結果にブレがありそうです。そこで10回計測し平均を求めることにしました。Benchmarkライブラリのreportでいうところのtotalの値をとっています。

結果は以下です。

処理名	avg(total)
while	0.1400025
times	0.3698695
step	0.370491
upto	0.3705193
each	0.3809972
for	    0.3900511
loop	0.4971561
map	    0.612236

whileが圧倒的にはやいですね。反対に、mapはかなり遅い結果となってしまいました。そもそもmapは既存配列をマッピングする(新たな配列を返す)用途で使われるものですので、ループを回したいだけであれば他の処理に軍配が上がってしまうのも仕方ありません。times,step,upto,each,forはさほど差はないですね。loopは一段速度が落ちるようです。

繰り返し内に単純な処理がある場合を測ってみる

では、mapの得意領域で計測してみます。新たな配列を作り、元配列の各要素に2を掛けた値を詰めていきます。

Benchmark.bm 7 do |r|
 r.report 'each2' do
   results = []
   (1..max_num).each do |v|
     results << v * 2
   end
 end

 r.report 'for2' do
   results = []
   for v in (1..max_num) do
     results << v * 2
   end
 end

 r.report 'map2' do
   results = (1..max_num).map { |v| v * 2 }
 end

 r.report 'while2' do
   results = []
   v = 1
   while v <= max_num
     results << v * 2
     v += 1
   end
 end

...(略)
end

極々単純な処理ですが、内容的にはmapを使うのが相応しいようなケースになっているかと思います。これでmapの本領発揮がみられるはずです。

結果は以下です。

処理名	avg
while	0.3360308
step	0.5334159
times	0.5351522
upto	0.5373073
each	0.543822
for	    0.5672487
map	    0.6312718
loop	0.6748546

引き続きwhileははやいですね。step,times,uptoはほぼ同等でしょうか。each,forの差が少し大きいですね。forの内部実装はeachだと聞いたことがあるので、forの方に構文解析にかかる時間分上乗せされていると考えれば妥当に思えます。mapは引き続き遅めではありますが、何もしない繰り返しよりは差が縮まっています。より複雑な処理を書いた場合は同等の速度までいく可能性も出てきますね。

用途・可読性・コード量・速度

単純な繰り返し速度ではwhileが一番はやそう、という結論となりました。map,loopは遅めです。times,uptoあたりを積極的に使ったことはありませんでしたが、繰り返し回数が明確な場合は可読性の面から見ても一考の余地がありそうですね。一方、forを使う理由は正直今のところ見当たりませんね。eachの方がいいような気がしています。

一応ご注意いただきたいのですが「はやいから全部whileで書こう」という主張をするつもりはありません。作りたいものに合った適切な書き方をするのがベスト、と考えております。昔の話ですが、当時私はforeachやforに比べてmapにオシャレさを感じていて、繰り返し処理をほとんどmapで書いたPRを出してしまったことがあります。コードレビューで「mapじゃない方がはやいし、副作用があるということが伝わりやすいよ。」と指摘を受けた思い出があります。そういった経験から「はやいからwhile一辺倒!」ではなくて、作りたい処理にあった用途のメソッド・制御構文を利用すること、可読性やコード量を意識すること、速度とのトレードオフをどこまで許容するか意識すること等、状況によって判断する多角的な視点が必要だと考えています。

おわりに

他言語の経験から「forがはやいんだろうな、でもRuby界ではよくeachを見るよなあ、じゃあeachがはやいのかなあ」というあやふやなイメージを持っていました。ある程度一つの言語に慣れていると、その言語での常識のようなものに引きずられて他言語でも手癖でコードを書いてしまいますよね。今回改めて基本的な構文の速度計測をして、意識をアップデートする良い機会になりました。このような極々簡単な計測でも「こういう時どうループさせようか」という迷いを断ち切る根拠の一つを得られたのは大きいと思っています。

未経験や経験の浅い言語を学ぶ時、こういった基本的なメソッド・制御構文の速度を測ると発見があり楽しいですよね。弊社ではRuby,Node.js,JS&TS,Golang等複数の言語・技術に触れられるので、色々な言語のクセや技術的な豆知識を知る機会が多いです。ちょっと気になったから、とその言語の思想や制御構文の実装を調べ始めたりすると時間がすごい速度で過ぎ去っていきます。やっぱり、速度って不思議ですね。

そんな複数言語を実務で扱える環境の弊社ですが、今まさにバックエンドエンジニアを募集しております。新しい技術を取り入れやすい文化もあり、色々な技術に触れてみたい、学びたい方はピッタリかと思いますので、是非下記リンクよりご応募お待ちしております。


この記事が気に入ったらサポートをしてみませんか?