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?
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)
Up ruby 🤓💪🏽😁
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!!!