Optimizing code is generally an expensive process (read: time-consuming) and there are established ways of getting to the bottom of “what to optimize”. Thankfully, profilers are available to help with a lot of the guesswork, so it’s generally a good idea to make sure you work with one *most of the time*. Moving along, it was high time for me to look at some Ruby profiling.
The documentation for ruby-prof is pretty neat and the library itself is quick to get up and running with. And so we start. For my initial problem, I wrote a goal-seek algorithm for accurately estimating gross earnings, given a target nett earning using a tax table- as opposed to just using a base tax-rate. Anyhow, my first stab algorithm (a simple linear search) included the lines:
def seek_annual_gross(m_nett, base_perc)   sample_gross = m_nett * base_perc   paye = Paye.new(sample_gross)   p_nett = sample_gross - paye.monthly_tax   margin = MARGIN*m_nett   if((p_nett-margin < m_nett) && (p_nett+margin > m_nett))    return paye.annual_gross.round_to2.to_f   elsif(p_nett-margin > m_nett)    return seek_annual_gross(m_nett, (base_perc - (margin)))   elsif(p_nett+margin < m_nett)    return seek_annual_gross(m_nett, (base_perc + (margin)))   end end
The profiler showed up what i kinda suspected- always a good sign. Essentially, my incremental margin for the next step was too small (fixed) and thus, getting closer to the solution was taking too long- and endangered the stack 🙂 What i needed was a better guess at how much to increment.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
22.58 0.28 0.28 177 1.58 3.22 Integer#times
16.13 0.48 0.20 171 1.17 259.18 NettGoalSeek#seek_annual_gross
6.45 0.56 0.08 179 0.45 0.50 Float#round_to2
Some minor adjustments to the routine, including an adjusted guess:
increment = MARGIN*(p_nett - m_nett)/margin
and modifying the appropriate calls
if((p_nett-margin < m_nett) && (p_nett+margin > m_nett)) Â Â return paye.annual_gross.round_to2.to_f elsif(p_nett-margin > m_nett) Â Â return seek_annual_gross(m_nett, (base_perc + increment)) elsif(p_nett+margin < m_nett) Â Â return seek_annual_gross(m_nett, (base_perc - increment)) end
And the profiler now reports:
% cumulative self self total
time seconds seconds calls ms/call ms/call name
2.78 0.16 0.01 11 0.91 6.36 NettGoalSeek#seek_annual_gross
2.78 0.35 0.01 17 0.59 1.18 Integer#times
0.00 0.36 0.00 19 0.00 0.00 Float#round_to2
A significant difference! Incidentally, the time to run, according to the test harness, went down from 1.122316 seconds to 0.189661 seconds. The high-level indicator showing enough of a difference as well.
The by-product of this optimization included the ability to get even more accurate estimations since the stack never overflowed, despite the required margin of error.
The moral: optimization doesn’t need to sacrifice code readability. At the right time, in the right spot, for the right reasons, you can achieve a sweetspot (of sorts) between two opposing(?) constraints. But that’s not to assume i’ve found the nicest sweetspot in this little piece 🙂
So in between refactorings or when there’s a lull in production, indulge the geek inside you.