Writing Fast (and Idiomatic) Ruby
Ruby hero Erik Michaels-Ober gave a superbly userful talk (video, slides) at this year’s Barcelona Ruby Conference. In it, Michaels-Ober offers up a dozen-or-so cases where some casual Ruby code can be made both faster and cleaner by using built-in Ruby features.
This sometimes means using one method instead of two: for example, instead of some_array.map(&:method).flatten
Ruby offers some_array.flat_map(&:method)
. And sometimes it means using one method in lieu of another, such as string.tr('_', '-')
instead of string.gsub('_', '-')
. As if to prempt concerns that this is old news, Michaels-Ober accompanies many of his slides with pull requests to major projects like Rails and Rubinius to fix where the slower, less clear alternatives are still being used.
Michaels-Ober begins with a discussion of his benchmarking methodology, recommending a gem called benchmark-ips which extends Ruby’s benchmark library to show iterations-per-second instead of seconds-per-itaration. This gives you a simple bigger-is-better result and keeps you from having to guess how many iterations you’ll need for the result to be meaningful. Now that benchmarking is a part of my toolkit, I can definitely see the usefulness of this gem.
I would definitely recommend you watch the talk (45 min), but if you’d like the tl;dw version (and for my own future reference), here are the key insights:
- Within a method that takes a block,
yield
is 5x faster thanblock.call
- Using Symbol#to_proc (e.g.
&:method
) is 20% faster than using a block .flat_map
is 4x faster than.map.flatten
.reverse_each
is 17% faster than.reverse.each
.each_key
is 33% faster than.keys.each
.sample
is 15x faster than.shuffle.first
- Hash#merge! is 3x faster than Hash#merge (as long as you don’t need immutable state)
- Passing a block as the second argument to
.fetch
is 2x faster than passing the block’s result directly .sub
is 50% faster than.gsub
if you know you’ll be making only one replacement.tr
is 5x faster than.gsub
if you don’t need regular expressions- sequential assignment (e.g.
a = 1; b = 2;
) is 40% faster than parallel assignment (e.g.a, b = 1, 2
) - Exceptions are quite slow. If you don’t know if an objects responds to a method, checking
.respond_to? :method
in anif
block is 10x faster than rescuingNoMethodError
Of course, as the speaker makes clear, the speed comparisons are sensitive to the particular use case so your mileage may vary, but in every case the faster option will still be faster and in most of these cases, more intuitive.