diff --git a/01-02.html b/01-02.html
index ee3d008..5c96b87 100644
--- a/01-02.html
+++ b/01-02.html
@@ -39,15 +39,15 @@
Rules for Building High-Performance Code
We’ve got the following rules for creating high-performance software:
-
-- Know where you’re going (understand the objective of the software).
-
- Make a big map (have an overall program design firmly in mind, so the various parts of the program and the data structures work well together).
-
- Make lots of little maps (design an algorithm for each separate part of the overall design).
-
- Know the territory (understand exactly how the computer carries out each task).
-
- Know when it matters (identify the portions of your programs where performance matters, and don’t waste your time optimizing the rest).
-
- Always consider the alternatives (don’t get stuck on a single approach; odds are there’s a better way, if you’re clever and inventive enough).
-
- Know how to turn on the juice (optimize the code as best you know how when it does matter).
-
+
+ - Know where you’re going (understand the objective of the software).
+ - Make a big map (have an overall program design firmly in mind, so the various parts of the program and the data structures work well together).
+ - Make lots of little maps (design an algorithm for each separate part of the overall design).
+ - Know the territory (understand exactly how the computer carries out each task).
+ - Know when it matters (identify the portions of your programs where performance matters, and don’t waste your time optimizing the rest).
+ - Always consider the alternatives (don’t get stuck on a single approach; odds are there’s a better way, if you’re clever and inventive enough).
+ - Know how to turn on the juice (optimize the code as best you know how when it does matter).
+
Making rules is easy; the hard part is figuring out how to apply them in the real world. For my money, examining some actual working code is always a good way to get a handle on programming concepts, so let’s look at some of the performance rules in action.
Know Where You’re Going
diff --git a/04-01.html b/04-01.html
index b293c2b..944b852 100644
--- a/04-01.html
+++ b/04-01.html
@@ -53,12 +53,12 @@
The 8088’s Ancestral Cycle-Eaters
Internally, the 8088 is a 16-bit processor, capable of running at full speed at all times—unless external data is required. External data must traverse the 8088’s external data bus and the PC’s data bus one byte at a time to and from peripherals, with cycle-eaters lurking along every step of the way. What’s more, external data includes not only memory operands but also instruction bytes, so even instructions with no memory operands can suffer from cycle-eaters. Since some of the 8088’s fastest instructions are register-only instructions, that’s important indeed.
The major cycle-eaters are:
-
-- The 8088’s 8-bit external data bus.
-
- The prefetch queue.
-
- Dynamic RAM refresh.
-
- Wait states, notably display memory wait states and, in the AT and 80386 computers, system memory wait states.
-
+
+ - The 8088’s 8-bit external data bus.
+ - The prefetch queue.
+ - Dynamic RAM refresh.
+ - Wait states, notably display memory wait states and, in the AT and 80386 computers, system memory wait states.
+
The locations of these cycle-eaters in the primordial 8088-based PC are shown in Figure 4.1. We’ll cover each of the cycle-eaters in turn in this chapter. The material won’t be easy since cycle-eaters are among the most subtle aspects of assembly programming. By the same token, however, this will be one of the most important and rewarding chapters in this book. Don’t worry if you don’t catch everything in this chapter, but do read it all even if the going gets a bit tough. Cycle-eaters play a key role in later chapters, so some familiarity with them is highly desirable.
The 8-Bit Bus Cycle-Eater
diff --git a/04-10.html b/04-10.html
index bc9f5af..334d321 100644
--- a/04-10.html
+++ b/04-10.html
@@ -54,12 +54,12 @@
Cycle-Eaters: A Summary
We’ve covered a great deal of sophisticated material in this chapter, so don’t feel bad if you haven’t understood everything you’ve read; it will all become clear from further reading, especially once you study, time, and tune code that you have written yourself. What’s really important is that you come away from this chapter understanding that on the 8088:
-
-- The 8-bit bus cycle-eater causes each access to a word-sized operand to be 4 cycles longer than an equivalent access to a byte-sized operand.
-
- The prefetch queue cycle-eater can cause instruction execution times to be as much as four times longer than the officially documented cycle times.
-
- The DRAM refresh cycle-eater slows most PC code, with performance reductions ranging as high as 8.33 percent.
-
- The display adapter cycle-eater typically doubles and can more than triple the length of the standard 4-cycle access to display memory, with intensive display memory access suffering most.
-
+
+ - The 8-bit bus cycle-eater causes each access to a word-sized operand to be 4 cycles longer than an equivalent access to a byte-sized operand.
+ - The prefetch queue cycle-eater can cause instruction execution times to be as much as four times longer than the officially documented cycle times.
+ - The DRAM refresh cycle-eater slows most PC code, with performance reductions ranging as high as 8.33 percent.
+ - The display adapter cycle-eater typically doubles and can more than triple the length of the standard 4-cycle access to display memory, with intensive display memory access suffering most.
+
This basic knowledge about cycle-eaters puts you in a good position to understand the results reported by the Zen timer, and that means that you’re well on your way to writing high-performance assembler code.
What Does It All Mean?
diff --git a/08-02.html b/08-02.html
index fa06b76..da2b218 100644
--- a/08-02.html
+++ b/08-02.html
@@ -77,13 +77,13 @@ jz Match
More on this shortly.
To recap, here are some things to look for when striving to convert C code into optimized assembly language:
-
-- Move the entire performance-critical section into a single assembly language function.
-
- Don’t use calls or stack frame accesses inside the critical code, if possible, and avoid unnecessary memory accesses of any kind.
-
- Change segments as infrequently as possible.
-
- Optimize in terms of what assembly does well, not in terms of fine-tuning compiled C code.
-
- Change the rules to the benefit of assembly, if necessary; for example, reorganize data structto allow efficient assembly language processing.
-
+
+ - Move the entire performance-critical section into a single assembly language function.
+ - Don’t use calls or stack frame accesses inside the critical code, if possible, and avoid unnecessary memory accesses of any kind.
+ - Change segments as infrequently as possible.
+ - Optimize in terms of what assembly does well, not in terms of fine-tuning compiled C code.
+ - Change the rules to the benefit of assembly, if necessary; for example, reorganize data structto allow efficient assembly language processing.
+
That said, let me show some of these precepts in action.
A C-to-Assembly Case Study
diff --git a/11-03.html b/11-03.html
index 52b0e36..fc49fbc 100644
--- a/11-03.html
+++ b/11-03.html
@@ -40,19 +40,19 @@
The picture is less clear in the 386 world since there are so many different memory architectures, but similar problems can occur in any computer built around a 286 or 386. The prefetch queue cycle-eater is even a factor—albeit a lesser one—on zero-wait-state machines, both because branching empties the queue and because some instructions can outrun even zero—5 cycles longer than the official execution time.)
To summarize:
-
-- Memory-accessing instructions don’t run at their official speeds on non-zero-wait-state 286/386 computers.
-
- The prefetch queue cycle-eater reduces performance on 286/386 computers, particularly when non-zero-wait-state memory is used.
-
- Branches often execute at less than their rated speeds on the 286 and 386 since the prefetch queue is emptied.
-
- The extent to which the prefetch queue and wait states affect performance varies from one 286/386 computer to another, making precise optimization impossible.
-
+
+ - Memory-accessing instructions don’t run at their official speeds on non-zero-wait-state 286/386 computers.
+ - The prefetch queue cycle-eater reduces performance on 286/386 computers, particularly when non-zero-wait-state memory is used.
+ - Branches often execute at less than their rated speeds on the 286 and 386 since the prefetch queue is emptied.
+ - The extent to which the prefetch queue and wait states affect performance varies from one 286/386 computer to another, making precise optimization impossible.
+
What’s to be learned from all this? Several things:
-
-- Keep your instructions short.
-
- Keep it in the registers; avoid memory, since memory generally can’t keep up with the processor.
-
- Don’t jump.
-
+
+ - Keep your instructions short.
+ - Keep it in the registers; avoid memory, since memory generally can’t keep up with the processor.
+ - Don’t jump.
+
Of course, those are exactly the rules that apply to 8088 optimization as well. Isn’t it convenient that the same general rules apply across the board?
Data Alignment
diff --git a/17-01.html b/17-01.html
index c06c9fe..4b04f8d 100644
--- a/17-01.html
+++ b/17-01.html
@@ -48,10 +48,10 @@
The Rules of the Game
The Game of Life is ridiculously simple. There is a cellmap, consisting of a rectangular matrix of cells, each of which may initially be either on or off. Each cell has eight neighbors: two horizontally, two vertically, and four diagonally. For each succeeding generation of cells, the game logic determines whether each cell will be on or off according to the following rules:
-
-- If a cell is on and has either two or three neighbors that are on in the current generation, it stays on; otherwise, the cell turns off.
-
- If a cell is off and has exactly three “on” neighbors in the current generation, it turns on; otherwise, it stays off. That’s all the rules there are—but they give rise to an astonishing variety of forms, including patterns that spin, march across the screen, and explode.
-
+
+ - If a cell is on and has either two or three neighbors that are on in the current generation, it stays on; otherwise, the cell turns off.
+ - If a cell is off and has exactly three “on” neighbors in the current generation, it turns on; otherwise, it stays off. That’s all the rules there are—but they give rise to an astonishing variety of forms, including patterns that spin, march across the screen, and explode.
+
It’s only a little more complicated to implement the Game of Life than it is to describe it. Listing 17.1, together with the display functions in Listing 17.2, is a C++ implementation of the Game of Life, and it’s very straightforward. A cellmap is an object that’s accessible through member functions to set, clear, and test cell states, and through a member function to calculate the next generation. Calculating the next generation involves nothing more than using the other member functions to set each cell to the appropriate state, given the number of neighboring on-cells and the cell’s current state. The only complication is that it’s necessary to place the next generation’s cells in another cellmap, and then copy the final result back to the original cellmap. This keeps us from corrupting the current generation’s cellmap before we’re done using it to calculate the next generation.
All in all, Listing 17.1 is a clean, compact, and elegant implementation of the Game of Life. Were it not that the code is as slow as molasses, we could stop right here.
diff --git a/17-06.html b/17-06.html
index e21bac0..a0ac10d 100644
--- a/17-06.html
+++ b/17-06.html
@@ -38,11 +38,11 @@
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.
-
+
+ - 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
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.
diff --git a/17-08.html b/17-08.html
index f072c1a..a07036d 100644
--- a/17-08.html
+++ b/17-08.html
@@ -47,14 +47,14 @@
The Challenge That Ate My Life
The most recent optimization challenge I laid my community of readers was to write the fastest possible Game of Life generation engine. By “engine” I meant that I didn’t care about time spent in input or output, only time consumed by the call to next-generation. The time spent updating the cellmap was what I wanted people to concentrate on.
Here are the rules I laid down for the challenge:
-
-- Readers could modify any code in Listing 17.5, except the main loop, as well as change the cell map representation any way they liked. However, the code had to produce exactly the same output as Listing 17.5 under all circumstances in order to be eligible to win.
-
- Engine code had to be less than 400 lines long in total, excluding the video-related code shown in Listing 17.2.
-
- Submissions had to compile/assemble with Borland C++ (in either C++ or C mode, as desired) and/or TASM.
-
- All submissions had to handle cellmaps at least 200x200 in size.
-
- Assembly language could of course be used to speed up any part of the program. C rather than C++ was legal as well, so long as entered implementations produced the same results as Listing 17.5 and 17.2 together and were less than 400 lines long.
-
- All entries would be timed on the same 33 MHz 486 with a 256K external cache.
-
+
+ - Readers could modify any code in Listing 17.5, except the main loop, as well as change the cell map representation any way they liked. However, the code had to produce exactly the same output as Listing 17.5 under all circumstances in order to be eligible to win.
+ - Engine code had to be less than 400 lines long in total, excluding the video-related code shown in Listing 17.2.
+ - Submissions had to compile/assemble with Borland C++ (in either C++ or C mode, as desired) and/or TASM.
+ - All submissions had to handle cellmaps at least 200x200 in size.
+ - Assembly language could of course be used to speed up any part of the program. C rather than C++ was legal as well, so long as entered implementations produced the same results as Listing 17.5 and 17.2 together and were less than 400 lines long.
+ - All entries would be timed on the same 33 MHz 486 with a 256K external cache.
+
That was the challenge I put to the readers. Little did I realize the challenge it would lay on me: Entries poured in from the four corners of the globe. Some were plain, some were brilliant, some were, well, berserk. Many didn’t even work. But all had to be gone through, examined for adherence to the rules, read, compiled, linked, run, and judged. I learned a lot—about a lot of things, not the least of which was the process (or maybe the wisdom) of laying down challenges to readers.
Who won? What did I learn? To find out, read on.
diff --git a/38-02.html b/38-02.html
index 811a1b4..df1c6e4 100644
--- a/38-02.html
+++ b/38-02.html
@@ -38,14 +38,14 @@
How Do You Fit Polygons Together?
How, then, do you fit polygons together? Very carefully. First, the line-tracing algorithm must be adjusted so that it selects only those pixels that are truly inside the polygon. This basically requires shifting a standard line-drawing algorithm horizontally by one half-pixel toward the polygon’s interior. That leaves the issue of how to handle points that are exactly on the boundary, and points that lie at vertices, so that those points are drawn once and only once. To deal with that, we’re going to adopt the following rules:
-
-- Points located exactly on nonhorizontal edges are drawn only if the interior of the polygon is directly to the right (left edges are drawn, right edges aren’t).
-

Figure 38.3 The adjacent polygons problem.
-
-- Points located exactly on horizontal edges are drawn only if the interior of the polygon is directly below them (horizontal top edges are drawn, horizontal bottom edges aren’t).
-
- A vertex is drawn only if all lines ending at that point meet the above conditions (no right or bottom edges end at that point).
-
+
All edges of a polygon except those that are flat tops or flat bottoms will be considered either right edges or left edges, regardless of slope. The left edge is the one that starts with the leftmost line down from the top of the polygon.
These rules ensure that no pixel is drawn more than once when adjacent polygons are filled, and that if polygons cover the full 360-degree range around a pixel, then that pixel will be drawn once and only once—just what we need in order to be able to fit filled polygons together seamlessly.
diff --git a/39-01.html b/39-01.html
index 68f7049..89c2e9c 100644
--- a/39-01.html
+++ b/39-01.html
@@ -51,11 +51,11 @@
In addressing the topic of filling convex polygons in the previous chapter, the implementation we came up with met all of our functional requirements. In particular, it met stringent rules that guaranteed that polygons would never overlap or have gaps at shared edges, an important consideration when building polygon-based images. Unfortunately, the implementation was also slow as molasses. In this chapter we’ll work up polygon-filling code that’s fast enough to be truly usable.
Our original polygon filling code involved three major tasks, each performed by a separate function:
-
-- Tracing each polygon edge to generate a coordinate list (performed by the function ScanEdge);
-
- Drawing the scanned-out horizontal lines that constitute the filled polygon (DrawHorizontalLineList ); and
-
- Characterizing the polygon and coordinating the tracing and drawing (FillConvexPolygon ).
-
+
+ - Tracing each polygon edge to generate a coordinate list (performed by the function ScanEdge);
+ - Drawing the scanned-out horizontal lines that constitute the filled polygon (DrawHorizontalLineList ); and
+ - Characterizing the polygon and coordinating the tracing and drawing (FillConvexPolygon ).
+
The amount of time that the previous chapter’s sample program spent in each of these areas is shown in Table 39.1. As you can see, half the time was spent drawing and the other half was spent tracing the polygon edges (the time spent in FillConvexPolygon was relatively minuscule), so we have our choice of where to begin optimizing.
Fast Drawing
Let’s start with drawing, which is easily sped up. The previous chapter’s code used a double-nested loop that called a draw-pixel function to plot each pixel in the polygon individually. That’s a ridiculous approach in a graphics mode that offers linearly mapped memory, as does VGA mode 13H, the mode in which we’re working. At the very least, we could point a far pointer to the left edge of each polygon scan line, then draw each pixel in that scan line in quick succession, using something along the lines of *ScrPtr++ = FillColor; inside a loop.
diff --git a/52-01.html b/52-01.html
index 609a03c..c934cfb 100644
--- a/52-01.html
+++ b/52-01.html
@@ -48,13 +48,13 @@
This Chapter’s Demo Program
Three-dimensional animation is a complicated business, and it takes an astonishing amount of functionality just to get off the launching pad: page flipping, polygon filling, clipping, transformations, list management, and so forth. I’ve been building toward a critical mass of animation functionality over the course of this book, and this chapter’s code builds on the code from no fewer than five previous chapters. The code that’s required in order to link this chapter’s animation demo program is the following:
-
-- Listing 50.1 from Chapter 50 (draw clipped line list);
-
- Listings 47.1 and 47.6 from Chapter 47 (Mode X mode set, rectangle fill);
-
- Listing 49.6 from Chapter 49;
-
- Listing 39.4 from Chapter 39 (polygon edge scan); and
-
- The FillConvexPolygon( ) function from Listing 38.1 from Chapter 38. Note that the struct keywords in FillConvexPolygon( ) must be removed to reflect the switch to typedefs in the animation header file.
-
+
+ - Listing 50.1 from Chapter 50 (draw clipped line list);
+ - Listings 47.1 and 47.6 from Chapter 47 (Mode X mode set, rectangle fill);
+ - Listing 49.6 from Chapter 49;
+ - Listing 39.4 from Chapter 39 (polygon edge scan); and
+ - The FillConvexPolygon( ) function from Listing 38.1 from Chapter 38. Note that the struct keywords in FillConvexPolygon( ) must be removed to reflect the switch to typedefs in the animation header file.
+
As always, all required files are in this chapter’s subdirectory on the CD-ROM.
diff --git a/62-03.html b/62-03.html
index a587ee8..f1873f4 100644
--- a/62-03.html
+++ b/62-03.html
@@ -38,13 +38,13 @@
The Rendering Pipeline
Conceptually rendering from a BSP tree really is that simple, but the implementation is a bit more complicated. The full rendering pipeline, as coordinated by UpdateWorld(), is this:
-
-- Update the current location.
-
- Transform all wall endpoints into viewspace (the world as seen from the current location with the current viewing angle).
-
- Clip all walls to the view pyramid.
-
- Project wall vertices to screen coordinates.
-
- Walk the walls back to front, and for each wall that lies at least partially in the view pyramid, perform backface culling (skip walls facing away from the viewer), and draw the wall if it’s not culled.
-
+
+ - Update the current location.
+ - Transform all wall endpoints into viewspace (the world as seen from the current location with the current viewing angle).
+ - Clip all walls to the view pyramid.
+ - Project wall vertices to screen coordinates.
+ - Walk the walls back to front, and for each wall that lies at least partially in the view pyramid, perform backface culling (skip walls facing away from the viewer), and draw the wall if it’s not culled.
+
Next, we’ll look at each part of the pipeline more closely. The pipeline is too complex for me to be able to discuss each part in complete detail. Some sources for further reading are Computer Graphics, by Foley and van Dam (ISBN 0-201-12110-7), and the DDJ Essential Books on Graphics Programming CD.
Moving the Viewer
The sample BSP program performs first-person rendering; that is, it renders the world as seen from your eyes as you move about. The rate of movement is controlled by key-handling code that’s not shown in Listing 62.1; however, the variables set by the key-handling code are used in UpdateViewPos() to bring the current location up to date.