Leo Brown Leo Brown - 1 month ago 6
Ruby Question

Is sum or reduce(:+) better in Ruby/Rails? Are there considerations other than speed?

It seems like #sum is faster than #reduce for long arrays, and they are basically the same for short ones.

def reduce_t(s,f)
start = Time.now
puts (s..f).reduce(:+) #Printing the result just to make sure something is happening.
finish = Time.now
puts finish - start
end
def sum_t(s,f)
start = Time.now
puts (s..f).sum
finish = Time.now
puts finish - start
end

irb(main):078:0> sum_t(1,10); reduce_t(1,10)
55
0.000445
55
0.000195
=> nil
irb(main):079:0> sum_t(1,1000000); reduce_t(1,1000000)
500000500000
8.1e-05
500000500000
0.101487
=> nil


Are there considerations other than speed? Are there any situations when it would be better to use #reduce instead of #sum to accomplish the same end, a simple sum?

Edit

mu is too short rightly pointed out that I should do numerous iterations before drawing conclusions about timing results. I didn't use
Benchmark
because I'm not familiar with it yet, but I hope what I've written below will be adequate and convincing.

def sum_reduce_t(s,f)
time_reduce = 0
time_sum = 0
reduce_faster = 0
sum_faster = 0
30.times do
start_reduce = Time.now
(s..f).reduce(:+)
finish_reduce = Time.now
time_reduce += (finish_reduce - start_reduce)
start_sum = Time.now
(s..f).sum
finish_sum = Time.now
time_sum += (finish_sum - start_sum)
if time_sum > time_reduce
reduce_faster += 1
else
sum_faster += 1
end
end
puts "Total time (s) spent on reduce: #{time_reduce}"
puts "Total time (s) spent on sum: #{time_sum}"
puts "Number of times reduce is faster: #{reduce_faster}"
puts "Number of times sum is faster: #{sum_faster}"
end

irb(main):205:0> sum_reduce_t(1,10)
Total time (s) spent on reduce: 0.00023900000000000004
Total time (s) spent on sum: 0.00015400000000000003
Number of times reduce is faster: 0
Number of times sum is faster: 30
=> nil
irb(main):206:0> sum_reduce_t(1,100)
Total time (s) spent on reduce: 0.0011480000000000004
Total time (s) spent on sum: 0.00024999999999999995
Number of times reduce is faster: 0
Number of times sum is faster: 30
=> nil
irb(main):207:0> sum_reduce_t(1,1000)
Total time (s) spent on reduce: 0.004804000000000001
Total time (s) spent on sum: 0.00019899999999999996
Number of times reduce is faster: 0
Number of times sum is faster: 30
=> nil
irb(main):208:0> sum_reduce_t(1,10000)
Total time (s) spent on reduce: 0.031862
Total time (s) spent on sum: 0.00010299999999999996
Number of times reduce is faster: 0
Number of times sum is faster: 30
=> nil
irb(main):209:0> sum_reduce_t(1,100000)
Total time (s) spent on reduce: 0.286317
Total time (s) spent on sum: 0.00013199999999999998
Number of times reduce is faster: 0
Number of times sum is faster: 30
=> nil
irb(main):210:0> sum_reduce_t(1,1000000)
Total time (s) spent on reduce: 2.7116779999999996
Total time (s) spent on sum: 0.00021200000000000008
Number of times reduce is faster: 0
Number of times sum is faster: 30
=> nil


My question remains: are there ever times when it makes sense to use #reduce instead of #sum?

Answer

One way that the behaviour and result of using sum differ from inject &:+ is when you are summing floating point values.

If you add a large floating point value to a small one, often the result is just the same as the larger one:

> 99999999999999.98 + 0.001
=> 99999999999999.98

This can lead to errors when adding arrays of floats, as the smaller values are effectively lost, even if there is a lot of them.

For example:

> a = [99999999999999.98, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001]
=> [99999999999999.98, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001]
> a.inject(&:+)
=> 99999999999999.98

In this example, you could add 0.001 as often as you want, it would never change the value of the result.

Ruby’s implementation of sum uses the Kahan summation algorithm when summing floats to reduce this error:

> a.sum
=> 100000000000000.0

(Note the result here, you might be expecting something ending in .99 as there are 10 0.001 in the array. This is just normal floating point behaviour, perhaps I should have tried to find a better example. The important point is that the sum does increase as you add lots of small values, which doesn’t happen with inject &:+.)