abrash-black-book/18-05.md
2013-12-30 20:26:41 +11:00

111 lines
No EOL
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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.

"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 {#Heading6}
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:
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.