Jesse vB
Posted on November 7, 2022
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
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
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 }
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
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
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.
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.
Posted on November 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.