ruby

Benchmarking Ruby Code

Jeff Kreeftmeijer

Jeff Kreeftmeijer on

Benchmarking Ruby Code

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.

Share this article

RSS

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps