LoginSignup
30
12

More than 5 years have passed since last update.

Rubyで配列の要素を合計するベストな方法

Posted at

Ruby でベンチマークを取る方法を読んでいて、配列の合計値の計算方法っていろいろあるよなーと思ったので。

配列の要素を合計する方法

  1. 素直に一つずつeachでループして足していく
  2. Enumerable#injectにブロック(ないしはProcオブジェクト)を渡す
  3. Enumerable#injectにシンボルを渡す
  4. sumメソッド(Ruby2.4以上)
# 1
sum = 0
array.each { |i| sum += i }
# 2
# array.inject { |sum, i| sum + i } と等価
array.inject(&:+)
# 3
array.inject(:+)
# 4
array.sum

うーん、さすがTMTOWTDI

結局どれが一番速いの?

というわけで、上記記事のコードを元に、以下のコードでベンチマークを取りました。
if array.respond_to?(:sum)としているのは、古いバージョンでも試したためです。

require 'benchmark'

array = 1000.times.map { rand }
num_iteration = 50000

Benchmark.bmbm 10 do |r|
  r.report "inject(&:+)" do
    num_iteration.times do
      sum = array.inject(&:+)
    end
  end

  r.report "inject(:+)" do
    num_iteration.times do
      sum = array.inject(:+)
    end
  end

  r.report "each" do
    num_iteration.times do
      sum = 0
      array.each{|x| sum += x}
    end
  end

  if array.respond_to?(:sum)
    r.report "sum" do
      num_iteration.times do
        sum = array.sum
      end
    end
  end
end

結果

Ruby 2.5.0

                  user     system      total        real
inject(&:+)   4.212000   0.000000   4.212000 (  4.212325)
inject(:+)    2.208000   0.000000   2.208000 (  2.208990)
each          3.700000   0.000000   3.700000 (  3.703916)
sum           0.356000   0.000000   0.356000 (  0.355434)

Ruby 2.3.0

                  user     system      total        real
inject(&:+)   4.920000   0.000000   4.920000 (  4.961357)
inject(:+)    4.500000   0.000000   4.500000 (  4.502774)
each          3.890000   0.000000   3.890000 (  3.892442)

Ruby 1.8.7p357

                  user     system      total        real
inject(&:+)  15.060000   0.000000  15.060000 ( 15.074112)
inject(:+)    6.190000   0.000000   6.190000 (  6.197370)
each         10.650000   0.000000  10.650000 ( 10.669348)

sumが最強

Ruby2.5では他に6倍以上の差を付けてsumの圧勝です。
2.4でも結果は同様でした。

ちなみにsumはFloatの誤差が小さくなるように実装されているので、結果も他3つより正確です。

inject(:+)は2.4で

  1. レシーバーが配列で
  2. 引数がシンボル

の場合の最適化処理が入ったようで、2.5では速くなっています。

2.3ではeachが多少速いですが、何度かやると入れ替わることもあり、あまり大差はない印象です。

もはや誰も使ってないでしょうが、1.8はinject(:+)の圧勝(と言ってもかなり遅い)。ブロック呼び出しが遅いんでしょうか。
いずれにしてもYARVは偉大。

ちなみに

injectのレシーバーが配列、引数がシンボルなら2.4以降では最適化処理が入ると言いましたが、Rubyのソースコードを眺めていると、配列の要素がIntegerだともう一段最適化が入るようです。

そこで、

array = 1000.times.map { rand(1000) }

として、もう一度2.5で試しました。

                  user     system      total        real
inject(&:+)   3.804000   0.000000   3.804000 (  3.823945)
inject(:+)    0.060000   0.000000   0.060000 (  0.058624)
each          2.804000   0.000000   2.804000 (  2.812767)
sum           0.060000   0.000000   0.060000 (  0.059265)

おお、inject(:+)sum並に速くなりました!
整数なら丸め誤差もないですし、inject(:+)もいいかもしれません。

結論

Ruby 2.4以降なら、速くて誤差も小さいArray#sumほぼ一択。ただし要素が全て整数ならinject(:+)もあり。

Ruby2.3までなら、どれもさほど変わらないのでお好きなものを(個人的にはinject(:+)が簡潔で好きです)。

30
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
12