Why Kernel#times is slower than while ???
Tags: ruby, rails, learning, coding
Published: 2024-05-11 23:35:29

Recently, I came across an article titled [“Ruby might be faster than you think”](https://www.johnhawthorn.com/2024/ruby-might-be-faster-than-you-think/) In this article, John Hawthorn revisits the code snippet from the [CrystalRuby README](https://github.com/wouterken/crystalruby/commit/e7c595122c5899d77ca29363aa401681e903deb8) 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:

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

```ruby
$ 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:

```ruby
$ 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:

```ruby
$ 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?

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!