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

116 lines
No EOL
6.9 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.

We're still not ready for assembly, though; what we need is a new
perspective that lends itself to vastly better performance in C++. The
Life program in the next section is *three to seven times* faster than
Listing 17.4—and it's still in C++.
How is this possible? Here are some hints:
- After a few dozen generations, most of the cellmap consists of cells
in the off state.
- There are many possible cellmap representations other than one
bit-per-pixel.
- Cells change state relatively infrequently.
### Bringing In the Right Brain {#Heading8}
In the previous section, we saw how a C++ program could be sped up about
eight times simply by rearranging the data and code in straightforward
ways. Now we're going to see how right-brain non-linear optimization can
speed things up by another four times—and make the code *simpler.*
Now *that's* Zen code optimization.
I have two objectives to achieve in the remainder of this chapter.
First, I want to show that optimization consists of many levels, from
assembly language up to conceptual design, and that assembly language
kicks in pretty late in the optimization process. Second, I want to
encourage you to saturate your brain with everything you know about any
particular optimization problem, then make space for your right brain to
solve the problem.
#### Re-Examining the Task {#Heading9}
Earlier in this chapter, we looked at a straightforward Game of Life
implementation, then increased performance considerably by making the
implementation a little less abstract and a little less general. We made
a small change to the cellmap format, adding padding bytes off the edges
so that pointer arithmetic would always work, but the major
optimizations were moving the critical code into a single loop and using
pointers rather than member functions whenever possible. In other words,
we took what we already knew and made it more efficient.
Now it's time to re-examine the nature of this programming task from the
ground up, looking for things that we *don't* yet know. Let's take a
moment to review what the Game of Life consists of. The basic task is
evolving a new generation, and that's done by looking at the number of
"on" neighbors a cell has and the cell's own state. If a cell is on, and
two or three neighbors are on, then the cell stays on; otherwise, an
on-cell is turned off. If a cell is off and exactly three neighbors are
on, then the cell is turned on; otherwise, an off-cell stays off. That's
all there is to it. As any fool can see, the trick is to arrange things
so that we can count neighbors and check the cell state as quickly as
possible. Large lookup tables, oddly encoded cellmaps, and lots of
bit-twiddling assembly code spring to mind as possible approaches. Can't
you just feel your adrenaline start to pump?
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Relax. Step back. Try to divine the true nature of the problem. The object is not to count neighbors and check cell states as quickly as possible; that's just one possible implementation. The object is to determine when a cell's state must be changed and to change it appropriately, and that's what we need to do as quickly as possible.*
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
What difference does that new perspective make? Let's approach it this
way. What does a typical cellmap look like? As it happens, after a few
generations, the vast majority of cells are off. In fact, the vast
majority of cells are not only off but are entirely surrounded by
off-cells. Also, cells change state infrequently; in any given
generation after the first few, most cells remain in the same state as
in the previous generation.
Do you see where I'm heading? Do you hear a whisper of inspiration from
your right brain? The original implementation stored cell states as
1-bits (on), or 0-bits (off). For each generation and for each cell, it
counted the states of the eight neighbors, for an average of eight
operations per cell per generation. Suppose, now, that on average 10
percent of cells change state from one generation to the next. (The
actual percentage is even lower, but this will do for illustration.)
Suppose also that we change the cell map format to store a byte rather
than a bit for each cell, with the byte storing not only the cell state
but also the count of neighboring on-cells for that cell. Figure 17.3
shows this format. Then, rather than counting neighbors each time, we
could just look at the neighbor count in the cell and operate directly
from that.
But what about the overhead needed to maintain the neighbor counts?
Well, each time a cell changes state, eight operations would be needed
to update the counts in the eight neighboring cells. But this happens
only once every ten cells, on average—so the cost of this approach is
only one-tenth that of the original approach!
*Know your data.*
![](images/17-03.jpg)\
**Figure 17.3**  *New cell format.*
#### Acting on What We Know {#Heading10}
Once we've changed the cellmap format to store neighbor counts as well
as states, with a byte for each cell, we can get another performance
boost by again examining what we know about our data. I said earlier
that most cells are off during any given generation. This means that
most cells have no neighbors that are on. Since the cell map
representation for an off-cell that has no neighbors is a zero byte, we
can skip over scads of unchanged cells at a pop simply by scanning for
non-zero bytes. This is much faster than explicitly testing cell states
and neighbor counts, and lends itself beautifully to assembly language
implementation as **REPZ SCASB** or (with a little cleverness) **REPZ
SCASW.** (Unfortunately, there's no C library function that can scan
memory for the next byte that's non-zero.)
Listing 17.5 is a Game of Life implementation that uses the
neighbor-count cell map format and scans for non-zero bytes. On a 20 MHz
386, Listing 17.5 is about 4.5 times faster at calculating generations
(that is, the generation engine is 4.5 times faster; I'm ignoring the
time consumed by drawing and text display) than Listing 17.4, which is
no slouch. On a 33 MHz 486, Listing 17.5 is about 3.5 times faster than
Listing 17.4. This is true even though Listing 17.5 must be compiled
using the large model. Imagine that—getting a four times speed-up while
switching from the small model to the large model!