Ruby
benchmark
RuboCop

[Ruby] Array.newとInteger#times.mapどっちが早い?(Performance/TimesMap)

概要

例えば以下のような、0から9の値それぞれを10倍した値を取得したい場合に、timesとmapを使って以下のようなコードを書いてみた。

result = 10.times.map do |i|
  i * 10
end

p result # [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

これをRuboCopに検査してもらうと

Inspecting 1 file
C

Offenses:

times.rb:1:10: C: Performance/TimesMap: Use Array.new(10) with a block instead of .times.map.
result = 10.times.map do |i| ...
         ^^^^^^^^^^^^^^^^^^^

1 file inspected, 1 offense detected

などと怒られる。どうやらRuboCop先生的には、パフォーマンス的に以下のように書いて欲しいらしい。

result = Array.new(10) do |i|
  i * 10
end

p result # [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

なるほど。Array.newにブロックを与えると、mapメソッドみたいに評価値の配列で初期化出来るみたい。

でもこれって本当にInteger#times.mapより早いの?仮にほんのすこしだけ、Array.new方式の方が早いのであれば、より可読性の高いInteger#times方式を採用したほうが良いんじゃないか。(ここでどちらの方が可読性が高いという話は置いておく)

benchmarkで比較する

ということで、benchmarkを使ってベンチマークしてみよう。

それなりの数字でやらないと差が出ないので、まずはN=100000から。

require 'benchmark'

N = 1000000
Benchmark.bm 10 do |r|
  r.report 'Integer#times' do
    N.times.map {|i| i * 10 }
  end
  r.report 'Array.new' do
    Array.new(N) {|i| i * 10 }
  end
end

誤差が出るので何度か実行してみる。

$ ruby times.rb
                 user     system      total        real
Integer#times  0.060000   0.000000   0.060000 (  0.068003)
Array.new      0.060000   0.000000   0.060000 (  0.062482)

$ ruby times.rb
                 user     system      total        real
Integer#times  0.050000   0.010000   0.060000 (  0.066772)
Array.new      0.060000   0.000000   0.060000 (  0.061637)

$ ruby times.rb
                 user     system      total        real
Integer#times  0.070000   0.000000   0.070000 (  0.070904)
Array.new      0.060000   0.000000   0.060000 (  0.060237)

$ ruby times.rb
                 user     system      total        real
Integer#times  0.070000   0.000000   0.070000 (  0.074295)
Array.new      0.060000   0.000000   0.060000 (  0.061608)

結構いい勝負だな・・・。

Nを増やしてみる。

N = 10000000
$ ruby times.rb
                 user     system      total        real
Integer#times  0.660000   0.030000   0.690000 (  0.684714)
Array.new      0.780000   0.050000   0.830000 (  0.832325)

ruby times.rb
                 user     system      total        real
Integer#times  0.680000   0.010000   0.690000 (  0.695185)
Array.new      0.560000   0.020000   0.580000 (  0.579955)

$ ruby times.rb
                 user     system      total        real
Integer#times  0.600000   0.030000   0.630000 (  0.632446)
Array.new      0.530000   0.030000   0.560000 (  0.557176)

$ ruby times.rb
                 user     system      total        real
Integer#times  0.640000   0.020000   0.660000 (  0.661193)
Array.new      0.540000   0.030000   0.570000 (  0.581285)

あ〜、たまに逆転することもあるけど、Array.newのほうが早いことのほうが多い。

benchmark/ips で比較する

この章は@scivola様よりコメントでご指摘いただいた内容を元に追記しました。(2018/04/10)
ご教授頂きありがとうございます!

benchmark/ipsは、処理の試行回数を意識しなくても繰り返し処理を実行して安定した結果を出せるみたい。1秒あたりに何回実行できたかの情報なども出て、比較もしやすそう。

インストール

$ gem install benchmark-ips

benchmark-ispを用いて先程のコードを改良する。

require 'benchmark/ips'

N = 10000

Benchmark.ips do |r|
  r.report 'Integer#times' do
    N.times.map { |i| i * 10 }
  end

  r.report 'Array.new' do
    Array.new(N) { |i| i * 10 }
  end

  r.compare!
end

実行すると以下のような集計が得られる。

Warming up --------------------------------------
       Integer#times   147.000  i/100ms
           Array.new   173.000  i/100ms
Calculating -------------------------------------
       Integer#times      1.555k (± 7.6%) i/s -      7.791k in   5.042171s
           Array.new      1.806k (± 6.0%) i/s -      9.169k in   5.096181s

Comparison:
           Array.new:     1805.7 i/s
       Integer#times:     1554.6 i/s - 1.16x  slower

上記結果から、Array.newは秒1805回、Integer#timesは秒1554回実行できので、やはりRuboCopの指摘どおりArray.newのほうが早いことがわかった。

結論

大差があるわけじゃないけど、まぁArray#newのほうが早かった。とりあえずパフォーマンスに関してはRuboCopに従っておいた方が良さそう。

でもサイズNの配列を確保してたらメモリ消費量的にはどうなんだろう・・・。