abrash-black-book/chapter-18.md
2014-01-06 23:29:31 +11:00

1022 lines
34 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Michael Abrash's Graphics Programming Black Book, Special Edition
author: Michael Abrash
date: '1997-07-01'
identifier:
- scheme: ISBN
text: 1576101746
publisher: The Coriolis Group
category: 'Web and Software Development: Game Development,Web and Software Development:
Graphics and Multimedia Development'
chapter: '18'
pages: 347-367
---
## Chapter 18 -- It's a plain Wonderful Life
### Optimization beyond the Pale
When I was in high school, my gym teacher had us run a race around the
soccer field, or rather, around a course marked with cones that roughly
outlined the shape of the field. I quickly settled into second place
behind Dwight Chamberlin. We cruised around the field, and when we came
to the far corner, Dwight cut across the corner, inside a cone placed
awkwardly far out from the others. I followed, and everyone else cut
inside the cone too—except the pear-shaped kid bringing up the rear, who
plodded his way around every single cone on his way to finishing about
half a lap behind. When the laggard finally crossed the finish line, the
coach named him the winner, to my considerable irritation. After all,
the object was to see who could run the fastest, wasn't it?
Actually, it wasn't. The object was to see who could run the fastest
according to the limitations placed upon the contest. This is a crucial
distinction, although usually taken for granted. Would it have been
legitimate if I had cut across the middle of the field? If I had ridden
a bike? If I had broken the world record for the 100 meters by dropping
100 meters from a plane? Competition has meaning only within a carefully
circumscribed arena.
Why am I telling you this? First, because it is a useful lesson for
programming.
> ![](images/i.jpg)
> All programming is performed within limitations, some of which can be
> bent or changed, but many of which cannot. You cannot change the maximum
> memory bandwidth of a VGA, or the maximum instruction execution rate of
> a 486. That is why the stunning 3D demos you see at SIGGRAPH have only
> passing relevance to everyday life on the desktop. A rule that Intel's
> chip designers cannot break is 8086 compatibility, much as I'm sure
> they'd like to, but of course the flip side is that although RISC chips
> are technically superior, they command but a small fraction of the
> market; raw performance is not the arena of competition. Similarly, you
> will often be unable to change the specifications for the software you
> implement.
### Breaking the Rules
The other reason for the anecdote has to do with the way my second
Optimization Challenge worked itself out. If you'll recall from the last
chapter, the challenge I made to the readers of *PC TECHNIQUES* was to
devise the fastest possible version of the Game of Life cellular
automata simulation game. I gave an example, laid out the rules, and
stood aside. Good thing, too. *Apres moi, le deluge....*
And when the dust had settled, I was left with the uneasy realization
that every submitted entry broke the rules. *Every single entry*. The
rules clearly stated that submitted code must produce *exactly the same
output* as my example implementation under all circumstances in order to
be eligible to win. I do not think that there can be any question about
what "exactly the same output" means. It means the same pixels, in the
same colors, at the same places on the screen at the same points in all
the Life simulations that the original code was capable of running.
Period. And not one of the entries met that standard. Some submitted
listings were more than 400 lines long. Some didn't display the
generation number at the right side of the screen, didn't draw the same
pixel colors, or didn't bother with magnification. Some had bugs. Some
didn't support all possible cellmap widths and heights up to 200x200,
requiring widths and heights that were specific multiples of a number of
cells that lent itself to a particular implementation.
This last mission is, in a way, a brilliant approach, as evidenced by
the fact that it yielded the two fastest submissions, but it is not
within the rules of the contest. Some of the rule-breaking was major,
some very minor, and some had nothing to do with the Life engine itself,
but the rules were clear; where was I to draw the line if not with exact
compliance? And I was fully prepared to draw that line rigorously,
disqualifying some mind-bending submissions in order to let lesser but
fully compliant entries win—until I realized that there *were* no fully
compliant entries.
Given which, I heaved a sigh of relief, threw away the rules, and picked
a winner in the true spirit of the contest: raw speed. Two winners, in
fact: Peter Klerings, a programmer for Turck GmbH in Munich, Germany,
whose entry just plain runs like a bat out of hell, and David Stafford
(who was also the winner of my first Optimization Challenge), of Borland
International, whose entry is slightly slower mainly because he didn't
optimize the drawing part of the program, in full accordance with the
contest rules, which specifically excluded drawing time from
consideration. Unfortunately, Peter's generation code and drawing code
are so tightly intertwined that it is impossible to separate them, and
hence not really possible to figure out whose generation engine is
faster. Anyway, at 180 to 200 generations per second, including drawing
time, for 200x200 cellmaps (and in the neighborhood of *1000* gps for
96x96 cellmaps, the size of my original implementation), they're the
fastest submissions I received. They're both more than an order of
magnitude faster than my final optimized C++ Life implementation shown
in Chapter 17, and more than 300 times faster than my original,
perfectly functional Life implementation. Not 300 percent—300 *times*.
Cell generations scud across the screen like clouds, and walkers shoot
out like bullets. Each is a worthy winner, and I feel confident that the
true objective of the challenge has been met: pure, breathtaking
*speed*.
Notwithstanding, *mea culpa*. The next time I lay a challenge, I will
define the rules with scrupulous care. Even so, this was much more than
just another cycle-counting contest. We're fortunate enough to be privy
to a startling demonstration of the power of the best optimizer anyone
has yet devised—you. (That's the general "you"; I realize that the
specific "you" may or may not be quite up to the optimizing level of the
specific "David Stafford" or "Peter Klerings.")
Onward to the code.
### Table-Driven Magic
David Stafford won my first Optimization Challenge by means of a huge
look-up table and an incredible state machine driven by that table. The
table didn't cause David's entry to exceed the line limit because
David's submission included code to generate the table on the fly as
part of the build process. David has done himself one better this time
with his QLIFE program; not only does his build process generate a 64K
table, but it also generates virtually all his code, consisting of
17,000-plus lines of assembly language spanning another 64K. What David
has done is write the equivalent of a bitblt compiler for the Game of
Life; one might in fact call it a Life compiler. What David's code
generates is still a general-purpose program; it takes arbitrary seed
values, and can run for an arbitrary number of generations, so it's not
as if David simply hardwired the instructions to draw each successive
screen. However, it's a general-purpose program that is exquisitely
tailored to the task it needs to perform.
All the pieces of QLIFE are shown in Listings 18.1 through 18.5, as
follows: Listing 18.1 is BUILD.BAT, the batch file used to build QLIFE;
Listing 18.2 is LCOMP.C, the program used to generate the assembler code
and data file QLIFE.ASM; Listing 18.3 is MAIN.C, the main program for
QLIFE; Listing 18.4 is VIDEO.C, the video-related functions, and Listing
18.5 is LIFE.H, the header file. The following sidebar contains David's
build instructions, exactly as he wrote them. I certainly won't have
room to discuss all the marvelous intricacies of David's code; I suggest
you look over these listings until you understand them thoroughly (it
took me a day to pick them apart) because there's a lot of neat stuff in
there, and it's an approach to performance programming that operates at
a more efficient, tightly integrated level than you may ever see again.
One hint: It helps a *lot* to build and run LCOMP.C, redirect its output
to QLIFE.ASM, and look at the assembly code in that file. This code is
the entirety of David's generation engine, and it's almost impossible to
visualize its operation without actually seeing it.
> How To Build Qlife
>
> QLIFE is written for Borland C++, but it shouldn't be too difficult to
> convert it to work with Microsoft C++. To build QLIFE, run the
> BUILD.BAT batch file with the size of the life grid on the command
> line (see below). The command-line options are:
>
> --------- -----------------------------------------------------------
> WIDTH 32 Sets the width of the life grid to 96 cells (divided by 3).
> HEIGHT 96 Sets the height of the life grid to 96 cells.
> NOCOUNTER Turns off the generation counter (optional).
> NODRAW Turns off drawing of the cell map (optional).
> GEN 1000 Calculates 1,000 generations (optional).
> --------- -----------------------------------------------------------
>
> These *must* be in uppercase. For example, the minimum you really need
> is "WIDTH 40 HEIGHT 120." I used "WIDTH 46 HEIGHT 138 NOCOUNTER NODRAW
> GEN 7000" during testing.
>
> If you have selected the GEN option, you will have to press a key to
> exit QLIFE when it is finished. This is so I could visually compare
> the result of N generations under QLIFE with N generations under
> Abrash's original life program. You should be aware that the program
> from the listing contains a small bug, which may make it appear that
> they do not generate identical results. The original program does not
> display a cell until it changes, so if a cell is alive on the first
> generation and never dies, then it will never be displayed. This bug
> is not present in QLIFE.
>
> You should have no trouble running QLIFE with cell grids up to
> 210x200.
>
> You *must* have a VGA and at least a 386 to run QLIFE. The 386
> features that it uses are not integral to the algorithm (they're a
> convenience for the code), so feel free to modify QLIFE to run on
> earlier CPUs if you wish. QLIFE works best if you have a large CPU
> cache (256K is recommended).
>
> —*David Stafford*
**LISTING 18.1 BUILD.BAT**
```bat
bcc -v -D%1=%2;%2=%3;%3=%4;%4=%5;%5=%6;%6=%7;%7=%8;%8 lcomp.c
lcomp > qlife.asm
tasmx /mx /kh30000 qlife
bcc -v -D%1=%2;%2=%3;%3=%4;%4=%5;%5=%6;%6=%7;%7=%8;%8 qlife.obj main.c video.c
```
**LISTING 18.2 LCOMP.C**
```c
// LCOMP.C
//
// Life compiler, ver 1.3
//
// David Stafford
//
#include <stdio.h>
#include <stdlib.h>
#include "life.h"
#define LIST_LIMIT (46 * 138) // when we need to use es:
int Old, New, Edge, Label;
char Buf[ 20 ];
void Next1( void )
{
char *Seg = "";
if( WIDTH * HEIGHT > LIST_LIMIT ) Seg = "es:";
printf( "mov bp,%s[si]\n", Seg );
printf( "add si,2\n" );
printf( "mov dh,[bp+1]\n" );
printf( "and dh,0FEh\n" );
printf( "jmp dx\n" );
}
void Next2( void )
{
printf( "mov bp,es:[si]\n" );
printf( "add si,2\n" );
printf( "mov dh,[bp+1]\n" );
printf( "or dh,1\n" );
printf( "jmp dx\n" );
}
void BuildMaps( void )
{
unsigned short i, j, Size, x = 0, y, N1, N2, N3, C1, C2, C3;
printf( "_DATA segment DATA'\nalign 2\n" );
printf( "public _CellMap\n" );
printf( "_CellMap label word\n" );
for( j = 0; j < HEIGHT; j++ )
{
for( i = 0; i < WIDTH; i++ )
{
if( i == 0 || i == WIDTH-1 || j == 0 || j == HEIGHT-1 )
{
printf( "dw 8000h\n" );
}
else
{
printf( "dw 0\n" );
}
}
}
printf( "ChangeCell dw 0\n" );
printf( "_RowColMap label word\n" );
for( j = 0; j < HEIGHT; j++ )
{
for( i = 0; i < WIDTH; i++ )
{
printf( "dw 0%02x%02xh\n", j, i * 3 );
}
}
if( WIDTH * HEIGHT > LIST_LIMIT )
{
printf( "Change1 dw offset _CHANGE:_ChangeList1\n" );
printf( "Change2 dw offset _CHANGE:_ChangeList2\n" );
printf( "ends\n\n" );
printf( "_CHANGE segment para public FAR_DATA'\n" );
}
else
{
printf( "Change1 dw offset DGROUP:_ChangeList1\n" );
printf( "Change2 dw offset DGROUP:_ChangeList2\n" );
}
Size = WIDTH * HEIGHT + 1;
printf( "public _ChangeList1\n_ChangeList1 label word\n" );
printf( "dw %d dup (offset DGROUP:ChangeCell)\n", Size );
printf( "public _ChangeList2\n_ChangeList2 label word\n" );
printf( "dw %d dup (offset DGROUP:ChangeCell)\n", Size );
printf( "ends\n\n" );
printf( "_LDMAP segment para public FAR_DATA'\n" );
do
{
// Current cell states
C1 = (x & 0x0800) >> 11;
C2 = (x & 0x0400) >> 10;
C3 = (x & 0x0200) >> 9;
// Neighbor counts
N1 = (x & 0x01C0) >> 6;
N2 = (x & 0x0038) >> 3;
N3 = (x & 0x0007);
y = x & 0x8FFF; // Preserve all but the next generation states
if( C1 && ((N1 + C2 == 2) || (N1 + C2 == 3)) )
{
y |= 0x4000;
}
if( !C1 && (N1 + C2 == 3) )
{
y |= 0x4000;
}
if( C2 && ((N2 + C1 + C3 == 2) || (N2 + C1 + C3 == 3)) )
{
y |= 0x2000;
}
if( !C2 && (N2 + C1 + C3 == 3) )
{
y |= 0x2000;
}
if( C3 && ((N3 + C2 == 2) || (N3 + C2 == 3)) )
{
y |= 0x1000;
}
if( !C3 && (N3 + C2 == 3) )
{
y |= 0x1000;
}
printf( "db 0%02xh\n", y >> 8 );
}
while( ++x != 0 );
printf( "ends\n\n" );
}
void GetUpAndDown( void )
{
printf( "mov ax,[bp+_RowColMap-_CellMap]\n" );
printf( "or ah,ah\n" );
printf( "mov dx,%d\n", DOWN );
printf( "mov cx,%d\n", WRAPUP );
printf( "jz short D%d\n", Label );
printf( "cmp ah,%d\n", HEIGHT - 1 );
printf( "mov cx,%d\n", UP );
printf( "jb short D%d\n", Label );
printf( "mov dx,%d\n", WRAPDOWN );
printf( "D%d:\n", Label );
}
void FirstPass( void )
{
char *Op;
unsigned short UpDown = 0;
printf( "org 0%02x00h\n", (Edge << 7) + (New << 4) + (Old << 1) );
// reset cell
printf( "xor byte ptr [bp+1],0%02xh\n", (New ^ Old) << 1 );
// get the screen address and update the display
#ifndef NODRAW
printf( "mov al,160\n" );
printf( "mov bx,[bp+_RowColMap-_CellMap]\n" );
printf( "mul bh\n" );
printf( "add ax,ax\n" );
printf( "mov bh,0\n" );
printf( "add bx,ax\n" ); // bx = screen offset
if( ((New ^ Old) & 6) == 6 )
{
printf( "mov word ptr fs:[bx],0%02x%02xh\n",
(New & 2) ? 15 : 0,
(New & 4) ? 15 : 0 );
if( (New ^ Old) & 1 )
{
printf( "mov byte ptr fs:[bx+2],%s\n",
(New & 1) ? "15" : "dl" );
}
}
else
{
if( ((New ^ Old) & 3) == 3 )
{
printf( "mov word ptr fs:[bx+1],0%02x%02xh\n",
(New & 1) ? 15 : 0,
(New & 2) ? 15 : 0 );
}
else
{
if( (New ^ Old) & 2 )
{
printf( "mov byte ptr fs:[bx+1],%s\n",
(New & 2) ? "15" : "dl" );
}
if( (New ^ Old) & 1 )
{
printf( "mov byte ptr fs:[bx+2],%s\n",
(New & 1) ? "15" : "dl" );
}
}
if( (New ^ Old) & 4 )
{
printf( "mov byte ptr fs:[bx],%s\n",
(New & 4) ? "15" : "dl" );
}
}
#endif
if( (New ^ Old) & 4 ) UpDown += (New & 4) ? 0x48 : -0x48;
if( (New ^ Old) & 2 ) UpDown += (New & 2) ? 0x49 : -0x49;
if( (New ^ Old) & 1 ) UpDown += (New & 1) ? 0x09 : -0x09;
if( Edge )
{
GetUpAndDown(); // ah = row, al = col, cx = up, dx = down
if( (New ^ Old) & 4 )
{
printf( "mov di,%d\n", WRAPLEFT ); // di = left
printf( "cmp al,0\n" );
printf( "je short L%d\n", Label );
printf( "mov di,%d\n", LEFT );
printf( "L%d:\n", Label );
if( New & 4 ) Op = "inc";
else Op = "dec";
printf( "%s word ptr [bp+di]\n", Op );
printf( "add di,cx\n" );
printf( "%s word ptr [bp+di]\n", Op );
printf( "sub di,cx\n" );
printf( "add di,dx\n" );
printf( "%s word ptr [bp+di]\n", Op );
}
if( (New ^ Old) & 1 )
{
printf( "mov di,%d\n", WRAPRIGHT ); // di = right
printf( "cmp al,%d\n", (WIDTH - 1) * 3 );
printf( "je short R%d\n", Label );
printf( "mov di,%d\n", RIGHT );
printf( "R%d:\n", Label );
if( New & 1 ) Op = "add";
else Op = "sub";
printf( "%s word ptr [bp+di],40h\n", Op );
printf( "add di,cx\n" );
printf( "%s word ptr [bp+di],40h\n", Op );
printf( "sub di,cx\n" );
printf( "add di,dx\n" );
printf( "%s word ptr [bp+di],40h\n", Op );
}
printf( "mov di,cx\n" );
printf( "add word ptr [bp+di],%d\n", UpDown );
printf( "mov di,dx\n" );
printf( "add word ptr [bp+di],%d\n", UpDown );
printf( "mov dl,0\n" );
}
else
{
if( (New ^ Old) & 4 )
{
if( New & 4 ) Op = "inc";
else Op = "dec";
printf( "%s byte ptr [bp+%d]\n", Op, LEFT );
printf( "%s byte ptr [bp+%d]\n", Op, UPPERLEFT );
printf( "%s byte ptr [bp+%d]\n", Op, LOWERLEFT );
}
if( (New ^ Old) & 1 )
{
if( New & 1 ) Op = "add";
else Op = "sub";
printf( "%s word ptr [bp+%d],40h\n", Op, RIGHT );
printf( "%s word ptr [bp+%d],40h\n", Op, UPPERRIGHT );
printf( "%s word ptr [bp+%d],40h\n", Op, LOWERRIGHT );
}
if( abs( UpDown ) > 1 )
{
printf( "add word ptr [bp+%d],%d\n", UP, UpDown );
printf( "add word ptr [bp+%d],%d\n", DOWN, UpDown );
}
else
{
if( UpDown == 1 ) Op = "inc";
else Op = "dec";
printf( "%s byte ptr [bp+%d]\n", Op, UP );
printf( "%s byte ptr [bp+%d]\n", Op, DOWN );
}
}
Next1();
}
void Test( char *Offset, char *Str )
{
printf( "mov bx,[bp+%s]\n", Offset );
printf( "cmp bh,[bx]\n" );
printf( "jnz short FIX_%s%d\n", Str, Label );
printf( "%s%d:\n", Str, Label );
}
void Fix( char *Offset, char *Str, int JumpBack )
{
printf( "FIX_%s%d:\n", Str, Label );
printf( "mov bh,[bx]\n" );
printf( "mov [bp+%s],bx\n", Offset );
if( *Offset != 0' ) printf( "lea ax,[bp+%s]\n", Offset );
else printf( "mov ax,bp\n" );
printf( "stosw\n" );
if( JumpBack ) printf( "jmp short %s%d\n", Str, Label );
}
void SecondPass( void )
{
printf( "org 0%02x00h\n",
(Edge << 7) + (New << 4) + (Old << 1) + 1 );
if( Edge )
{
// finished with second pass
if( New == 7 && Old == 0 )
{
printf( "cmp bp,offset DGROUP:ChangeCell\n" );
printf( "jne short NotEnd\n" );
printf( "mov word ptr es:[di],offset DGROUP:ChangeCell\n" );
printf( "pop di si bp ds\n" );
printf( "mov ChangeCell,0\n" );
printf( "retf\n" );
printf( "NotEnd:\n" );
}
GetUpAndDown(); // ah = row, al = col, cx = up, dx = down
printf( "push si\n" );
printf( "mov si,%d\n", WRAPLEFT ); // si = left
printf( "cmp al,0\n" );
printf( "je short L%d\n", Label );
printf( "mov si,%d\n", LEFT );
printf( "L%d:\n", Label );
Test( "si", "LEFT" );
printf( "add si,cx\n" );
Test( "si", "UPPERLEFT" );
printf( "sub si,cx\n" );
printf( "add si,dx\n" );
Test( "si", "LOWERLEFT" );
printf( "mov si,cx\n" );
Test( "si", "UP" );
printf( "mov si,dx\n" );
Test( "si", "DOWN" );
printf( "cmp byte ptr [bp+_RowColMap-_CellMap],%d\n",
(WIDTH - 1) * 3 );
printf( "mov si,%d\n", WRAPRIGHT ); // si = right
printf( "je short R%d\n", Label );
printf( "mov si,%d\n", RIGHT );
printf( "R%d:\n", Label );
Test( "si", "RIGHT" );
printf( "add si,cx\n" );
Test( "si", "UPPERRIGHT" );
printf( "sub si,cx\n" );
printf( "add si,dx\n" );
Test( "si", "LOWERRIGHT" );
}
else
{
Test( itoa( LEFT, Buf, 10 ), "LEFT" );
Test( itoa( UPPERLEFT, Buf, 10 ), "UPPERLEFT" );
Test( itoa( LOWERLEFT, Buf, 10 ), "LOWERLEFT" );
Test( itoa( UP, Buf, 10 ), "UP" );
Test( itoa( DOWN, Buf, 10 ), "DOWN" );
Test( itoa( RIGHT, Buf, 10 ), "RIGHT" );
Test( itoa( UPPERRIGHT, Buf, 10 ), "UPPERRIGHT" );
Test( itoa( LOWERRIGHT, Buf, 10 ), "LOWERRIGHT" );
}
if( New == Old ) Test( "0", "CENTER" );
if( Edge ) printf( "pop si\n" "mov dl,0\n" );
Next2();
if( Edge )
{
Fix( "si", "LEFT", 1 );
Fix( "si", "UPPERLEFT", 1 );
Fix( "si", "LOWERLEFT", 1 );
Fix( "si", "UP", 1 );
Fix( "si", "DOWN", 1 );
Fix( "si", "RIGHT", 1 );
Fix( "si", "UPPERRIGHT", 1 );
Fix( "si", "LOWERRIGHT", New == Old );
}
else
{
Fix( itoa( LEFT, Buf, 10 ), "LEFT", 1 );
Fix( itoa( UPPERLEFT, Buf, 10 ), "UPPERLEFT", 1 );
Fix( itoa( LOWERLEFT, Buf, 10 ), "LOWERLEFT", 1 );
Fix( itoa( UP, Buf, 10 ), "UP", 1 );
Fix( itoa( DOWN, Buf, 10 ), "DOWN", 1 );
Fix( itoa( RIGHT, Buf, 10 ), "RIGHT", 1 );
Fix( itoa( UPPERRIGHT, Buf, 10 ), "UPPERRIGHT", 1 );
Fix( itoa( LOWERRIGHT, Buf, 10 ), "LOWERRIGHT", New == Old );
}
if( New == Old ) Fix( "0", "CENTER", 0 );
if( Edge ) printf( "pop si\n" "mov dl,0\n" );
Next2();
}
void main( void )
{
char *Seg = "ds";
BuildMaps();
printf( "DGROUP group _DATA\n" );
printf( "LIFE segment CODE'\n" );
printf( "assume cs:LIFE,ds:DGROUP,ss:DGROUP,es:NOTHING\n" );
printf( ".386C\n" "public _NextGen\n\n" );
for( Edge = 0; Edge <= 1; Edge++ )
{
for( New = 0; New < 8; New++ )
{
for( Old = 0; Old < 8; Old++ )
{
if( New != Old ) FirstPass(); Label++;
SecondPass(); Label++;
}
}
}
// finished with first pass
printf( "org 0\n" );
printf( "mov si,Change1\n" );
printf( "mov di,Change2\n" );
printf( "mov Change1,di\n" );
printf( "mov Change2,si\n" );
printf( "mov ChangeCell,0F000h\n" );
printf( "mov ax,seg _LDMAP\n" );
printf( "mov ds,ax\n" );
Next2();
// entry point
printf( "_NextGen: push ds bp si di\n" "cld\n" );
if( WIDTH * HEIGHT > LIST_LIMIT ) Seg = "seg _CHANGE";
printf( "mov ax,%s\n", Seg );
printf( "mov es,ax\n" );
#ifndef NODRAW
printf( "mov ax,0A000h\n" );
printf( "mov fs,ax\n" );
#endif
printf( "mov si,Change1\n" );
printf( "mov dl,0\n" );
Next1();
printf( "LIFE ends\nend\n" );
}
```
**LISTING 18.3 MAIN.C**
```c
// MAIN.C
//
// David Stafford
//
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
#include <time.h>
#include <bios.h>
#include "life.h"
// functions in VIDEO.C
void enter_display_mode( void );
void exit_display_mode( void );
void show_text( int x, int y, char *text );
void InitCellmap( void )
{
unsigned int i, j, t, x, y, init;
for( init = (HEIGHT * WIDTH * 3) / 2; init; init )
{
x = random( WIDTH * 3 );
y = random( HEIGHT );
CellMap[ (y * WIDTH) + x / 3 ] |= 0x1000 << (2 - (x % 3));
}
for( i = j = 0; i < WIDTH * HEIGHT; i++ )
{
if( CellMap[ i ] & 0x7000 )
{
ChangeList1[ j++ ] = (short)&CellMap[ i ];
}
}
NextGen(); // Set cell states, prime the pump.
}
void main( void )
{
unsigned long generation = 0;
char gen_text[ 80 ];
long start_time, end_time;
unsigned int seed;
printf( "Seed (0 for random seed): " );
scanf( "%d", &seed );
if( seed == 0 ) seed = (unsigned) time(NULL);
srand( seed );
#ifndef NODRAW
enter_display_mode();
show_text( 0, 10, "Generation:" );
#endif
InitCellmap(); // randomly initialize cell map
_bios_timeofday( _TIME_GETCLOCK, &start_time );
do
{
NextGen();
generation++;
#ifndef NOCOUNTER
sprintf( gen_text, "%10lu", generation );
show_text( 0, 12, gen_text );
#endif
}
#ifdef GEN
while( generation < GEN );
#else
while( !kbhit() );
#endif
_bios_timeofday( _TIME_GETCLOCK, &end_time );
end_time -= start_time;
#ifndef NODRAW
getch(); // clear keypress
exit_display_mode();
#endif
printf( "Total generations: %ld\nSeed: %u\n", generation, seed );
printf( "%ld ticks\n", end_time );
printf( "Time: %f generations/second\n",
(double)generation / (double)end_time * 18.2 );
}
```
**LISTING 18.4 VIDEO.C**
```c
/* VGA mode 13h functions for Game of Life.
Tested with Borland C++. */
#include <stdio.h>
#include <conio.h>
#include <dos.h>
#define TEXT_X_OFFSET 28
#define SCREEN_WIDTH_IN_BYTES 320
#define SCREEN_SEGMENT 0xA000
/* Mode 13h mode-set function. */
void enter_display_mode()
{
union REGS regset;
regset.x.ax = 0x0013;
int86(0x10, &regset, &regset);
}
/* Text mode mode-set function. */
void exit_display_mode()
{
union REGS regset;
regset.x.ax = 0x0003;
int86(0x10, &regset, &regset);
}
/* Text display function. Offsets text to non-graphics area of
screen. */
void show_text(int x, int y, char *text)
{
gotoxy(TEXT_X_OFFSET + x, y);
puts(text);
}
```
**LISTING 18.5 LIFE.H**
```c
void far NextGen( void );
extern unsigned short CellMap[];
extern unsigned short far ChangeList1[];
#define LEFT (-2)
#define RIGHT (+2)
#define UP (WIDTH * LEFT)
#define DOWN (WIDTH * RIGHT)
#define UPPERLEFT (UP + LEFT)
#define UPPERRIGHT (UP + RIGHT)
#define LOWERLEFT (DOWN + LEFT)
#define LOWERRIGHT (DOWN + RIGHT)
#define WRAPLEFT (RIGHT * (WIDTH - 1))
#define WRAPRIGHT (LEFT * (WIDTH - 1))
#define WRAPUP (DOWN * (HEIGHT - 1))
#define WRAPDOWN (UP * (HEIGHT - 1))
```
### Keeping Track of Change with a Change List
In my earlier optimizations to the Game of Life, described in the last
chapter, I noted that most cells in a Life cellmap are dead, and in most
cases all the neighbors are dead as well. This observation enabled me to
get a major speed-up by scanning the cellmap for the few non-zero bytes
(cells that were either alive or have neighbors that are alive).
Although that was a big improvement, it still required my code to touch
every cell to check its state. David has improved on this by maintaining
a *change list;* that is, a list of pointers to cells that change in the
current generation. Only those cells and their neighbors need to be
checked or touched in any way in order to create the next generation,
saving a great many instructions and also a great many cache misses due
to the fact that cellmaps are too big to fit into the 486's internal
cache. During a given generation, David runs down the list of cells that
changed from the previous generation to make the changes for this
generation, and in the process generates the change list for the next
generation.
That's the overall approach, but this being David Stafford, it's not
that simple, of course. I'll let him tell you how his implementation
works in his own words. (I've edited David's text a bit, and added my
own comments in square brackets, so blame me for any errors.)
"Each three cells in the life grid are packed into two bytes, as shown
in Figure 18.1. So, it is convenient if the width of the cell array is
an even multiple of three. There's nothing in the algorithm that
prevents it from supporting any arbitrary size, but the code is a bit
simpler this way. So if you want a 200x200 grid, I recommend just using
a 201x200 grid, and be happy with the extra free column. Otherwise the
edge wrapping code gets more complex.
"Since every cell has from zero to eight neighbors, you may be wondering
how I can manage to keep track of them with only three bits. Each cell
really has only a maximum of seven neighbors since we only need to keep
track of neighbors *outside* of the current cell word. That is, if cell
B' changes state then we don't need to reflect this in the neighbor
counts of cells A' and C.' Updating is made a little faster. [In other
words, when David picks up a word representing three cells, each of the
three cells has at least one of the other cells in that word as a
neighbor, and the state of that neighbor is stored right in that word,
as shown in Figure 18.1. Therefore, the neighbor count for a given cell
never needs to reflect more than seven neighbors, because at least one
of the eight neighbors' states is already encoded in the word.]
![**Figure 18.1**  *Cell triplet storage.*](images/18-01.jpg)
"The basic idea is to maintain a change list.' This is an array of
pointers into the cell array. Each change list element points to a word
which changes in the next generation. This way we don't have to waste
time scanning every cell since most of them do not change. Two passes
are made through the change list. The first pass updates the cell
display on the screen, sets the life/death status of each cell for this
new generation, and updates the neighbor counts for the adjacent cells.
There are some efficiencies gained by using cell triplets rather than
individual cells since we usually don't need to set all eight neighbors.
[Again, the neighbor counts for cells in the same word are implied by
the states of those cells.] The second pass sets the next-generation
states for the cells and their neighbors, and in the process builds the
change list for the next generation.
"Processing each word is a little complex but very fast. A 64K block of
code exists with routines on each 256-byte boundary. Generally speaking,
the entry point corresponds to the high byte of the cell word. This byte
contains the life/death values and a bit to indicate if this is an edge
condition. During the first pass we take the cell triplet word, AND it
with 0XFE00, and jump to that address. During the second pass we take
the cell triplet word, AND it with 0xFE00, OR it with 0x0100, and jump
to that address. [Therefore, there are 128 possible jump targets on the
first pass, and 128 more on the second, all on 256-byte boundaries and
all keyed off the high 7 bits of the cell triplet state; because bit 8
of the jump index is 0 on the first pass and 1 on the second, there is
no conflict. The lower bit isn't needed for other purposes because only
the edge flag bit and the six life/death state bits matter for jumping
into David's state machine. The other nine bits, the bits used for the
neighbor counts, are used only in the next step.]
"Determining which changes must be made to a cell triplet is easy and
surprisingly quick. There's no counting! Instead, I use a 64K lookup
table indexed by the cell triplet itself. The value of the lookup table
entry is equal to what the high byte should be in the next generation.
If this value is equal to the current high byte, then no changes are
necessary to the cell. Otherwise it is placed in the change list. Look
at the code in the `Test()` and `Fix()` functions to see how this is
done." [This step is as important as it is obscure. David has a 64K
table organized so that if you use a word describing a cell triplet as a
lookup index, the byte you will read will be the state of the high byte
for the next generation. In other words, David's table is constructed so
that the edge flag bit, the life/death states, and the three neighbor
count fields form an index to a byte describing the next generation
state for that triplet. In practice, only the next generation field of
the cell changes. Then, if another change to a nearby cell tries to
nudge that cell into changing again, David's code sees that the desired
state is already set, and does not add that cell to the change list
again.]
Segment usage in David's assembly code is summarized in Listing 18.6.
**LISTING 18.6 QLIFE Assembly Segment Usage**
CS : 64K code (table of routines on 256 byte boundaries)
DS : DGROUP (1st pass) / 64K cell life/death classification table (second pass)
ES : Change list
SS : DGROUP; the life cell grid and row/column table
FS : Video segment
GS : Unused
#### A Layperson's Overview of QLIFE
Most likely, you're scratching your head right now in bemusement. I
don't blame you; I felt the same way myself at first. It's actually
pretty simple, though, once you have the hang of it. Basically, David
runs down the change list, visiting every cell that's due to change in
this generation, setting it to the new state, drawing it in the new
state, and adjusting the counts of all its neighbors. David has a
separate assembly routine for every possible change of state for a cell
triplet, and he jumps to the proper routine by taking the cell triplet
word, masking off the lower 9 bits, and jumping to the address where the
appropriate code to perform that particular change of state resides. He
does this for every entry in the change list. When this is completed,
the current generation has been drawn and updated.
Now David runs down the change list again to generate the change list
for the next generation. In this case, for every changed cell triplet,
David looks at that triplet and all affected neighbors to see which will
change in the next generation. He tests for this condition by using each
potentially changed cell triplet word as an index into the
aforementioned lookup table of new states. If the current state matches
the appropriate state for the next generation, then there's nothing to
do and the cell is not added to the change list. If the states don't
match, then the cell is added to the change list, and the appropriate
state for the next generation is set in the cell triplet. David checks
the minimum possible number of cells for change by branching to code
that checks only the relevant cells around each cell triplet in the
current change list; that branching is accomplished by taking the cell
triplet word, masking off the lower 9 bits, setting bit 8 to a 1-bit,
and branching to the routine at that address. As with everything in this
amazing program, this represents the least possible work to accomplish
the desired result—just three instructions:
```nasm
mov dh,[bp+1]
or dh,1
jmp dx
```
These suffice to select the proper, minimum-work code to process the
next cell triplet that has changed, and all potentially affected
neighbors. For all the size of David's code, it has an astonishing
economy of effort, as execution glides through the change list without a
wasted instruction.
Alas, I don't have the room to discuss Peter Klerings' equally
remarkable Life implementation here. I'll close this chapter with a
quote from Terje Mathisen, one of the finest optimizers it has ever been
my pleasure to meet, who, after looking over David's and Peter's
entries, said, "This has been an eye-opening experience for me. I
honestly thought I had the fastest possible approach." TANSTATFC.
There Ain't No Such Thing As the Fastest Code.