DEV Community

Cover image for How fast is your ruby code?
Jesse vB
Jesse vB

Posted on

How fast is your ruby code?

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.

# 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
Enter fullscreen mode Exit fullscreen mode

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?

# convert the integers to String before the join
array = []
10_000_000.times { |num| array << num.to_s }
array.join
Enter fullscreen mode Exit fullscreen mode

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.

# instantiate a String instead of an array
string = ""
10_000_000.times { |num| string << num.to_s }
Enter fullscreen mode Exit fullscreen mode

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.

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

Benchmark.measure("label") do
  # block of code to measure
end
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)