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 insued never happened, and it came down to a measuring contest: whose which key is better and faster.
TLDR;
For small hashes it doesn’t really matter, for larger ones :symbol is 1.15x faster than string’ keys. Frozen string’ keys are in-between.
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}
end
def symbol_key_arrow
{:symbol => 42}
end
def symbol_key_in_string_form
{'sym_str': 42}
end
def string_key_arrow_double_quotes
{"string" => 42}
end
def string_key_arrow_single_quotes
{'string' => 42}
end
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
Warming up --------------------------------------
{symbol: 42} 1.929M i/100ms
{:symbol => 42} 1.878M i/100ms
{'sym_str': 42} 1.946M i/100ms
{"string" => 42} 1.942M i/100ms
{'string' => 42} 1.920M i/100ms
Calculating -------------------------------------
{symbol: 42} 19.160M (± 1.7%) i/s (52.19 ns/i) - 96.429M in 5.034458s
{:symbol => 42} 18.991M (± 1.0%) i/s (52.66 ns/i) - 95.802M in 5.045019s
{'sym_str': 42} 19.097M (± 1.7%) i/s (52.37 ns/i) - 97.284M in 5.095874s
{"string" => 42} 19.093M (± 4.0%) i/s (52.38 ns/i) - 97.098M in 5.095656s
{'string' => 42} 18.816M (± 5.3%) i/s (53.15 ns/i) - 94.098M in 5.018065s
Comparison:
{symbol: 42}: 19159666.9 i/s
{'sym_str': 42}: 19096638.6 i/s - same-ish: difference falls within error
{"string" => 42}: 19092784.0 i/s - same-ish: difference falls within error
{:symbol => 42}: 18991332.9 i/s - same-ish: difference falls within error
{'string' => 42}: 18816259.9 i/s - same-ish: difference falls within error
Used to be a difference on Ruby 2.*, but running benchmark on Ruby 3.3.5 - we can see that style doesn’t really matter. Using string "42"
for values instead of number 42
, will slightly change the benchmark, but insignificant in the grand scheme of things.
Don’t overcomplicate
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. Lets try it out for a 1000 key-value pairs.
require "benchmark/ips"
STRING_KEYS = (1001..2000).map{|x| "key_#{x}"}.shuffle
FROZEN_KEYS = (2001..3000).map{|x| "key_#{x}".freeze}.shuffle
SYMBOL_KEYS = (3001..4000).map{|x| "key_#{x}".to_sym}.shuffle
# If we use static values for Hash, speed improves even more.
def symbol_hash
SYMBOL_KEYS.collect { |k| [ k, rand(1..100)]}.to_h
end
def string_hash
STRING_KEYS.collect { |k| [ k, rand(1..100)]}.to_h
end
# See this article for the discussion of using frozen strings instead of symbols
# http://blog.arkency.com/could-we-drop-symbols-from-ruby/
def frozen_hash
FROZEN_KEYS.collect { |k| [ k, rand(1..100)]}.to_h
end
SYMBOL_HASH = symbol_hash
STRING_HASH = string_hash
FROZEN_HASH = frozen_hash
def reading_symbol_hash
SYMBOL_HASH[SYMBOL_KEYS.sample]
end
def reading_string_hash
STRING_HASH[STRING_KEYS.sample]
end
def reading_frozen_hash
FROZEN_HASH[FROZEN_KEYS.sample]
end
Benchmark.ips do |x|
puts "Creating large Hash"
x.report("Symbol Keys") { symbol_hash }
x.report("String Keys") { string_hash }
x.report("Frozen Keys") { frozen_hash }
x.compare!
end
Benchmark.ips do |x|
puts "Reading large Hash"
x.report("Symbol Keys") { reading_symbol_hash }
x.report("String Keys") { reading_string_hash }
x.report("Frozen Keys") { reading_frozen_hash }
x.compare!
end
Let’s see the results:
Creating large Hash
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
Warming up --------------------------------------
Symbol Keys 912.000 i/100ms
String Keys 670.000 i/100ms
Frozen Keys 819.000 i/100ms
Calculating -------------------------------------
Symbol Keys 8.836k (± 2.7%) i/s (113.17 μs/i) - 44.688k in 5.061218s
String Keys 6.580k (± 2.7%) i/s (151.98 μs/i) - 33.500k in 5.095462s
Frozen Keys 7.988k (± 2.7%) i/s (125.18 μs/i) - 40.131k in 5.027532s
Comparison:
Symbol Keys: 8836.2 i/s
Frozen Keys: 7988.3 i/s - 1.11x slower
String Keys: 6579.6 i/s - 1.34x slower
Reading large Hash
ruby 3.3.5 (2024-09-03 revision ef084cc8f4) [arm64-darwin24]
Warming up --------------------------------------
Symbol Keys 1.536M i/100ms
String Keys 1.239M i/100ms
Frozen Keys 1.263M i/100ms
Calculating -------------------------------------
Symbol Keys 15.299M (± 1.4%) i/s (65.36 ns/i) - 76.817M in 5.022118s
String Keys 12.283M (± 3.7%) i/s (81.41 ns/i) - 61.971M in 5.053602s
Frozen Keys 13.216M (± 1.4%) i/s (75.67 ns/i) - 66.925M in 5.065031s
Comparison:
Symbol Keys: 15299046.1 i/s
Frozen Keys: 13215920.7 i/s - 1.16x slower
String Keys: 12282846.0 i/s - 1.25x slower
So as we can see on a larger Hashes with with 1000’s of keys, the difference become 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.