Ruby でベンチマークを取る方法を読んでいて、配列の合計値の計算方法っていろいろあるよなーと思ったので。
配列の要素を合計する方法
- 素直に一つずつ
each
でループして足していく -
Enumerable#inject
にブロック(ないしはProcオブジェクト)を渡す -
Enumerable#inject
にシンボルを渡す -
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で
- レシーバーが配列で
- 引数がシンボル
の場合の最適化処理が入ったようで、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(:+)
が簡潔で好きです)。