Ruby Hash[key] Showdown :symbol vs “string”

Who cares?

There was recently a discussion on Trailblazer Gitter channel about Hashes as params, how to pass them around, and as customary a flame-war war ~~ensued~~ never happened, and it came down to a measuring contest: ~~whose~~ which key is better and faster.

For the impatient: for small Hashes it doesn’t really matter, for larger ones :symbol is 2x faster than string’ keys. Frozen string’ keys are almost as fast as :symbol keys.

The best way to argue is to present facts. So I coded a couple of benchmarks and submitted a pull request to fast-ruby (Github). Here are the details.

Round 1: Hash[:symbol] vs Hash[“string”]

First, lets measure allocating Hash in various ways that Ruby gives us

require "benchmark/ips"

def symbol_key
  {symbol: 42}

def symbol_key_arrow
  {:symbol => 42}

def symbol_key_in_string_form
  {'sym_str': 42}

def string_key_arrow_double_quotes
  {"string" => 42}

def string_key_arrow_single_quotes
  {'string' => 42}

Full code for the benchmark is here on Github and the results on rig with 8 cores and 16 GB of RAM, running on Ruby 2.4.2

        {symbol: 42}:  1731221.3 i/s
     {:symbol => 42}:  1714113.4 i/s - same-ish
     {'sym_str': 42}:  1711084.8 i/s - same-ish
    {"string" => 42}:  1508413.1 i/s - 1.15x  slower
    {'string' => 42}:  1452896.9 i/s - 1.19x  slower

So while 19% slower may be a fairly big difference, keep in mind that this is for 1.5 million iterations per second. Who in their right mind creates 1,500,000 single pair hashes per seconds? Right?

string keys are not going to be the speed bottleneck in your app.

Round 2: But what about large hashes?

Don’t worry I got you covered. You can check out specific benchmark right here on Github. Let’s try it out for a 1000 key-value pairs.

require "benchmark/ips"

STRING_KEYS = (1..1000).map{|x| "key_#{x}"}.shuffle
FROZEN_KEYS ={|x| "fr_#{x}".freeze}

# If we use static values for Hash, speed improves even more.
def symbol_hash
  SYMBOL_KEYS.collect { |k| [ k, rand(1..100)]}.to_h

def string_hash
  STRING_KEYS.collect { |k| [ k, rand(1..100)]}.to_h

# See this article for the discussion of using frozen strings instead of symbols
def frozen_hash
  FROZEN_KEYS.collect { |k| [ k, rand(1..100)]}.to_h

SYMBOL_HASH = symbol_hash
STRING_HASH = string_hash
FROZEN_HASH = frozen_hash

def reading_symbol_hash

def reading_string_hash

def reading_frozen_hash

Full benchmark code . Let’s see the results:

Creating large Hash

         Symbol Keys:     3262.0 i/s
         Frozen Keys:     3023.2 i/s - same-ish
         String Keys:     2476.7 i/s - 1.32x  slower

Reading large Hash

         Symbol Keys:  5280882.7 i/s
         Frozen Keys:  4791128.0 i/s - same-ish
         String Keys:  4275730.5 i/s - 1.24x  slower

So as we can see on a larger Hashes with 1000’s of keys, the difference becomes more apparent and being mindful of it can help improve speed, if you are using a lot of large hashes. Otherwise, my recommendation, keep using what works better for you app. If string keys make it easier, let’s say you are importing them from an external file, don’t go through the trouble of converting them to symbols, it’s probably not worth it.

But don’t take my word for it. Just measure it.

Also published on Medium.

Published by

Nick Gorbikoff

Nick Gorbikoff is a Ruby & React DevOps engineer and hands-on CTO working on ERP Systems. In his spare time, Nick is a fearless follower of 2 kids and an awesome wife, collector of useless historical trivia, tinkerer of sorts, fast food connoisseur and aspiring martial artist.

5 thoughts on “Ruby Hash[key] Showdown :symbol vs “string””

  1. Unknown Unknown Unknown Unknown

    Good post Nick. Came across some confusion on this myself recently, where a JSON body posted to a controller with the “Content-Type” header was being deserialised into the params hash with string based keys, yet other request params are symbolised, so the resulting hash was mixed both symbols and strings, very messy.

    1. Unknown Unknown Unknown Unknown

      Thanks Robert

      There is HashWithIndiffentAccess provided by Rails for that. I actually should have benchmarked that as well just for completeness .

      But I agree for consistency sake it’s better to stick with one or the other.

  2. Unknown Unknown Unknown Unknown

    Hello, your code snippet block arguments are coming across incorrectly. All of the block args are either naked, or wrapped in "x" instead of |x|. They were obviously correct in your original code, but it would be helpful if you could update them in the blog post as well, as it was distracting, and had me wondering if I was just not understanding something fancy.

    1. Google Chrome 62.0.3202.75 Google Chrome 62.0.3202.75 Mac OS X  10.13.1 Mac OS X 10.13.1
      Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36

      Thank you Tim!

What do you think?