Hashes have been optimized for symbols and strings in Ruby which technically are objects but this article is for revealing how much of a difference this makes when using other objects as hash keys. There are some cases where this makes a big difference but many times you won’t notice much of a difference.
I wrote a little experiment to see how different kinds of keys would perform for a hash. It’s been very common for me to use the self reference as a key in a hash and now I know that’s not good for performance. Here’s the code.
A = Object.new def a :result end def value_of [1] end hash_of = { a: [1], "a" => [1], result: [1], A => [1] } require "benchmark/ips" Benchmark.ips do |x| x.report("Method Call") do value_of() end x.report("Hash w/ Symbol key") do hash_of[:a] end x.report("Hash w/ String key") do hash_of["a"] end x.report("Hash w/ Method key") do hash_of[a] end x.report("Hash w/ Constant key") do hash_of[A] end x.compare! end
The first part of the code is setting up objects and methods to act as keys for the hash, then there’s the hash, and finally the benchmark. Here are the results.
Warming up -------------------------------------- Method Call 144.797k i/100ms Hash w/ Symbol key 164.880k i/100ms Hash w/ String key 156.820k i/100ms Hash w/ Method key 141.293k i/100ms Hash w/ Constant key 92.234k i/100ms Calculating ------------------------------------- Method Call 5.511M (± 2.7%) i/s - 27.656M in 5.021800s Hash w/ Symbol key 7.581M (± 2.7%) i/s - 37.922M in 5.006266s Hash w/ String key 6.620M (± 2.9%) i/s - 33.089M in 5.002952s Hash w/ Method key 4.542M (± 5.0%) i/s - 22.748M in 5.022166s Hash w/ Constant key 1.837M (± 1.0%) i/s - 9.223M in 5.022241s Comparison: Hash w/ Symbol key: 7580536.5 i/s Hash w/ String key: 6619759.4 i/s - 1.15x slower Method Call: 5511494.2 i/s - 1.38x slower Hash w/ Method key: 4541697.4 i/s - 1.67x slower Hash w/ Constant key: 1836695.9 i/s - 4.13x slower
So any time you index a hash with an object as a key you are having your code look up the result 313% slower than it would with a Symbol type object.
Summary
I had always heard that symbols were that faster than strings in hash lookups, but I wasn’t aware that hashes were faster than method calls (see comment section below) or how slow objects were for keys. It’s okay to use objects as hash keys if you really want to. Just know that you pay a small price for doing so.
Where you really need to be more concerned with this is when you implement some code that will be used a lot in your code base. So when you implement something like a raw type which may be called thousands of times in one run this is where that difference really matters. Generally you don’t have to worry about this performance loss as Ruby itself is fast and in most cases the code written doesn’t get called that much.
One example of where it would make a big difference is the Pathname class in Ruby’s standard library. In older versions of Rails this was called many thousands of times per request because of the asset pipeline. I wrote the gem FasterPath to implement this heavily used code in Rust just to improve performance. The more times the code is called in a small time frame, the more you should keep performance in mind.
Hopefully you found this information useful! I know this is a short post. Let me know if you like posts like this and I’ll write more. Please feel free to comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan
God Bless!
-Daniel P. Clark
Image by Levan Gokadze via the Creative Commons Attribution-ShareAlike 2.0 Generic License