How Benchmarking Your Code Will Improve Your Ruby Skills

Written by jubaan | Published 2020/08/02
Tech Story Tags: ruby | benchmark | learn-to-code-ruby | rubygems | code-performance | how-to-benchmark-ruby | benchmark-ips | benchmark-memory

TLDR How Benchmarking Your Code Will Improve Your Ruby Skills will step up your game. The better understanding of your code, the better code you'll start programming. Ruby makes it easy for us, as it already has a "benchmarking" class, but we'll complement it with two other ruby gems. We'll use the benchmark methods to measure our code performance. We'll show you an example on an example, and you'll understand what I meant and what I'll show later.via the TL;DR App

Learning to code is a path full of struggles, and learning Ruby isn't the exception. But you'll agree with me, that practice is the best way to learn and develop your skills.
Out there are plenty of sites to choose from where you can achieve this. But after n amount of solved challenges, you'll start questioning your solutions.
You'll start feeling, that is not enough to come up with an answer to the challenge. And you'll be wondering if there is a better, faster, and less consuming memory approach.
If this is your case, if you have reached this stage, then you're more than ready to begin benchmarking your code. And if you're not, well, let me tell you that learning this will step up your game.

So, what is this "benchmarking" thing anyway?...

Benchmarking is the process of measuring the performance of something... in this case the performance of your code.
Benchmarking is the process of measuring the performance of something.
And that's it, there is nothing more to add.
Yeah, I know what you're thinking, that knowing the meaning doesn't serve your purpose. But I'm positive that at least it will give you a broad idea.

With this in mind, the next question to answer is, "How benchmarking my code, will help me improve my coding skills?".

This is an easy one. Benchmarking gives you knowledge, and knowledge is power. The better understanding of your code, the better code you'll start programming. Is that simple!.
Benchmarking gives you knowledge, and knowledge is power.
Benchmarking gives you a simple view of how your code is performing. This view focuses on three main areas: elapsed time, memory, and iterations per second.
  1. Elapsed time: is the amount of time your code takes to finish a task.
  2. Memory: is the allocated space in your drive that your code occupies to solve the task.
  3. Iterations per second: is the total number of repetitions your code can do the same task, over and over, in a second.
Ok, at this point I assume you have understood the basics of benchmarking. Hopefully, you are also saying to yourself - "Yeah, this is what I was missing, this will help me become a better programmer!".
If you don't think that way yet; I hope knowing how it works will do.

Now, how do we apply this?

Ruby makes it easy for us, as it already has a "Benchmarking" class, but we'll complement it with two other ruby gems.
  1. Benchmarking-ips
  2. Benchmarking-memory
Before describing the steps to benchmark your code. I'm assuming you are using a linux/unix distribution, and have a working version of Ruby installed.
If you're running some other OS like Windows, the next series of commands, probably, won't work. But the benchmarking steps are going to be identical.
Let's begin!.
a. The first step is to install this rubygems in your system; for that, you can use this command:
gem install benchmark-ips benchmark-memory
b. The second step is creating a ruby file, I'm going to call it "benchmarking_ruby.rb", but you can name it in any way you want:
touch benchmarking_ruby.rb
b.1 Open the file with your preferred text editor. This file will contain three main sections:
- Section 1: Here we use require to include and grant our file access to the gems recently installed and the Ruby Benchmark Class. Doing so, will allow us to use the benchmark methods to measure our code performance.
require 'benchmark'
require 'benchmark/memory'
require 'benchmark/ips'
- Section 2: We write the code that we want to benchmark.
def method_1(params)
    # some code
end

def method_2(params)
    # some code
end
Note: Benchmark works if you want to measure a single method but it's best if you test at least two or more. Don't worry too much about this right now, later on, I'll show you an example and you'll understand what I meant.
- Section 3: We set up the test.
This part is tricky but not complicated. Look at the image below and try to identify what's happening.
def benchmark(params)
    Benchmark.bmbm do |x|
        x.report ("Method One") { method_1(params) }
        x.report ("Method Two") { method_2(params) }
    end

    Benchmark.memory do |x|
        x.report ("Method One") { method_1(params) }
        x.report ("Method Two") { method_2(params) }
        x.compare!
    end

    Benchmark.ips do |x|
        x.report ("Method One") { method_1(params) }
        x.report ("Method Two") { method_2(params) }
        x.compare!
    end
end

benchmark(params)
We are declaring a method called "benchmark" that accepts some parameters (or arguments).
def benchmark(params)
    ...
end
Inside it, we are using three Class methods, ::bmbm, ::memory and ::ips, and we are passing them a block.
def benchmark(params)
    Benchmark.bmbm do |x|
        # ::bmbm will measure the "ELAPSED TIME"
    end

    Benchmark.memory do |x|
        # ::memory will measure the "ALLOCATED MEMORY"
    end

    Benchmark.ips do |x|
        # ::ips will measure "ITERATIONS PER SECOND"
    end
end
Notice the comments inside each block?
The inner part of each block contains a report method that accepts a string and also a block. The string acts as a short description, and in the block, we call the methods we wrote in Section 2.
...
    Benchmark.bmbm do |x|
        x.report ("description or identifier") { # method call }
    end
...
Important: all benchmark blocks (::bmbm, ::memory and ::ips) use the same methods. Except for the last two (::memory and :: ips), they use an extra method called #compare!.
...
    Benchmark.memory do |x|
        x.report ("Method One") { method_1(params) }
        x.report ("Method Two") { method_2(params) }
        x.compare!
    end
...
The last bit in our file is just the call for the "benchmark" method.
benchmark(params)
c) And last but not least, The third step is to save your changes and run the ruby script you have created.
ruby bencharking_ruby.rb
I'm sure by now, you think this is interesting and you wanna give it a try. Until now I have only shown you how to set things up so you can benchmark your code.

But, How does it actually look?

Let's use what we have learned in a real example.
Imagine that you have been asked to sum all the elements inside an array and you have to do it in the most efficient way.
# You're given an array with n elements inside it.
# Your task is to print the sum fo all elements in 
# the most efficient way possible.

# Input

[*(0..1_000_000)] # ordered array of one million elements.

# Output

500_000_500_000
You start to think about it, and after some time you have come up with four solutions. But you don't know which one is better.
# Solution 1: sum all elements using a "while loop".
def sum_1(arr)
  sum = 0

  i = 0
  while i < arr.size
    sum += arr[i]
    i += 1
  end

  sum
end

# Solution 2: sum all elements using the "each" method.
def sum_2(arr)
  sum = 0

  arr.each do |num|
    sum += num
  end

  sum
end

# Solution 3: sum all elements using the "sum" method.
def sum_3(arr)
  arr.sum
end

# Solution 4: sum all elements using the "inject" method.
def sum_4(arr)
  arr.inject(&:+)
end
At this time you remember that you learn to benchmark your code by reading a random article in hackernoon and you set up your benchmarking environment.
You create your file.
touch sum_array_elements.rb
You made the corresponding changes and put in your code.
require "benchmark"
require "benchmark-memory"
require "benchmark/ips"

def sum_1(arr)
  sum = 0

  i = 0
  while i < arr.size
    sum += arr[i]
    i += 1
  end

  sum
end

def sum_2(arr)
  sum = 0

  arr.each do |num|
    sum += num
  end

  sum
end

def sum_3(arr)
  arr.sum
end

def sum_4(arr)
  arr.inject(&:+)
end

def benchmark(arr)
  puts "--- Elapsed Time ----------------------"
  Benchmark.bmbm do |x|
    x.report ("while loop") { sum_1(arr) }
    x.report ("#each") { sum_2(arr) }
    x.report ("#sum") { sum_3(arr) }
    x.report ("#inject") { sum_4(arr) }
  end

  puts "--- Memory ----------------------------"
  Benchmark.memory do |x|
    x.report ("while loop") { sum_1(arr) }
    x.report ("#each") { sum_2(arr) }
    x.report ("#sum") { sum_3(arr) }
    x.report ("#inject") { sum_4(arr) }
    x.compare!
  end

  puts "--- Iterations per Second -------------"
  Benchmark.ips do |x|
    x.report ("while loop") { sum_1(arr) }
    x.report ("#each") { sum_2(arr) }
    x.report ("#sum") { sum_3(arr) }
    x.report ("#inject") { sum_4(arr) }
    x.compare!
  end
end

arr = [*(0..1_000_000)]

benchmark(arr)
You run the script.
ruby sum_array_elements.rb
And you get the result.
--- Elapsed Time ---------------------------------------

Rehearsal ----------------------------------------------
while loop   0.066358   0.000000   0.066358 (  0.069475)
#each        0.068786   0.000000   0.068786 (  0.070810)
#sum         0.001983   0.000000   0.001983 (  0.002005)
#inject      0.158601   0.000956   0.159557 (  0.163468)
------------------------------------- total: 0.296684sec

                 user     system      total        real
while loop   0.056899   0.000016   0.056915 (  0.058878)
#each        0.082691   0.000000   0.082691 (  0.085142)
#sum         0.002014   0.000000   0.002014 (  0.002128)
#inject      0.156500   0.001905   0.158405 (  0.163096)


--- Memory ---------------------------------------------

Calculating -------------------------------------
          while loop     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
               #each     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
                #sum     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
             #inject     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)

Comparison:
          while loop:          0 allocated
               #each:          0 allocated - same
                #sum:          0 allocated - same


--- Iterations per Second ------------------------------

Warming up --------------------------------------
          while loop     1.000  i/100ms
               #each     1.000  i/100ms
                #sum    42.000  i/100ms
             #inject     1.000  i/100ms
Calculating -------------------------------------
          while loop     19.848  (±10.1%) i/s -     99.000  in   5.021740s
               #each     14.161  (± 7.1%) i/s -     71.000  in   5.037058s
                #sum    430.010  (± 5.1%) i/s -      2.184k in   5.092623s
             #inject      6.405  (±15.6%) i/s -     32.000  in   5.031657s

Comparison:
                #sum:      430.0 i/s
          while loop:       19.8 i/s - 21.66x  (± 0.00) slower
               #each:       14.2 i/s - 30.37x  (± 0.00) slower
             #inject:        6.4 i/s - 67.13x  (± 0.00) slower
As I said before, benchmarking your code gives you knowledge and knowledge is power.

But, what's the kind of power that you're getting?

Well, let's analyze the output so you could appreciate the of advantages of benchmarking.
Remember you were asked to deliver the most efficient solution so let's start with the first part:

Elapsed Time

--- Elapsed Time ---------------------------------------

Rehearsal ----------------------------------------------
while loop   0.066358   0.000000   0.066358 (  0.069475)
#each        0.068786   0.000000   0.068786 (  0.070810)
#sum         0.001983   0.000000   0.001983 (  0.002005)
#inject      0.158601   0.000956   0.159557 (  0.163468)
------------------------------------- total: 0.296684sec

                 user     system      total        real
while loop   0.056899   0.000016   0.056915 (  0.058878)
#each        0.082691   0.000000   0.082691 (  0.085142)
#sum         0.002014   0.000000   0.002014 (  0.002128)
#inject      0.156500   0.001905   0.158405 (  0.163096)
As you can see, the first test "Benchmark::bmbm", throw us the amount of time the solutions took to sum all elements. If you look at the last column in the second row you'll see the "Real" time for each one of them.
From this first part, we can assume that the best solution at the moment is the #sum method. It only took 0.002128 seconds to complete the task.
So which method is the most efficient one?. It's nonconclusive. We still have to review the other results.

Memory

--- Memory ----------------------------------------

Calculating -------------------------------------
          while loop     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
               #each     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
                #sum     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)
             #inject     0.000  memsize (     0.000  retained)
                         0.000  objects (     0.000  retained)
                         0.000  strings (     0.000  retained)

Comparison:
          while loop:          0 allocated
               #each:          0 allocated - same
                #sum:          0 allocated - same
             #inject:          0 allocated - same
In this case, it's a tie. Every method seems efficient, none of them is using space in your hard drive. We can tell by looking at the "Comparison" section.
At this point, every method still has a chance of proving that if they are the best solution to the problem.
So we analyze the last part.

Iterations per Second

--- Iterations per Second ------------------------------

Warming up --------------------------------------
          while loop     1.000  i/100ms
               #each     1.000  i/100ms
                #sum    42.000  i/100ms
             #inject     1.000  i/100ms
Calculating -------------------------------------
          while loop     19.848  (±10.1%) i/s -     99.000  in   5.021740s
               #each     14.161  (± 7.1%) i/s -     71.000  in   5.037058s
                #sum    430.010  (± 5.1%) i/s -      2.184k in   5.092623s
             #inject      6.405  (±15.6%) i/s -     32.000  in   5.031657s

Comparison:
                #sum:      430.0 i/s
          while loop:       19.8 i/s - 21.66x  (± 0.00) slower
               #each:       14.2 i/s - 30.37x  (± 0.00) slower
             #inject:        6.4 i/s - 67.13x  (± 0.00) slower
Again if we look at the "Comparison" results we observe an outstanding result. The #sum method is 21 times faster than the closest competitor. That is incredible!.
With all the sections analyzed. We now can conclude that the best way to approach this task is to use the #sum method. It was the fastest to solve the task, it didn't use any memory at all, and it was able to iterate 430 times per second.

Conclusion

Benchmarking your ruby code will make you a better programmer. Doing it will give useful information, and will help you link your ideas to facts, not perceptions. Also, it will save you from lots of "Runtime Errors".
Thanks for taking your time reading this, I hope you benefit from it, as I have.

Written by jubaan | Hi!, I'm a remote Full-stack Web Developer and student at Microverse.
Published by HackerNoon on 2020/08/02