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
``````

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
``````

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
``````

🤯🤯🤯

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
``````

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?

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!