Categories
Technology

Optimizing And Readability

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.