Roman Numerals: a tale of Ruby performance improvement

There are an infinite number of opportunities to learn and improve your skills as a developer. I like to take some time now and then to do it consciously, so I pick a topic and go at it.
The trigger this time was a Sandi Metz newsletter entry about the Roman Numerals quiz. (That drove me to take interest in Ruby refinements, but I digress).
I remember trying to solve that same problem some years back when first learning ruby. I found it here, as part of ruby quiz blog and the associated book.

This time, a part of the proposed solution got me thinking:

Oh, this can be optimized! In different ways. So I tried.

First question: why is it needed to sort the same array 4000 times? Oh, because the ROMAN_MAP is in ascending order and we need it in descending order. Let’s put the original array in descending order to begin with, and then there is no need for reversing it each iteration:

Is it better? How do I know? By how much? Let’s benchmark it with benchmark-ips (bigger is better :P):

Ok, what else? roman_map.keys does not change and is called for every iteration. It can be put outside the loop:

Ok, not too big of an improvement. Now take a closer look at the behaviour implemented for each key (called ‘div’ in the code). If the div is greater than the target number, the division is always 0 and the accumulator does not change. We can make the comparison upfront, saving unnecessary division, modulus, multiplication by 0 and trying to add empty strings to the accumulator:

Not bad!
Still, a lot of calculations are being repeated over and over. For instance, for the number 324, the “24” part we already calculated when 24 was the target. Also, for 24, we already constructed the 4 part when 4 was the target. We can improve this code by not repeating calculations we already did. This caching should improve the performance significantly:

And indeed it does speed up things!

The code is available on github.

This is about learning and improving. Not all the code in an needs to be super performant, and surely this method didn’t from the perspective of a beginner rubyist starting to grasp the language.
Learn to pick the right battles. Try to improve the parts that cause the most pain, that are used the most often. And remember that for the most part, performance tuning is a tradeoff between computation, memory and readability/maintainability.