DEV Community

Cover image for Why Kernel#times is slower than while ???
Pimp My Ruby
Pimp My Ruby

Posted on

Why Kernel#times is slower than while ???

Recently, I came across an article titled “Ruby might be faster than you think” In this article, John Hawthorn revisits the code snippet from the CrystalRuby README and optimizes it to show that Ruby is just as performant, if not more, than the alternative presented by CrystalRuby.

I understood everything in the article except for one part. The author replaces the use of Kernel#times with a while loop, and this change almost halved the code execution time.

Seriously??? Halved? If it was that simple and dumb, why am I not putting while loops everywhere now?

So, I quickly replicated his Benchmark to check the discrepancy, and more importantly, to see how significant it is.

I run this benchmark:

require 'benchmark/ips'

def fib_while(n)
  a = 0
  b = 1
  while n > 0
    a, b = b, a + b
    n -= 1
  end
  a
end

def fib_times(n)
  a = 0
  b = 1
  n.times { a, b = b, a + b; nil }
  a
end

Benchmark.ips do |x|
  x.report('while loop') { fib_while(30) }
  x.report('times loop') { fib_times(30) }
  x.compare!
end
Enter fullscreen mode Exit fullscreen mode

And to my surprise, I got this output:

$ rbenv local 3.2.0 && ruby iterations.rb

Warming up --------------------------------------
          while loop    88.145k i/100ms
          times loop    88.970k i/100ms
Calculating -------------------------------------
          while loop    871.169k (± 0.9%) i/s -      4.407M in   5.059392s
          times loop    885.111k (± 0.6%) i/s -      4.448M in   5.026107s

Comparison:
          times loop:   885110.9 i/s
          while loop:   871169.3 i/s - 1.02x slower
Enter fullscreen mode Exit fullscreen mode

I’m devastated. We’ve been lied to. I test several combinations of the Benchmark, but nothing changes. Locally, both methods are completely equal in terms of performance.

Well, fortunately, I finally understood.

The problem was with my Ruby version. I was running my Benchmark on Ruby version 3.2.0. Here’s the output once the same script was run on 3.3.0:

$ rbenv local 3.3.0 && ruby iterations.rb

Warming up --------------------------------------
          while loop   212.807k i/100ms
          times loop   109.784k i/100ms
Calculating -------------------------------------
          while loop      2.133M (± 0.8%) i/s -     10.853M in   5.087899s
          times loop      1.103M (± 3.2%) i/s -      5.599M in   5.080973s

Comparison:
          while loop:  2133283.8 i/s
          times loop:  1103244.8 i/s - 1.93x slower
Enter fullscreen mode Exit fullscreen mode

🤯🤯🤯

Wow. That’s a significant difference! So all this time, John Hawthorn was running his benchmark on Ruby version 3.3.0. He could have told us!

Proud of this discovery, it led me to a question. How does the impact of versions really affect performance? And especially, what is the state for versions prior to 3.2.0?

I then take the oldest version I have locally on my computer and test it:

$ rbenv local 2.7.1 && ruby iterations.rb

Warming up --------------------------------------
          while loop    26.937k i/100ms
          times loop    20.069k i/100ms
Calculating -------------------------------------
          while loop    269.869k (± 0.1%) i/s -      1.374M in   5.090573s
          times loop    200.938k (± 0.1%) i/s -      1.024M in   5.093709s

Comparison:
          while loop:   269869.1 i/s
          times loop:   200938.2 i/s - 1.34x slower
Enter fullscreen mode Exit fullscreen mode

The performance gap between version 3.3.0 and 2.7.1 is astonishing.

But then comes a question, why is it so much faster in 3.3.0 compared to 3.2.0?

The answer: ✨YJIT Compiler✨

Without going into details, the use of YJIT drastically increases the performance of our while loop. From what I understand, while loops are much more predictable in terms of execution path, allowing YJIT to be very efficient.

In conclusion, if you want your Ruby application to be efficient, keep it updated!

Top comments (2)

Collapse
 
adesoji1 profile image
Adesoji1

Up ruby 🤓💪🏽😁

Collapse
 
kiyosama17 profile image
Kiyosama

can it work with async code such as IO-bound operations in downloading files concurrently? or it's just affect for CPU-bound only? i need experiment with this!!!