Optimizing with metaprogramming
Most times, optimization is a trade-off. You sacrifice something, usually readability and simplicity, in favor of a piece of code that executes faster. Is your judgment who decides if that speed gain is worth the loss in the other aspects. This post is about a technique that focuses only on maximizing speed. It is just another tool in our toolbox. It may be used if necessary given the trade-offs.
I came up with this idea while facing the optimization phase on the AI for tic tac toe. I’ll use that example.
The code processed the following data; an array containing the marks
where each player has made a move, and the groups of indexes in marks
that conform a line
.
For a 3 by 3 board that data would be:
One of the requirements is also to support a 4 by 4 board, so my generic implementation was:
Given a simple integration scenario, the profiler measurements pointed that the winner
method was being called 17280 times and roughly 75% of the processing time was spent there. 40% of which was spent on select
calls and 30% on map
calls. Those map
and select
calls are very expensive, we need to get rid of those.
When we make a piece of code more generic, we are extending the amount of scenarios solved by that code. If given good names it also creates an abstraction layer that helps human reasoning. But generalizing comes with a small cost in performance. Most of the times that cost is completely irrelevant. 75% is not irrelevant.
Basically, what we are doing in those lines is looking at the marks
array by the line
indexes to get the mark that is occupying a full line
.
Lets try to think closer to low-level: remove generalizations and just focus on 3 by 3 board:
The &&
and ||
operations in Ruby may require some explanation:
Measuring again with the profiler, given exactly the same integration scenario, we see that the winner
method now spends only 9% of the total time, which is an immense improvement.
But, by doing that, we lost the generic properties of the initial algorithm; it will only work for the 3 by 3 board.
We need to regain the correct behavior independently of the lines
. Generalizing the code will slow us down again. Now, metaprogramming comes in handy:
Now, we are generating the correct code to handle any type of board. And that code will execute much faster!