【Ruby】each_with_index と each.with_index はどっちが速いのか?

これは、フィヨルドブートキャンプ Part 2 Advent Calendar 2021 - Adventar の 3日目の記事です。

Part1 もあります : フィヨルドブートキャンプ Part 1 Advent Calendar 2021 - Adventar

はじめに

※コメントでご指摘していただいているように、each_with_index と each.with_index のどちらが速いかを結論付けるには不十分な内容になっています。タイトルの結論が出ておらず申し訳ありませんが、1つの参考記録として読んでいただけるとありがたいです。追加で検証をするかは未定です。 (2021/12/20 追記)

Rubyを学び始めると、Enumerable#each_with_indexEnumerable#with_index の2つのメソッドについて知る人は多いのではないかと思います。

Enumerable#each_with_index (Ruby 3.0.0 リファレンスマニュアル) Enumerator#with_index (Ruby 3.0.0 リファレンスマニュアル)

with_indexeach.with_index のように使うことで each_with_index とは違って offset 引数を受け取れる面で便利ですが、速度はどうなのでしょうか?

調べてみると、古い記事ですが次のようなものが見つかりました。

... from the point of view of speeding up the code, each_with_index runs slightly faster than each.with_index.

出典 : difference between each.with_index and each_with_index in Ruby? - Stack Overflow

slightly (わずかに) って実際どれくらいなんだと疑問に思ったので計測してみます。

検証環境

処理速度の計測には、benchmark を使います。

(benchmark の使い方については、Ruby でベンチマークを取る方法 - Qiita を参考にさせていただきました。)

検証結果

配列の要素数100件、1000件、1万件それぞれに対して繰り返し処理を実行した結果、

  • 100件では each_with_index の方が速い
  • 1000件、1万件では each.with_index の方が速い

という結果になりました。

3つのケースしか調べていないため断言はできないですが、差はいずれも0.01秒もないので、どっちを使っても大差はないと思います。

詳細

まず100件。

require 'benchmark'
array = Array.new(100) { |i| i }
Benchmark.bm 15 do |r|
  r.report 'each_with_index' do
    array.each_with_index { |n, i| x = [n, i] }
  end
  r.report 'each.with_index' do
    array.each.with_index { |n, i| x = [n, i] }
  end
end
#
# 1回目
# =>
#                       user     system      total        real
# each_with_index   0.000016   0.000003   0.000019 (  0.000013)
# each.with_index   0.000663   0.000001   0.000664 (  0.000665)
#
# 2回目
# =>
#                       user     system      total        real
# each_with_index   0.000017   0.000005   0.000022 (  0.000014)
# each.with_index   0.000772   0.000001   0.000773 (  0.000775)

まさに slightly (わずかに) each_with_index のほうが速い。

次は1000件で。

require 'benchmark'
array = Array.new(1000) { |i| i }
Benchmark.bm 15 do |r|
  r.report 'each_with_index' do
    array.each_with_index { |n, i| x = [n, i] }
  end
  r.report 'each.with_index' do
    array.each.with_index { |n, i| x = [n, i] }
  end
  end
end
#
# 1回目
# =>
#                       user     system      total        real
# each_with_index   0.000711   0.000004   0.000715 (  0.000709)
# each.with_index   0.000085   0.000002   0.000087 (  0.000085)
#
# 2回目
# =>
#                       user     system      total        real
# each_with_index   0.000657   0.000004   0.000661 (  0.000656)
# each.with_index   0.000077   0.000001   0.000078 (  0.000077)

each_with_index の方が遅くなりました。 しかも、each.with_index は100件の場合より速くなっている。

更に1万件で。

require 'benchmark'
array = Array.new(10_000) { |i| i }
Benchmark.bm 15 do |r|
  r.report 'each_with_index' do
    array.each_with_index { |n, i| x = [n, i] }
  end
  r.report 'each.with_index' do
    array.each.with_index { |n, i| x = [n, i] }
  end
end
#
# 1回目
# =>
#                       user     system      total        real
# each_with_index   0.002912   0.000071   0.002983 (  0.002979)
# each.with_index   0.000889   0.000096   0.000985 (  0.000985)
#
# 2回目
# =>
#                       user     system      total        real
# each_with_index   0.003260   0.000034   0.003294 (  0.003292)
# each.with_index   0.001105   0.000093   0.001198 (  0.001198)

1万件でも each.with_index の方が速い結果になりました。

終わりに

検証の結果、この記事が有用なTipsになった感じはしませんが、個人的に気になっていたことをアドベントカレンダーをきっかけに調べてみることができてよかったです。

ということで、フィヨルドブートキャンプ Part 2 Advent Calendar 2021 - Adventar の3日目の記事でした。

(Part1はこちら : フィヨルドブートキャンプ Part 1 Advent Calendar 2021 - Adventar