Ruby has a benchmarking tool in its standard library to help measure the performance of your code. It's most useful when comparing two implementations, to find out which is fastest.
In this example, we're tasked with converting a Hash with string keys (like {"foo" => "bar"}
to one with symbols (like {:foo => "bar"}
). Throughout the examples, we'll use a hash with a key and a value for each letter in the English alphabet.
To quickly generate this hash without having to type it out, we'll convert a range of letters to our testing hash. We'll put it in the input
variable to use later.
1input = ("a".."z").map {|letter| [letter, letter]}.to_h
2# => {"a"=>"a", "b"=>"b", "c"=>"c", "d"=>"d", "e"=>"e", "f"=>"f", "g"=>"g", "h"=>"h", "i"=>"i", "j"=>"j", "k"=>"k", "l"=>"l", "m"=>"m", "n"=>"n", "o"=>"o", "p"=>"p", "q"=>"q", "r"=>"r", "s"=>"s", "t"=>"t", "u"=>"u", "v"=>"v", "w"=>"w", "x"=>"x", "y"=>"y", "z"=>"z"}
Now that we have our input
variable to test our implementations with, we'll write one to see how it performs. A nice one-liner to convert all keys in our input hash to symbols instead of strings looks like this:
1input.map { |key, value| [key.to_sym, value] }.to_h
2# => {:a=>"a", :b=>"b", :c=>"c", :d=>"d", :e=>"e", :f=>"f", :g=>"g", :h=>"h", :i=>"i", :j=>"j", :k=>"k", :l=>"l", :m=>"m", :n=>"n", :o=>"o", :p=>"p", :q=>"q", :r=>"r", :s=>"s", :t=>"t", :u=>"u", :v=>"v", :w=>"w", :x=>"x", :y=>"y", :z=>"z"}
This implementation uses the map
method to loop over the hash to run a block for each key-value pair. In the block, it converts the key to a symbol and returns a two-element array with the newly created symbol key, and the untouched value.
The result from the map
command is an array with 26 key-value arrays. Since we need a hash, we use #to_h
to convert our new array back into a hash.
Benchmark.measure
Now that we have a working implementation, we can use Ruby's Benchmark module to see how it performs.
1require 'benchmark'
2
3input = ('a'..'z').map { |letter| [letter, letter] }.to_h
4
5puts Benchmark.measure {
6 50_000.times do
7 input.map { |key, value| [key.to_sym, value] }.to_h
8 end
9}
Benchmark.measure
takes a block, which is executed while keeping track of how long it took to execute. It returns a report string, which is printed to the console using puts
.
Since this is a quick piece of code, we run it 50.000 times to make sure we get some visible results.
1$ ruby bench.rb
2 0.810000 0.000000 0.810000 ( 0.816964)
The report string shows four numbers, which represent the user CPU time (the time spent executing your code), the system CPU time (the time spent in the kernel), both user and system CPU time added up, and the actual time (or wall clock time) it took for the block to execute in brackets.
The wall time shows us that we can run the block of code above 50.000 times in a little over 800 milliseconds. While that's an impressive number, we don't know what that means unless we compare it to another implementation of the code.
Benchmark.bm
Besides Benchmark.measure
, Ruby provides Benchmark.bm
, which can run multiple code samples and print their results. For each sample, we'll call Benchmark#report
with a name, and the block to be executed.
1require 'benchmark'
2
3input = ("a".."z").map { |letter| [letter, letter] }.to_h
4n = 50_000
5
6Benchmark.bm do |benchmark|
7 benchmark.report("Hash[]") do
8 n.times do
9 input.map { |key, value| [key.to_sym, value] }.to_h
10 end
11 end
12
13 benchmark.report("{}.tap") do
14 n.times do
15 {}.tap do |new_hash|
16 input.each do |key, value|
17 new_hash[key.to_sym] = value
18 end
19 end
20 end
21 end
22end
In this benchmark, we'll use Benchmark.bm
to test two implementations by running each 50.000 times. The first measurement block is the same as the example from before.
In the second measurement block, we use a longer implementation, which creates a new hash up front. It loops over the string-key hash, and adds an element to the new hash for every item. This way, it doesn't have to convert the hash to an array, and back to a hash when it's finished.
Running the benchmark again will show us this implementation is more than 25% faster, although the code is longer (and a little less clever) than the one-liner we tried before.
1$ ruby bench.rb
2 user system total real
3Hash[] 0.850000 0.000000 0.850000 ( 0.851106)
4{}.tap 0.610000 0.020000 0.630000 ( 0.637070)
More benchmarking
When working on an important piece of code in your codebase, running benchmarks to compare different implementations can give more insight into their execution speeds. By comparing different implementations to understand how they impact performance, you'll be able to avoid anti-patterns and write faster Ruby.
Tip: A lot of common idioms are pre-benchmarked, and their results are published as fast-ruby. Reading through the examples can save you some benchmarking in the future.
There are more options you can test for this example, and the Ruby's benchmarking library has a lot more sophisticated features you can try, but this gives a good introduction to how benchmarking works in Ruby. If you'd like to know more about benchmarking, or have any questions or suggestions, please let us know at @AppSignal.