Stopwatch
 

Benchmarking each_with_object Against inject when building Hashes from Arrays

 

I read Better Hash Injection using each_with_object with interest.

I’d long known that using Ruby’s Hash#merge! rather than Hash#merge was much faster: merge hash in place in memory, don’t copy and assign. I’d never come across each_with_index in the wild, at least and remembered.

What a fool I’ve been.

Rather than use code like either of these…

array_of_stuff.inject({}) do |result, element|
  result[element.id] = element.value
  result
end

array_of_stuff.inject({}) do |result, element|
  result.merge!(element.id => element.value)
end

…it’s much more idiomatic Ruby to use each_with_object.

array_of_stuff.each_with_object({}) do |element, result|
  result[element.id] = element.value
end

I was interested to see how this idiomatic Ruby performed. I put together a little script to test the various ways of generating a Hash from an decent-sized array of simple Struct-based objects. I used the benchmark-ips gem.

require 'benchmark/ips'

User = Struct.new(:id, :stuff)
a = Array.new(1000) { |i| User.new(i, stuff: rand(1000)) }

Benchmark.ips do |x|
  x.report('assign&return') do
    a.inject({}) { |memo, i| memo[i.id] = i.stuff; memo }
  end
  x.report('merge') do
    a.inject({}) { |memo, i| memo.merge(i.id => i.stuff) }
  end
  x.report('merge!') do
    a.inject({}) { |memo, i| memo.merge!(i.id => i.stuff) }
  end
  x.report('map with tuples') do
    a.map { |i| [i.id, i.stuff] }.to_h
  end
  x.report('each_with_object') do
    a.each_with_object({}) { |i, memo| memo[i.id] = i.stuff }
  end
end

The results were interesting.

assign&return 3136.7(±8.2%) i/s
merge 5.9(±0.0%) i/s
merge! 1168.0(±28.3%) i/s
map with tuples 2400.8(±23.0%) i/s
each_with_object 3220.8(±3.3%) i/s

Turns out the most idiomatic code is also the fastest. Followed surprisingly closely by the ‘do the simplest thing’ variant, but not by a huge amount.

PS If you’re using merge without the ! inside loops like this… just don’t.

photo by Sabri Tuzcu

Don’t miss my next post, sign up to the One Ruby Thing email and get my next post in your inbox.