Stopwatch

Sabri Tuzcu

Benchmarking each_with_object Against inject when building Hashes from Arrays

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.

Last updated on October 21st, 2014