Life Abridged.

TableSalt: optimizing code layout for speed in lua

A while back I wrote TableSalt - a constraint satisfaction framework written in [currently pure] lua. Every now and again I'll revisit it and try to make it run faster and get rid of any bugs found along the way. For benchmarking I use my SudokuSolver on a test of 95 sudoku puzzles.

My goal is a 100ms average. Back in May 2014 I was chilling at 337ms. Before these layout changes, I was at 253ms. Now I'm at 222ms and am only inching ever closer!

Original Code Layout

Initially I had a cell object, this is best thought of as an empty "cell" in sudoku or a variable in constraint satisfaction problems. Each cell has a domain, value, and a reference to each of the constraints it is associated with.

As such the code was pretty straigtforward:

function cell:initialize(domain)  
     self.domain = domain
     self.value = nil
     self.constraints = {}

I needed a table of cells to keep track of all the cells:

self.cells = {}  
for i = 1, self.size do  
    self.cells[i] = cell:new({unpack(domain)})

The {unpack()} created a deepcopy for each cell's domain. unpack is a built-in lua function that explodes a table, the curly brackets around it create a new version and a deep copy is born!

The Backup

While writing TableSalt, I wrote a ForwardCheck (or look-ahead) function that needed to backup and restore the current state of the cells. Easy enough:

function TableSalt:backupCells()  
    local serial = {{}, {}}
    for i = 1, self.size do
        serial[1][i] = {unpack(self.cells[i].domain)}
        serial[2][i] = self.cells[i].value
    return serial

function TableSalt:restoreCells(serial)  
    for i=1, self.size do
        self.cells[i].domain = serial[1][i]
        self.cells[i].value = serial[2][i]

The Problem.

This was slow. When I ran the code against a profiler, the code backup and AllDiff constraint were usually the prime culprits of wasting time. On my [unrealistic?] goal towards 100ms, wasting time on backups and restores was not an option. I read up on optimizing lua code and posted on stackoverflow looking for help.


I got rid of the cell class. Arguably, it's bad code design, but I did it in the name of speed. Here's what the initializer code became:

self.cellValue = {}  
self.cellDomain = {}  
self.cellConstraint = {}  
for i = 1, self.size do  
    self.cellConstraint[i] = {}
    self.cellDomain[i] = {unpack(domain)}

and as some of you lua aficionados might notice - I could've used a self.cells = {} and made each of those tables into self.cells.value, self.cells.domain, and self.cells.constrains, but I would gain ~4ms if I did so. If I ever get sub-100ms, I may consider adding it back in for the sake of better code design.

Anyway, this allowed me to do was create clones quickly and avoid the need to "restore" the values to their initial state. Now the backup function (which looks VERY similar to before) is:

local function backupCells(cellDomain, cellValue)  
    local cellDomain, cellValue = cellDomain, cellValue
    local serial = {{}, {}}
    for i = 1, #cellDomain do
        serial[1][i] = {unpack(cellDomain[i])}
        serial[2][i] = cellValue[i]
    return serial

and restore has become a quick two-liner:

self.cellDomain = cellCopy[1]  
self.cellValue = cellCopy[2]  

This provided a fairly significant speedup! On the sudoku benchmark, I now average ~222ms/puzzle - a whopping 31ms speedup from the 253ms before it.

The Future

I'm starting to think I'm reaching the end of pure-lua optimizations. I want to convert the cell backup and parts of the AllDiff function to C and see if that offers any significant speedup. If anyone has any other suggestions for speed (the code can be found on GitHub) - please let me know I'd love to hear it!

comments powered by Disqus