Page 1 of 1

Optimising a Small Life Program?

Posted: June 29th, 2010, 5:43 pm
by geckojsc
I'm currently making a piece of homebrew software for the Nintendo DS, which is a Life simulator with a decent set of features. There have only been a few attempts at this, but none of them have been that good.

Feature-wise my Life program is fine, but it's not good speed-wise. I've been using a plain "count the neighbours of one array, apply the rules to another array, then swap their roles for the next generation" algorithm. This was fine for an earlier Life test of mine (a small console program) but when the world's size is 256x192, things get a lot slower.

I know I will have to change or enhance my algorithm, but I was wondering just how I might do this. What are your tips for optimising this very basic Life implementation, or am I doing it completely the wrong way? I'd like to know!


The tips I've found so far are:
  • Don't recalculate the number of neighbours for every cell, just get rid of the ones that aren't neighbours any more and add the ones that are.
  • Store a list of the active cells and only update those cells.
  • Use a quadtree to seek out the active areas.

Re: Optimising a Small Life Program?

Posted: June 30th, 2010, 6:47 am
by apg
What are your tips for optimising this very basic Life implementation
Well, for such a small universe as 256 * 192, you don't want to do anything insanely powerful like HashLife. Paul Callahan devised an interesting method a while ago, which is about four times faster than the naive method. It relies on the computer having at least 16-bit unsigned registers; does the Nintendo DS allow this?
  • Store each 2*2 block of cells in a single element of your array (0..15).
  • A 2*2 block is determined by its surrounding 4*4 block, which can be decomposed into four 2*2 blocks (offset from the original).
  • This can be calculated using a 65536-element lookup table, where g = table((a << 12 ) | (b << 8 ) | (c << 4 ) | (d)), using bitwise arithmetic. g is the new state of the block; the other letters represent the previous states of the four neighbouring blocks.
Amazingly, if you do this correctly, you won't require double-buffering.

Here's Paul Callahan's original explanation (8 Nov 1997):
But there's a very nice kind of spatial packing that just happens to
work for 2x2 patches (has it been used elsewhere?): Observe, trivially,
that every 2x2 patch is determined by the 4x4 patch obtained by
adding a one-cell border around it. This patch is itself composed
of 4 2x2 patches. They aren't aligned with the center patch, but this
turns out not be much of a problem.

Consider a 16-state CA based on an asymmetric 4-cell neighborhood
consisting of a cell and its eastern, southern, and south-eastern neighbors.
Now we can embed 2x2 patches from any 2-state (Moore neighborhood or
subset) CA into single cells on this CA. Suppose we derive the rule
for determining the middle 2x2 patch from a 4x4 patch in the 2-state CA and
express it as the rule of the 16-state CA. Then if we were to convert
to the new CA, apply the rule, and convert back, we would have computed
the original 2-state rule, except that every new state would be shifted
north-west from where it should be. We can always compute the correction
needed after applying a certain number of steps, so in fact, we can
work primarily within the 16-state CA until we need to use the
individual cells (alternatively, we could derive 2 16-state CAs that
cancel either other's shifts when applied one after the other; I
chose not to implement this in order to avoid code duplication).

So, once you accept that the 16-state 4-cell neighborhood CA can embed
your 2-state CA, the next question is how to compute its rule efficiently.
Well, this too be done with row-majored-ordered lists of
cell positions and states. In fact, it is a little easier than the
straight Life code, because 2x2 neighborhoods can be scanned by maintaining
just 2 pointers into the list. A 2x2 neighborhood of 16-state cells fits
into 16 bits. Thus, the transition rule fits into a look-up table of
2^16 entries. Luckily, this introduces very low overhead on any modern
computer. The table is generally a redundant representation of 4 overlapping
3x3 neighborhoods. The procedure that fills in the entries need not be
optimized, and works with any 2-state Moore-neighborhood rule.

I obtained the following times running rabbits for 17331 generations on
a SPARC 5 (all compiled at the same optimization level with gcc):

a) List of single cells: 56.2 seconds user time. (Unix time command)
b) List of 2x2 patches: 12.8 seconds user time. (" " " )
c) Xlife (G command): 32.3 seconds (timed with a stopwatch)

Display updates complicate the problem of benchmarking Xlife. But the
G command only keeps a running display of the number of generations
finished (every hundred) and the above seems to be the fastest Xlife can
generate the pattern in practice.

Now that I have this code, I can probably run some experiments faster.
On the other hand, even something as simple as the population checks
to determine stability are complicated (slightly) by the new method.
Checking if subpatterns are destroyed, determining connected oscillators, etc.
are all much easier after first unpacking the 2x2 patches into a
list of single cells.

--Paul

Re: Optimising a Small Life Program?

Posted: June 30th, 2010, 4:43 pm
by geckojsc
That looks a bit confusing - but very clever. I haven't done much bitwise stuff before, although I sort of understand the concept. I think this will be a good opportunity for me get some use out of the bitwise operators. Cheers!

I'll be sure to post the final results up here for anybody with a homebrew-enabled DS to try out!
If anybody else has some optimisation tips, feel free to share them here.

Re: Optimising a Small Life Program?

Posted: September 24th, 2012, 6:30 pm
by johnhw
This is an old topic, but here's some simple Python code which implements Paul Callahan's neat idea. This code prints generation 30 of the Gosper glider gun to the console.

It's not very fast (~100 gens/second on breeder.lif on my laptop), and it uses dictionaries (hash tables) rather than walking a list with a set of pointers which would be much more efficient in (e.g.) a C implementation.

The life code is just create_life_table() and Board.generation(); the rest of the code just packs and unpacks from various formats.

Code: Select all


def create_life_table():
    """Generate the lookup table for the cells."""
    successors = {}
    
    def life(*args):
        n = sum(args[1:])
        n |= args[0] # if we or with the cell value, we can just test == 3        
        if n==3:
            return 1
        else:
            return 0
        
    # generate all 16 bit strings
    for iv in range(65536):
        bv = [(iv>>j)&1 for j in range(16)]
        a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p = bv
        
        # compute next state of the inner 2x2
        nw = life(f, a, b, c, e, g, i, j, k)
        ne = life(g, b, c, d, f, h, j, k, l)
        sw = life(j, e, f, g, i, k, m, n, o)
        se = life(k, f, g, h, j, l, n, o, p)

        # compute the index of this 4x4
        nw_code = a | (b<<1) | (e<<2) | (f<<3)
        ne_code = c | (d<<1) | (g<<2) | (h<<3)
        sw_code = i | (j<<1) | (m<<2) | (n<<3)
        se_code = k | (l<<1) | (o<<2) | (p<<3)
        
        # compute the state for the 2x2
        next_code = nw | (ne<<1) | (sw<<2) | (se<<3)

        # get the 4x4 index, and write intp the table
        this_code = nw_code | (ne_code<<4) | (sw_code<<8) | (se_code<<12) 
        successors[this_code] = next_code
        
    return successors
   

class Board:

  def __init__(self):
    """Create a new, empty simulation."""    
    self.clear()
    
  
  def clear(self):
    """Clear the state of the simulation."""
    self.cells = {}
    self.life_table = create_life_table()
    self.offset = 0
        
  def get_all(self):
    """Return a list of all the ON cells inside the rectangle (x1,y1), (x2,y2)"""
    o = -self.offset
    
    # points are stored as a 4 bit number, in the order:
    # (MSB) se sw ne nw (LSB)
    
    pts = [((x<<1)+o,(y<<1)+o) for  x,y in self.cells if self.cells[(x,y)]&1]
    pts += [((x<<1)+1+o,(y<<1)+o) for  x,y in self.cells if self.cells[(x,y)]&2]
    pts += [((x<<1)+o,(y<<1)+1+o) for  x,y in self.cells if self.cells[(x,y)]&4]
    pts += [((x<<1)+1+o,(y<<1)+1+o) for x,y in self.cells if self.cells[(x,y)]&8]
    return pts
                     
    
  def set(self, x, y, value):
    """Set the cell at (x,y) to value (either 0 or 1)"""
    # get 2x2 co-ordinate
    xc = x>>1
    yc = y>>1    
    c = self.cells.get((xc,yc),0)

    # flip the appropriate bit
    mask = 1 << ((y&1)<<1)
    mask <<= x & 1
    if value:
        c = c | mask
    else:
        c = c & ~mask        
    self.cells[(xc,yc)] = c
        
    
  def set_all(self, pts):
    """Set a list of points."""
    for cell in pts:
        self.set(cell[0], cell[1], 1)
        
  def step(self, n):
    """Run n generations."""
    for i in range(n):
        self.generation()
        
  def generation(self):
    """Advance by one generation."""    
    new_cells = {}
    
    # compute cell co-ordinates of each live 2x2 block, and its NW, N and W neighbour
    cells = set(self.cells.keys())
    cells.update([(x-1,y) for x,y in cells])
    cells.update([(x,y-1) for x,y in cells])
        
    # for each of these x,y co-ordinates
    for x,y in cells:
                
        # get the neighbours
        nw = self.cells.get((x, y), 0)
        ne = self.cells.get((x+1, y), 0) << 4
        sw = self.cells.get((x, y+1), 0) << 8        
        se = self.cells.get((x+1, y+1), 0) << 12

        # lookup up the 2x2 successor of this 4x4 block
        code = nw | ne | sw | se
        result = self.life_table[code]
        
        if result:
            new_cells[(x,y)] = result
                                           
    # shift the offset, flip the buffer
    self.offset += 2
    self.cells = new_cells

## utilities: convert from RLE / to ASCII   
def to_str(pts):
    """Convert to ASCII"""
    if not pts:
        return ""
    pts = sorted(pts, key=lambda x:(x[1],x[0]))
    xs = [p[0] for p in pts]
    min_x, max_x, min_y, max_y = min(xs), max(xs), pts[0][1], pts[-1][1]
    rle = []
    x,y = min_x, min_y
    for xn,yn in pts:
        while yn!=y:
            y, rle, x = y+1, rle+['\n'], min_x
        while xn!=x:
            rle, x = rle+[' '],x+1
        rle, x = rle+['*'], x+1
    return "".join(rle)
        
def from_rle(rle):
    """Parse an RLE string"""
    pts = []
    x,y = 0,0 
    rc, count = 1, 0
    lines = rle.split("\n")
    for line in lines:    
        line = line.strip().rstrip().lower()
        if line and line[0] in 'bo$0123456789':
            for char in line:
                if char=='o':
                    pts +=  [(x+i,y) for i in range(rc)]
                if char in 'bo':                    
                    x += rc
                if char=='$':
                    y += rc
                    x = 0
                if char.isdigit():                                        
                    count = count*10 + int(char)
                    rc = count
                else:
                    count, rc = 0, 1
    return pts
    
if __name__=="__main__":
    b = Board()
    b.set_all(from_rle("""x = 36, y = 9, rule = B3/S23
27bo$26b4o$9boo14boobobo$bo7bobbo11b3obobbobboo$o3boo7bo11boobobo3boo$
o5bo6bo12b4o$b5o7bo7bo5bo$9bobbo9bo$9boo9b3o!"""))
    b.step(30)
    print to_str(b.get_all())