How fast is your ruby code?
Tags: ruby, tutorial, benchmark, webdev
Published: 2022-11-07 02:24:34

Sometimes you might need a way to measure the time it takes your code to execute.

## This works...
Throw in a good old `Time.now` and then subtract the time start time from the new `Time.now` after the code executes.

```Ruby
# let's see how long it takes the Ruby interpreter
# to string together 5 million integers.
def string_together_ten_million_integers
  start_time = Time.now

  array = []
  10_000_000.times { |num| array << num }
  array.join
  
  end_time = Time.now
  puts "Took #{end_time - start_time} seconds to execute"
end
```
For me, this takes around 1.7 seconds on average.  Is yours similar?  

Now, we can change things up and see if there are faster solutions. For instance, what if you convert the integers to String before you join?

```ruby
# convert the integers to String before the join
array = []
10_000_000.times { |num| array << num.to_s }
array.join
```
Hmm... 🤔 now it's more like 1.3 seconds.  Making that change seemed to shave off half a second.

But wouldn't it make more sense to just instantiate a mutable String to push integers into?  Then, there is no need to join.

```ruby
# instantiate a String instead of an array
string = ""
10_000_000.times { |num| string << num.to_s }
```
Which one is the fastest for you?

It's getting harder to keep track of all our results! Wouldn't it be great to see these methods profiled all at once and see the results neatly printed together?

That's where the Ruby `Benchmark` class comes in.

## This is better...

Benchmark is part of the Ruby standard library, meaning that it's readily available to require and use.  Just type `require 'benchmark'` at the top of your file or any time during your IRB session.

You can also type `irb -r benchmark` when you start up your IRB session.

Like every module in the Ruby StdLib you can find the documentation at [ruby-doc.org](https://ruby-doc.org/stdlib-2.5.3/libdoc/benchmark/rdoc/Benchmark.html).

The simplest way to use Benchmark is to send it the class method `::measure` and provide a block of code as the argument.

```ruby
Benchmark.measure("label") do
  # block of code to measure
end
```
Try inserting some of the above methods into the Benchmark block and see what the resulting times are.

You're going to see the return value as a `Benchmark::Tms` class with the following attributes:

* cstime - children system CPU time
* cutime - children user CPU time
* stime - system CPU time
* utime - user CPU time
* total - cstime + cutime + stime + utime 
* real - total elapsed time

Now for a little explanation of these terms.  The user time is how long it actually takes for your code to execute.  The system time is the underlying system underneath your code to do what it needs to do (your computer's kernel, etc.).  The total time is the addition of the system and the user times.  

So what is the real time?  That's adding in user input, network connections, etc.  If you only care about the actual wall clock time from start to finish, just look at the real time.

The children system and user times would be for any other methods called within your block.  These would be child processes.

Although using `Benchmark::measure` is a good way to measure the run time for one block of code, there is a way to measure different blocks side by side.

## The best way...

Instead of `::measure`, let's use the `::bm` method. This method allows you to generate reports of multiple code blocks.  Perfect!  Let's use this with our three methods above.
> By the way, in Ruby we use '#' before instance method names and '::' before class method names when we refer to them. Since we never have to call `Benchmark.new`, we use the class methods only.
```ruby
require 'benchmark'

Benchmark.bm do |x|
  x.report("array/join") do
    array = []
    10_000_000.times { |num| array << num }
    array.join 
  end

  x.report("array/to_s/join") do
    array = []
    10_000_000.times { |num| array << num.to_s }
    array.join
  end
   
  x.report("string/to_s") do
    string = ""
    10_000_000.times { |num| string << num.to_s }
  end
end
```
Did the results that printed to your console align correctly?  With longer labels we need to adjust the `label_width` of the results.  It's actually the first argument to the `::bm` method, so try `Benchmark.bm(20) do |x|` to spread out the results.

These are the results on my machine:

```
                           user     system      total        real
array/join             1.551520   0.168228   1.719748 (  1.789678)
array/to_s/join        1.209965   0.122323   1.332288 (  1.373220)
string/to_s            0.893824   0.047670   0.941494 (  0.949304)
```
Hopefully you are feeling empowered to profile your Ruby code with the Benchmark utility!

## Advanced additional content

One way to challenge your understanding of the Ruby Standard Library is to peruse through the source code.  You'll find that it's not as intimidating as you might think.  

Don't feel pressure to understand it all at first glance.  Feel encouraged that you understand any of it and with practice your understanding will grow and grow!

Where can you find it?  I use RVM (Ruby Version Manager) to manage and install my rubies and gems.  So for me, I have a .rvm directory in my home directory. This is where the Standard Library is housed.

For me its `~/.rvm/rubies/ruby-3.0.0/lib/ruby/3.0.0`.  

From there you can open open up the benchmark.rb file and see the source code.  It's a small module (written over 20 years ago!) of just over 500 lines with much of that taken up with documentation.

### Notes

I first noted the different parameters the various methods received.  This helped me to understand how I could use the methods.  

I wanted my reports to have a longer width. I saw that the `Report` class was instantiated with a `width` attribute from the `::benchmark` method which is called from the `::bm` method.  That's how I learned I could call `::bm` with '20' as the argument to spread out the report.

Another interesting note is that the magic of Benchmark really happens in the `::measure` method on line 291.  

```ruby
def measure(label = "") # :yield:
    t0, r0 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
    yield
    t1, r1 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
    Benchmark::Tms.new(t1.utime  - t0.utime,
                       t1.stime  - t0.stime,
                       t1.cutime - t0.cutime,
                       t1.cstime - t0.cstime,
                       r1 - r0,
                       label)
  end
```

Just like we were inserting `Time.now` in our original code, that's really all Benchmark does. Only, it uses a more sophisticated approach, `Process.times` and `Process.clock_gettime()`.  Then it yields the block of code supplied and calls the same Process methods again and does the subtraction.  This almost exactly what we were doing.

Apparently it isn't magic, just good Ruby developers like you and me writing simple code. 👊

If you were to do a little more digging in the the Ruby `Process` you might be able to develop your own customized Benchmark module.