diff --git a/01-01.html b/01-01.html index 43571c9..b0231e0 100644 --- a/01-01.html +++ b/01-01.html @@ -53,7 +53,7 @@

Notice that the above definition most emphatically does not say anything about making the software as fast as possible. It also does not say anything about using assembly language, or an optimizing compiler, or, for that matter, a compiler at all. It also doesn’t say anything about how the code was designed and written. What it does say is that high-performance code shouldn’t get in the user’s way—and that’s all.

That’s an important distinction, because all too many programmers think that assembly language, or the right compiler, or a particular high-level language, or a certain design approach is the answer to creating high-performance code. They’re not, any more than choosing a certain set of tools is the key to building a house. You do indeed need tools to build a house, but any of many sets of tools will do. You also need a blueprint, an understanding of everything that goes into a house, and the ability to use the tools.

Likewise, high-performance programming requires a clear understanding of the purpose of the software being built, an overall program design, algorithms for implementing particular tasks, an understanding of what the computer can do and of what all relevant software is doing—and solid programming skills, preferably using an optimizing compiler or assembly language. The optimization at the end is just the finishing touch, however.

-
Without good design, good algorithms, and complete understanding of the program’s operation, your carefully optimized code will amount to one of mankind’s least fruitful creations—a fast slow program. +
Without good design, good algorithms, and complete understanding of the program’s operation, your carefully optimized code will amount to one of mankind’s least fruitful creations—a fast slow program.

“What’s a fast slow program?” you ask. That’s a good question, and a brief (true) story is perhaps the best answer.

diff --git a/01-02.html b/01-02.html index 70e591e..b123347 100644 --- a/01-02.html +++ b/01-02.html @@ -109,7 +109,7 @@ main(int argc, char *argv[]) {

Table 1.1 shows the time taken for Listing 1.1 to generate a checksum of the WordPerfect version 4.2 thesaurus file, TH.WP (362,293 bytes in size), on a 10 MHz AT machine of no special parentage. Execution times are given for Listing 1.1 compiled with Borland and Microsoft compilers, with optimization both on and off; all four times are pretty much the same, however, and all are much too slow to be acceptable. Listing 1.1 requires over two and one-half minutes to checksum one file!

-
Listings 1.2 and 1.3 form the C/assembly equivalent to Listing 1.1, and Listings 1.6 and 1.7 form the C/assembly equivalent to Listing 1.5. +
Listings 1.2 and 1.3 form the C/assembly equivalent to Listing 1.1, and Listings 1.6 and 1.7 form the C/assembly equivalent to Listing 1.5.

These results make it clear that it’s folly to rely on your compiler’s optimization to make your programs fast. Listing 1.1 is simply poorly designed, and no amount of compiler optimization will compensate for that failing. To drive home the point, conListings 1.2 and 1.3, which together are equivalent to Listing 1.1 except that the entire checksum loop is written in tight assembly code. The assembly language implementation is indeed faster than any of the C versions, as shown in Table 1.1, but it’s less than 10 percent faster, and it’s still unacceptably slow.


diff --git a/01-04.html b/01-04.html index 599a2f0..3f24d28 100644 --- a/01-04.html +++ b/01-04.html @@ -37,7 +37,7 @@


-
Make sure you understand what really goes on when you insert a seemingly-innocuous function call into the time-critical portions of your code. +Make sure you understand what really goes on when you insert a seemingly-innocuous function call into the time-critical portions of your code.

In this case that means knowing how DOS and the C/C++ file-access libraries do their work. In other words, know the territory!

LISTING 1.4 L1-4.C

@@ -81,7 +81,7 @@ main(int argc, char *argv[]) {

Know When It Matters

The last section contained a particularly interesting phrase: the time-critical portions of your code. Time-critical portions of your code are those portions in which the speed of the code makes a significant difference in the overall performance of your program—and by “significant,” I don’t mean that it makes the code 100 percent faster, or 200 percent, or any particular amount at all, but rather that it makes the program more responsive and/or usable from the user’s perspective.

Don’t waste time optimizing non-time-critical code: set-up code, initialization code, and the like. Spend your time improving the performance of the code inside heavily-used loops and in the portions of your programs that directly affect response time. Notice, for example, that I haven’t bothered to implement a version of the checksum program entirely in assembly; Listings 1.2 and 1.6 call assembly subroutines that handle the time-critical operations, but C is still used for checking command-line parameters, operning files, printing, and the like.

-
If you were to implement any of the listings in this chapter entirely in hand-optimized assembly, I suppose you might get a performance improvement of a few percent—but I rather doubt you’d get even that much, and you’d sure as heck spend an awful lot of time for whatever meager improvement does result. Let C do what it does well, and use assembly only when it makes a perceptible difference. +
If you were to implement any of the listings in this chapter entirely in hand-optimized assembly, I suppose you might get a performance improvement of a few percent—but I rather doubt you’d get even that much, and you’d sure as heck spend an awful lot of time for whatever meager improvement does result. Let C do what it does well, and use assembly only when it makes a perceptible difference.

Besides, we don’t want to optimize until the design is refined to our satisfaction, and that won’t be the case until we’ve thought about other approaches.

diff --git a/01-05.html b/01-05.html index 157c5be..83e27ac 100644 --- a/01-05.html +++ b/01-05.html @@ -37,7 +37,7 @@


The third reason is often fallacious. C library functions are not always written in assembly, nor are they always particularly well-optimized. (In fact, they’re often written for portability, which has nothing to do with optimization.) What’s more, they’re general-purpose functions, and often can be outperformed by well-but-not- brilliantly-written code that is well-matched to a specific task. As an example, consider Listing 1.5, which uses internal buffering to handle blocks of bytes at a time. Table 1.1 shows that Listing 1.5 is 2.5 to 4 times faster than Listing 1.4 (and as much as 49 times faster than Listing 1.1!), even though it uses no assembly at all.

-
Clearly, you can do well by using special-purpose C code in place of a C library function—if you have a thorough understanding of how the C library function operates and exactly what your application needs done. Otherwise, you’ll end up rewriting C library functions in C, which makes no sense at all. +
Clearly, you can do well by using special-purpose C code in place of a C library function—if you have a thorough understanding of how the C library function operates and exactly what your application needs done. Otherwise, you’ll end up rewriting C library functions in C, which makes no sense at all.

LISTING 1.5 L1-5.C

diff --git a/01-06.html b/01-06.html index 530ea45..3b6bffb 100644 --- a/01-06.html +++ b/01-06.html @@ -93,7 +93,7 @@ _ChecksumChunkendp

Note that in Table 1.1, optimization makes little difference except in the case of Listing 1.5, where the design has been refined considerably. Execution time in the other cases is dominated by time spent in DOS and/or the C library, so optimization of the code you write is pretty much irrelevant. What’s more, while the approximately two-times improvement we got by optimizing is not to be sneezed at, it pales against the up-to-50-times improvement we got by redesigning.

By the way, the execution times even of Listings 1.6 and 1.7 are dominated by DOS disk access times. If a disk cache is enabled and the file to be checksummed is already in the cache, the assembly version is three times as fast as the C version. In other words, the inherent nature of this application limits the performance improvement that can be obtained via assembly. In applications that are more CPU-intensive and less disk-bound, particularly those applications in which string instructions and/or unrolled loops can be used effectively, assembly tends to be considerably faster relative to C than it is in this very specific case.

-
Don’t get hung up on optimizing compilers or assembly language—the best optimizer is between your ears. +
Don’t get hung up on optimizing compilers or assembly language—the best optimizer is between your ears.

All this is basically a way of saying: Know where you’re going, know the territory, and know when it matters.

diff --git a/02-01.html b/02-01.html index 054af38..3a603a6 100644 --- a/02-01.html +++ b/02-01.html @@ -59,7 +59,7 @@ LoopTop:

Now, it’s hard to write code that’s much faster than seven instructions, only one of which accesses memory, and most programmers would have called it a day at this point. Still, something bothered me, so I spent a bit of time going over the code again. Suddenly, the answer struck me—the code was rotating each bit into place separately, so that a multibit rotation was being performed every time through the loop, for a total of four separate time-consuming multibit rotations!

-
While the instructions themselves were individually optimized, the overall approach did not make the best possible use of the instructions. +
While the instructions themselves were individually optimized, the overall approach did not make the best possible use of the instructions.

I changed the code to the following:

diff --git a/02-02.html b/02-02.html index b1cd455..9f57490 100644 --- a/02-02.html +++ b/02-02.html @@ -56,7 +56,7 @@

In the PC world, you can never have enough knowledge, and every item you add to your store will make your programs better. Thorough familiarity with both the operating system APIs and BIOS interfaces is important; since those interfaces are well-documented and reasonably straightforward, my advice is to get a good book or two and bring yourself up to speed. Similarly, familiarity with the PC hardware is required. While that topic covers a lot of ground—display adapters, keyboards, serial ports, printer ports, timer and DMA channels, memory organization, and more—most of the hardware is well-documented, and articles about programming major hardware components appear frequently in the literature, so this sort of knowledge can be acquired readily enough.

The single most critical aspect of the hardware, and the one about which it is hardest to learn, is the CPU. The x86 family CPUs have a complex, irregular instruction set, and, unlike most processors, they are neither straightforward nor wellregarding true code performance. What’s more, assembly is so difficult to learn that most articles and books that present assembly code settle for code that just works, rather than code that pushes the CPU to its limits. In fact, since most articles and books are written for inexperienced assembly programmers, there is very little information of any sort available about how to generate high-quality assembly code for the x86 family CPUs. As a result, knowledge about programming them effectively is by far the hardest knowledge to gather. A good portion of this book is devoted to seeking out such knowledge.

-
Be forewarned, though: No matter how much you learn about programming the PC in assembly, there’s always more to discover. +
Be forewarned, though: No matter how much you learn about programming the PC in assembly, there’s always more to discover.


diff --git a/02-03.html b/02-03.html index 19a6396..f706aba 100644 --- a/02-03.html +++ b/02-03.html @@ -41,7 +41,7 @@

Basically, there are only two possible objectives to high-performance assembly programming: Given the requirements of the application, keep to a minimum either the number of processor cycles the program takes to run, or the number of bytes in the program, or some combination of both. We’ll look at ways to achieve both objectives, but we’ll more often be concerned with saving cycles than saving bytes, for the PC generally offers relatively more memory than it does processing horsepower. In fact, we’ll find that two-to-three times performance improvements over already tight assembly code are often possible if we’re willing to spend additional bytes in order to save cycles. It’s not always desirable to use such techniques to speed up code, due to the heavy memory requirements—but it is almost always possible.

You will notice that my short list of objectives for high-performance assembly programming does not include traditional objectives such as easy maintenance and speed of development. Those are indeed important considerations—to persons and companies that develop and distribute software. People who actually buy software, on the other hand, care only about how well that software performs, not how it was developed nor how it is maintained. These days, developers spend so much time focusing on such admittedly important issues as code maintainability and reusability, source code control, choice of development environment, and the like that they often forget rule #1: From the user’s perspective, performance is fundamental.

-
Comment your code, design it carefully, and write non-time-critical portions in a high-level language, if you wish—but when you write the portions that interact with the user and/or affect response time, performance must be your paramount objective, and assembly is the path to that goal. +
Comment your code, design it carefully, and write non-time-critical portions in a high-level language, if you wish—but when you write the portions that interact with the user and/or affect response time, performance must be your paramount objective, and assembly is the path to that goal.

Knowledge of the sort described earlier is absolutely essential to fulfilling either of the objectives of assembly programming. What that knowledge doesn’t do by itself is meet the need to write code that both performs to the requirements of the application at hand and also operates as efficiently as possible in the PC environment. Knowledge makes that possible, but your programming instincts make it happen. And it is that intuitive, on-the-fly integration of a program specification and a sea of facts about the PC that is the heart of the Zen-class assembly optimization.

diff --git a/03-01.html b/03-01.html index bbda66f..e1612e5 100644 --- a/03-01.html +++ b/03-01.html @@ -46,7 +46,7 @@

The Costs of Ignorance

As diligent as the author had been, he had nonetheless committed a cardinal sin of x86 assembly language programming: He had assumed that the information available to him was both correct and complete. While the execution times provided by Intel for its processors are indeed correct, they are incomplete; the other—and often more important—part of code performance is instruction fetch time, a topic to which I will return in later chapters.

Had the author taken the time to measure the true performance of his code, he wouldn’t have put his reputation on the line with relatively low-performance code. What’s more, had he actually measured the performance of his code and found it to be unexpectedly slow, curiosity might well have led him to experiment further and thereby add to his store of reliable information about the CPU.

-
There you have an important tenet of assembly language optimization: After crafting the best code possible, check it in action to see if it’s really doing what you think it is. If it’s not behaving as expected, that’s all to the good, since solving mysteries is the path to knowledge. You’ll learn more in this way, I assure you, than from any manual or book on assembly language. +
There you have an important tenet of assembly language optimization: After crafting the best code possible, check it in action to see if it’s really doing what you think it is. If it’s not behaving as expected, that’s all to the good, since solving mysteries is the path to knowledge. You’ll learn more in this way, I assure you, than from any manual or book on assembly language.

Assume nothing. I cannot emphasize this strongly enough—when you care about performance, do your best to improve the code and then measure the improvement. If you don’t measure performance, you’re just guessing, and if you’re guessing, you’re not very likely to write top-notch code.

Ignorance about true performance can be costly. When I wrote video games for a living, I spent days at a time trying to wring more performance from my graphics drivers. I rewrote whole sections of code just to save a few cycles, juggled registers, and relied heavily on blurry-fast register-to-register shifts and adds. As I was writing my last game, I discovered that the program ran perceptibly faster if I used look-up tables instead of shifts and adds for my calculations. It shouldn’t have run faster, according to my cycle counting, but it did. In truth, instruction fetching was rearing its head again, as it often does, and the fetching of the shifts and adds was taking as much as four times the nominal execution time of those instructions.

diff --git a/04-04.html b/04-04.html index 71e776f..aa80c4d 100644 --- a/04-04.html +++ b/04-04.html @@ -39,7 +39,7 @@

Official Execution Times Are Only Part of the Story

The sequence of 5 SHR instructions in the last example is 10 bytes long. That means that it can never execute in less than 24 cycles even if the 4-byte prefetch queue is full when it starts, since 6 instruction bytes would still remain to be fetched, at 4 cycles per fetch. If the prefetch queue is empty at the start, the sequence could take 40 cycles. In short, thanks to instruction fetching, the code won’t run at its documented speed, and could take up to four times longer than it is supposed to.

Why does Intel document Execution Unit execution time rather than overall instruction execution time, which includes both instruction fetch time and Execution Unit (EU) execution time? Well, instruction fetching isn’t performed as part of instruction execution by the Execution Unit, but instead is carried on in parallel by the Bus Interface Unit (BIU) whenever the external data bus isn’t in use or whenever the EU runs out of instruction bytes to execute. Sometimes the BIU is able to use spare bus cycles to prefetch instruction bytes before the EU needs them, so in those cases instruction fetching takes no time at all, practically speaking. At other times the EU executes instructions faster than the BIU can fetch them, and instruction fetching then becomes a significant part of overall execution time. As a result, the effective fetch time for a given instruction varies greatly depending on the code mix preceding that instruction. Similarly, the state in which a given instruction leaves the prefetch queue affects the overall execution time of the following instructions.

-
In other words, while the execution time for a given instruction is constant, the fetch time for that instruction depends heavily on the context in which the instruction is executing—the amount of prefetching the preceding instructions allowed—and can vary from a full 4 cycles per instruction byte to no time at all. +
In other words, while the execution time for a given instruction is constant, the fetch time for that instruction depends heavily on the context in which the instruction is executing—the amount of prefetching the preceding instructions allowed—and can vary from a full 4 cycles per instruction byte to no time at all.

As we’ll see later, other cycle-eaters, such as DRAM refresh and display memory wait states, can cause prefetching variations even during different executions of the same code sequence. Given that, it’s meaningless to talk about the prefetch time of a given instruction except in the context of a specific code sequence.

diff --git a/04-05.html b/04-05.html index a325834..69ea7cb 100644 --- a/04-05.html +++ b/04-05.html @@ -74,7 +74,7 @@

As you can see, it’s easy to be drawn into thinking you’re saving cycles when you’re not. You can only improve the performance of a specific bit of code by reducing the factor—either instruction fetch time or execution time, or sometimes a mix of the two—that’s limiting the performance of that code.

In case you missed it in all the excitement, the variability of prefetching means that our method of testing performance by executing 1,000 instructions in a row by no means produces “true” instruction execution times, any more than the official execution times in the Intel manuals are “true” times. The fact of the matter is that a given instruction takes at least as long to execute as the time given for it in the Intel manuals, but may take as much as 4 cycles per byte longer, depending on the state of the prefetch queue when the preceding instruction ends.

-
The only true execution time for an instruction is a time measured in a certain context, and that time is meaningful only in that context. +
The only true execution time for an instruction is a time measured in a certain context, and that time is meaningful only in that context.

What we really want is to know how long useful working code takes to run, not how long a single instruction takes, and the Zen timer gives us the tool we need to gather that information. Granted, it would be easier if we could just add up neatly documented instruction execution times—but that’s not going to happen. Without actually measuring the performance of a given code sequence, you simply don’t know how fast it is. For crying out loud, even the people who designed the 8088 at Intel couldn’t tell you exactly how quickly a given 8088 code sequence executes on the PC just by looking at it! Get used to the idea that execution times are only meaningful in context, learn the rules of thumb in this book, and use the Zen timer to measure your code.

Approximating Overall Execution Times

diff --git a/04-10.html b/04-10.html index 4895cf5..f70feb8 100644 --- a/04-10.html +++ b/04-10.html @@ -37,7 +37,7 @@


-
A line-drawing subroutine, which executes perhaps a dozen instructions for each display memory access, generally loses less performance to the display adapter cycle-eater than does a block-copy or scrolling subroutine that uses REP MOVS instructions. Scaled and three-dimensional graphics, which spend a great deal of time performing calculations (often using very slow floating-point arithmetic), tend to suffer less. +A line-drawing subroutine, which executes perhaps a dozen instructions for each display memory access, generally loses less performance to the display adapter cycle-eater than does a block-copy or scrolling subroutine that uses REP MOVS instructions. Scaled and three-dimensional graphics, which spend a great deal of time performing calculations (often using very slow floating-point arithmetic), tend to suffer less.

In addition, code that accesses display memory infrequently tends to suffer only about half of the maximum display memory wait states, because on average such code will access display memory halfway between one available display memory access slot and the next. As a result, code that accesses display memory less intensively than the code in Listing 4.11 will on average lose 4 or 5 rather than 8-plus cycles to the display adapter cycle-eater on each memory access.

@@ -45,7 +45,7 @@

What to Do about the Display Adapter Cycle-Eater?

What can we do about the display adapter cycle-eater? Well, we can minimize display memory accesses whenever possible. In particular, we can try to avoid read/modify/write display memory operations of the sort used to mask individual pixels and clip images. Why? Because read/modify/write operations require two display memory accesses (one read and one write) each time display memory is manipulated. Instead, we should try to use writes of the sort that set all the pixels in a given byte of display memory at once, since such writes don’t require accompanying read accesses. The key here is that only half as many display memory accesses are required to write a byte to display memory as are required to read a byte from display memory, mask part of it off and alter the rest, and write the byte back to display memory. Half as many display memory accesses means half as many display memory wait states.

-
Moreover, 486s and Pentiums, as well as recent Super VGAs, employ write-caching schemes that make display memory writes considerably faster than display memory reads. +
Moreover, 486s and Pentiums, as well as recent Super VGAs, employ write-caching schemes that make display memory writes considerably faster than display memory reads.

Along the same line, the display adapter cycle-eater makes the popular exclusive-OR animation technique, which requires paired reads and writes of display memory, less-than-ideal for the PC. Exclusive-OR animation should be avoided in favor of simply writing images to display memory whenever possible.

diff --git a/05-01.html b/05-01.html index b80b8b0..c900899 100644 --- a/05-01.html +++ b/05-01.html @@ -46,7 +46,7 @@

In Chapter 1, I briefly discussed using restartable blocks. This, you might remember, is the process of handling in chunks data sets too large to fit in memory so that they can be processed just about as fast as if they did fit in memory. The restartable block approach is very fast but is relatively difficult to program.

At the opposite end of the spectrum lies byte-by-byte processing, whereby DOS (or, in less extreme cases, a group of library functions) is allowed to do all the hard work, so that you only have to deal with one byte at a time. Byte-by-byte processing is easy to program but can be extremely slow, due to the vast overhead that results from invoking DOS each time a byte must be processed.

Sound familiar? It should. I moved via the byte-by-byte approach, and the overhead of driving back and forth made for miserable performance. Renting a truck (the restartable block approach) would have required more effort and forethought, but would have paid off handsomely.

-
The easy, familiar approach often has nothing in its favor except that it requires less thinking; not a great virtue when writing high-performance code—or when moving. +
The easy, familiar approach often has nothing in its favor except that it requires less thinking; not a great virtue when writing high-performance code—or when moving.

And with that, let’s look at a fairly complex application of restartable blocks.

diff --git a/05-02.html b/05-02.html index 631fd4d..46c328a 100644 --- a/05-02.html +++ b/05-02.html @@ -37,7 +37,7 @@


We could put a zero byte at the end of our buffer to allow strstr() to work, but why bother? The strstr() function must spend time either checking for the end of the string being searched or determining the length of that string—wasted effort given that we already know exactly how long our search buffer is. Even if a given strstr() implementation is well-written, its performance will suffer, at least for our application, from unnecessary overhead.

-
This illustrates why you shouldn’t think of C/C++ library functions as black boxes; understand what they do and try to figure out how they do it, and relate that to their performance in the context you’re interested in. +
This illustrates why you shouldn’t think of C/C++ library functions as black boxes; understand what they do and try to figure out how they do it, and relate that to their performance in the context you’re interested in.

Brute-Force Techniques

Given that no C/C++ library function meets our needs precisely, an obvious alternative approach is the brute-force technique that uses memcmp() to compare every potential matching location in the buffer to the string we’re searching for, as illustrated in Figure 5.1.

diff --git a/05-05.html b/05-05.html index 922e790..d7c2791 100644 --- a/05-05.html +++ b/05-05.html @@ -39,7 +39,7 @@

It might also be worth converting the search engine to assembly for searches performed entirely in memory; with the overhead of file access eliminated, improvements in search-engine performance would translate directly into significantly faster overall performance. One such application that would have much the same structure as Listing 5.1 would be searching through expanded memory buffers, and another would be searching through huge (segment-spanning) buffers.

And so we find, as we so often will, that optimization is definitely not a cut-and-dried matter, and that there is no such thing as a single “best” approach.

-
You must know what your application will typically do, and you must know whether you’re more concerned with average or worst-case performance before you can decide how best to speed up your program—and, indeed, whether speeding it up is worth doing at all. +
You must know what your application will typically do, and you must know whether you’re more concerned with average or worst-case performance before you can decide how best to speed up your program—and, indeed, whether speeding it up is worth doing at all.

By the way, don’t think that just because very large block sizes don’t much improve performance, it wasn’t worth using restartable blocks in Listing 5.1. Listing 5.1 runs more than three times more slowly with a block size of 32 bytes than with a block size of 4K, and any byte-by-byte approach would surely be slower still, due to the overhead of repeated calls to DOS and/or the C stream I/O library.

@@ -48,7 +48,7 @@

I’ve explained two important lessons: Know when it’s worth optimizing further, and use restartable blocks to process large data sets as a series of blocks, with each block handled at high speed. The first lesson is less obvious than it seems.

When I set out to write this chapter, I fully intended to write an assembly language version of Listing 5.1, and I expected the assembly version to be much faster. When I actually looked at where execution time was going (which I did by modifying the program to remove the calls to the read() function, but a code profiler could be used to do the same thing much more easily), I found that the best code in the world wouldn’t make much difference.

-
When you try to speed up code, take a moment to identify the hot spots in your program so that you know where optimization is needed and whether it will make a significant difference before you invest your time. +
When you try to speed up code, take a moment to identify the hot spots in your program so that you know where optimization is needed and whether it will make a significant difference before you invest your time.

As for restartable blocks: Here we tackled a considerably more complex application of restartable blocks than we did in Chapter 1—which turned out not to be so difficult after all. Don’t let irregularities in the programming tasks you tackle, such as strings that span blocks, fluster you into settling for easy, general—and slow—solutions. Focus on making the inner loop—the code that handles each block—as efficient as possible, then structure the rest of your code to support the inner loop.

diff --git a/06-02.html b/06-02.html index ca561d2..596a9fb 100644 --- a/06-02.html +++ b/06-02.html @@ -132,7 +132,7 @@ add ebx,edx

and would in either case require the destruction of the contents of another register.

Multiplying a 32-bit value by a non-power-of-two multiplier in just 2 cycles is a pretty neat trick, even though it works only on a 386 or 486.

-
The full list of values that LEA can multiply a register by on a 386 or 486 is: 2, 3, 4, 5, 8, and 9. That list doesn’t include every multiplier you might want, but it covers some commonly used ones, and the performance is hard to beat. +
The full list of values that LEA can multiply a register by on a 386 or 486 is: 2, 3, 4, 5, 8, and 9. That list doesn’t include every multiplier you might want, but it covers some commonly used ones, and the performance is hard to beat.

I’d like to extend my thanks to Duane Strong of Metagraphics for his help in brainstorming uses for the 386 version of LEA and for pointing out the complications of 486 instruction timings.


diff --git a/07-01.html b/07-01.html index 02d49c8..339507d 100644 --- a/07-01.html +++ b/07-01.html @@ -50,16 +50,16 @@

68 degrees is warm for an uninsulated third floor in Buffalo in the dead of winter. Damn warm. It is not, however, particularly warm for a sauna. Eventually someone acknowledges the obvious and allows that it might have been a stupid idea after all, and everyone agrees, and they shut off the heater and leave, each no doubt offering silent thanks that they had gotten out of this without any incidents requiring major surgery.

And so we see that the best idea in the world can fail for lack of either proper design or adequate horsepower. The primary cause of the Great Buffalo Sauna Fiasco was a lack of horsepower; the gas heater was flat-out undersized. This is analogous to trying to write programs that incorporate features like bitmapped text and searching of multisegment buffers without using high-performance assembly language. Any PC language can perform just about any function you can think of—eventually. That heater would eventually have heated the room to 110 degrees, too—along about the first of June or so.

The Great Buffalo Sauna Fiasco also suffered from fundamental design flaws. A more powerful heater would indeed have made the room hotter—and might well have burned the house down in the process. Likewise, proper algorithm selection and good design are fundamental to performance. The extra horsepower a superb assembly language implementation gives a program is worth bothering with only in the context of a good design.

-
Assembly language optimization is a small but crucial corner of the PC programming world. Use it sparingly and only within the framework of a good design—but ignore it and you may find various portions of your anatomy out in the cold. +
Assembly language optimization is a small but crucial corner of the PC programming world. Use it sparingly and only within the framework of a good design—but ignore it and you may find various portions of your anatomy out in the cold.

So, drawing fortitude from the knowledge that our quest is a pure and worthy one, let’s resume our exploration of assembly language instructions with hidden talents and instructions with well-known talents that are less than they appear to be. In the process, we’ll come to see that there is another, very important optimization level between the algorithm/design level and the cycle-counting/individual instruction level. I’ll call this middle level local optimization; it involves focusing on optimizing sequences of instructions rather than individual instructions, all with an eye to implementing designs as efficiently as possible given the capabilities of the x86 family instruction set.

And yes, in case you’re wondering, the above story is indeed true. Was I there? Let me put it this way: If I were, I’d never admit it!

When LOOP Is a Bad Idea

Let’s examine first an instruction that is less than it appears to be: LOOP. There’s no mystery about what LOOP does; it decrements CX and branches if CX doesn’t decrement to zero. It’s so beautifully suited to the task of counting down loops that any experienced x86 programmer instinctively stuffs the loop count in CX and reaches for LOOP when setting up a loop. That’s fine—LOOP does, of course, work as advertised—but there is one problem:

-
On half of the processors in the x86 family, LOOP is slower than DEC CX followed by JNZ. (Granted, DEC CX/JNZ isn’t precisely equivalent to LOOP, because DEC alters the flags and LOOP doesn’t, but in most situations they’re comparable.) +
On half of the processors in the x86 family, LOOP is slower than DEC CX followed by JNZ. (Granted, DEC CX/JNZ isn’t precisely equivalent to LOOP, because DEC alters the flags and LOOP doesn’t, but in most situations they’re comparable.)

How can this be? Don’t ask me, ask Intel. On the 8088 and 80286, LOOP is indeed faster than DEC CX/JNZ by a cycle, and LOOP is generally a little faster still because it’s a byte shorter and so can be fetched faster. On the 386, however, things change; LOOP is two cycles slower than DEC/JNZ, and the fetch time for one extra byte on even an uncached 386 generally isn’t significant. (Remember that the 386 fetches four instruction bytes at a pop.) LOOP is three cycles slower than DEC/JNZ on the 486, and the 486 executes instructions in so few cycles that those three cycles mean that DEC/JNZ is nearly twice as fast as LOOP. Then, too, unlike LOOP, DEC doesn’t require that CX be used, so the DEC/JNZ solution is both faster and more flexible on the 386 and 486, and on the Pentium as well. (By the way, all this is not just theory; I’ve timed the relative performances of LOOP and DEC CX/JNZ on a cached 386, and LOOP really is slower.)

-
Things are stranger still for LOOP’s relative JCXZ, which branches if and only if CX is zero. JCXZ is faster than AND CX,CX/JZ on the 8088 and 80286, and equivalent on the 80386—but is about twice as slow on the 486! +
Things are stranger still for LOOP’s relative JCXZ, which branches if and only if CX is zero. JCXZ is faster than AND CX,CX/JZ on the 8088 and 80286, and equivalent on the 80386—but is about twice as slow on the 486!


diff --git a/07-02.html b/07-02.html index c3e0fee..c02c393 100644 --- a/07-02.html +++ b/07-02.html @@ -60,7 +60,7 @@ jz SkipLoop ;If field is 0, don’t bother

One level at which assembly language programming pays off handsomely is that of local optimization; that is, selecting the best sequence of instructions for a task. The key to local optimization is viewing the 80x86 instruction set as a set of building blocks, each with unique characteristics. Your job is to sequence those blocks so that they perform well. It doesn’t matter what the instructions are intended to do or what their names are; all that matters is what they do.

Our discussion of LOOP versus DEC/JNZ is an excellent example of optimization by cycle counting. It’s worth knowing, but once you’ve learned it, you just routinely use DEC/JNZ at the bottom of loops in 386/486-specific code, and that’s that. Besides, you’ll save at most a few cycles each time, and while that helps a little, it’s not going to make all that much difference.

Now let’s step back for a moment, and with no preconceptions consider what the x86 instruction set can do for us. The bulk of the time with both LOOP and DEC/JNZ is taken up by branching, which just happens to be one of the slowest aspects of every processor in the x86 family, and the rest is taken up by decrementing the count register and checking whether it’s zero. There may be ways to perform those tasks a little faster by selecting different instructions, but they can get only so fast, and branching can’t even get all that fast.

-
The trick, then, is not to find the fastest way to decrement a count and branch conditionally, but rather to figure out how to accomplish the same result without decrementing or branching as often. Remember the Kobiyashi Maru problem in Star Trek?The same principle applies here: Redefine the problem to one that offers better solutions. +
The trick, then, is not to find the fastest way to decrement a count and branch conditionally, but rather to figure out how to accomplish the same result without decrementing or branching as often. Remember the Kobiyashi Maru problem in Star Trek?The same principle applies here: Redefine the problem to one that offers better solutions.

Consider Listing 7.1, which searches a buffer until either the specified byte is found, a zero byte is found, or the specified number of characters have been checked. Such a function would be useful for scanning up to a maximum number of characters in a zero-terminated buffer. Listing 7.1, which uses LOOP in the main loop, performs a search of the sample string for a period (‘.’) in 170 µs on a 20 MHz cached 386.

When the LOOP in Listing 7.1 is replaced with DEC CX/JNZ, performance improves to 168 µs, less than 2 percent faster than Listing 7.1. Actually, instruction fetching, instruction alignment, cache characteristics, or something similar is affecting these results; I’d expect a slightly larger improvement—around 7 percent—but that’s the most that counting cycles could buy us in this case. (All right, already; LOOPNZ could be used at the bottom of the loop, and other optimizations are surely possible, but all that won’t add up to anywhere near the benefits we’re about to see from local optimization, and that’s the whole point.)


diff --git a/07-04.html b/07-04.html index 33a59fb..f30cfe9 100644 --- a/07-04.html +++ b/07-04.html @@ -170,7 +170,7 @@ SearchMaxLengthendp

How much difference? Listing 7.2 runs in 121 µs—40 percent faster than Listing 7.1, even though Listing 7.2 still uses LOOP rather than DEC CX/JNZ. (The loop in Listing 7.2 could be unrolled further, too; it’s just a question of how much more memory you want to trade for ever-decreasing performance benefits.) That’s typical of local optimization; it won’t often yield the order-of-magnitude improvements that algorithmic improvements can produce, but it can get you a critical 50 percent or 100 percent improvement when you’ve exhausted all other avenues.

-
The point is simply this: You can gain far more by stepping back a bit and thinking of the fastest overall way for the CPU to perform a task than you can by saving a cycle here or there using different instructions. Try to think at the level of sequences of instructions rather than individual instructions, and learn to treat x86 instructions as building blocks with unique characteristics rather than as instructions dedicated to specific tasks. +
The point is simply this: You can gain far more by stepping back a bit and thinking of the fastest overall way for the CPU to perform a task than you can by saving a cycle here or there using different instructions. Try to think at the level of sequences of instructions rather than individual instructions, and learn to treat x86 instructions as building blocks with unique characteristics rather than as instructions dedicated to specific tasks.


diff --git a/07-05.html b/07-05.html index ff45267..b1f61b1 100644 --- a/07-05.html +++ b/07-05.html @@ -79,11 +79,11 @@ BIT_PATTERN=BIT_PATTERN SHL 1 -
Besides illustrating the advantages of local optimization, this example also shows that it generally pays to precalculate results; this is often done at or before assembly time, but precalculated tables can also be built at run time. This is merely one aspect of a fundamental optimization rule: Move as much work as possible out of your critical code by whatever means necessary. +Besides illustrating the advantages of local optimization, this example also shows that it generally pays to precalculate results; this is often done at or before assembly time, but precalculated tables can also be built at run time. This is merely one aspect of a fundamental optimization rule: Move as much work as possible out of your critical code by whatever means necessary.

NOT Flips Bits—Not Flags

The NOT instruction flips all the bits in the operand, from 0 to 1 or from 1 to 0. That’s as simple as could be, but NOT nonetheless has a minor but interesting talent: It doesn’t affect the flags. That can be irritating; I once spent a good hour tracking down a bug caused by my unconscious assumption that NOT does set the flags. After all, every other arithmetic and logical instruction sets the flags; why not NOT? Probably because NOT isn’t considered to be an arithmetic or logical instruction at all; rather, it’s a data manipulation instruction, like MOV and the various rotates. (These are RCR, RCL, ROR, and ROL, which affect only the Carry and Overflow flags.) NOT is often used for tasks, such as flipping masks, where there’s no reason to test the state of the result, and in that context it can be handy to keep the flags unmodified for later testing.

-
Besides, if you want to NOT an operand and set the flags in the process, you can just XOR it with -1. Put another way, the only functional difference between NOT AX and XOR AX,0FFFFH is that XOR modifies the flags and NOT doesn’t. +
Besides, if you want to NOT an operand and set the flags in the process, you can just XOR it with -1. Put another way, the only functional difference between NOT AX and XOR AX,0FFFFH is that XOR modifies the flags and NOT doesn’t.

The x86 instruction set offers many ways to accomplish almost any task. Understanding the subtle distinctions between the instructions—whether and which flags are set, for example—can be critical when you’re trying to optimize a code sequence and you’re running out of registers, or when you’re trying to minimize branching.

@@ -121,7 +121,7 @@ LOOP_TOP:

It’s not that the Listing 7.6 approach is necessarily better or worse; that depends on the processor and the situation. The Listing 7.6 approach is different, and if you understand the differences, you’ll be able to choose the best approach for whatever code you happen to write. (DEC has the same property of preserving the Carry flag, by the way.)

There are a couple of interesting aspects to the last example. First, note that LOOP doesn’t affect any flags at all; this allows the Carry flag to remain unchanged from one addition to the next. Not altering the arithmetic flags is a common characteristic of program control instructions (as opposed to arithmetic and logical instructions like SUB and AND, which do alter the flags).

-
The rule is not that the arithmetic flags change whenever the CPU performs a calculation; rather, the flags change whenever you execute an arithmetic, logical, or flag control (such as CLC to clear the Carry flag) instruction. +
The rule is not that the arithmetic flags change whenever the CPU performs a calculation; rather, the flags change whenever you execute an arithmetic, logical, or flag control (such as CLC to clear the Carry flag) instruction.

Not only do LOOP and JCXZ not alter the flags, but REP MOVS, which counts down CX to 0, doesn’t affect the flags either.

The other interesting point about the last example is the use of LAHF and SAHF, which transfer the low byte of the FLAGS register to and from AH, respectively. These instructions were created to help provide compatibility with the 8080’s (that’s 8080, not 8088) PUSH PSW and POP PSW instructions, but turn out to be compact (one byte) instructions for saving and restoring the arithmetic flags. A word of caution, however: SAHF restores the Carry, Zero, Sign, Auxiliary Carry, and Parity flags—but not the Overflow flag, which resides in the high byte of the FLAGS register. Also, be aware that LAHF and SAHF provide a fast way to preserve the flags on an 8088 but are relatively slow instructions on the 486 and Pentium.

diff --git a/08-01.html b/08-01.html index ac6e17d..6a6f77b 100644 --- a/08-01.html +++ b/08-01.html @@ -44,7 +44,7 @@

“Well,” said Jeff, “I think it suffered in the translation from the French.”

Ah-ha! Mystery solved. Apparently everyone but me knew that it was translated from French, and that novelty undoubtedly made the song a big hit. The translation was also surely responsible for the sappy lyrics; dollars to donuts that the original French lyrics were stronger.

Which brings us without missing a beat to this chapter’s theme, speeding up C with assembly language. When you seek to speed up a C program by converting selected parts of it (generally no more than a few functions) to assembly language, make sure you end up with high-performance assembly language code, not fine-tuned C code. Compilers like Microsoft C/C++ and Watcom C are by now pretty good at fine-tuning C code, and you’re not likely to do much better by taking the compiler’s assembly language output and tweaking it.

-
To make the process of translating C code to assembly language worth the trouble, you must ignore what the compiler does and design your assembly language code from a pure assembly language perspective. With a merely adequate translation, you risk laboring mightily for little or no reward. +
To make the process of translating C code to assembly language worth the trouble, you must ignore what the compiler does and design your assembly language code from a pure assembly language perspective. With a merely adequate translation, you risk laboring mightily for little or no reward.

Apropos of which, when was the last time you heard of Terry Jacks?

@@ -53,7 +53,7 @@

Before I discuss Rule 1 further, let me mention rule number 0: Only optimize where it matters. The bulk of execution time in any program is spent in a very small portion of the code, and most code beyond that small portion doesn’t have any perceptible impact on performance. Unless you’re supremely concerned with code size (an area in which assembly-only programs can excel), I’d suggest that you write most of your code in C and reserve assembly for the truly critical sections of your code; that’s the formula that I find gives the most bang for the buck.

This is not to say that complete programs shouldn’t be designed with optimized assembly language in mind. As you’ll see shortly, orienting your data structures towards assembly language can be a salubrious endeavor indeed, even if most of your code is in C. When it comes to actually optimizing code and/or converting it to assembly, though, do it only where it matters. Get a profiler—and use it!

Also make it a point to concentrate on refining your program design and algorithmic approach at the conceptual and/or C levels before doing any assembly language optimization.

-
Assembly language optimization is the final and far from the only step in the optimization chain, and as such should be performed last; converting to assembly too soon can lock in your code before the design is optimal. At the very least, conversion to assembly tends to make future changes and debugging more difficult, slowing you down and limiting your options. +
Assembly language optimization is the final and far from the only step in the optimization chain, and as such should be performed last; converting to assembly too soon can lock in your code before the design is optimal. At the very least, conversion to assembly tends to make future changes and debugging more difficult, slowing you down and limiting your options.

Don’t Call Your Functions on Me, Baby

In order to think differently from a compiler, you must understand both what compilers and C programmers tend to do and how that differs from what assembly language does well. In this pursuit, it can be useful to examine the code your compiler generates, either by viewing the code in a debugger or by having the compiler generate an assembly language output file. (The latter is done with /Fa or /Fc in Microsoft C/C++ and -S in Borland C++.)

diff --git a/08-02.html b/08-02.html index 523c98b..d1c992d 100644 --- a/08-02.html +++ b/08-02.html @@ -43,14 +43,14 @@

Torn Between Two Segments

C compilers are not terrific at handling segments. Some compilers can efficiently handle a single far pointer used in a loop by leaving ES set for the duration of the loop. But two far pointers used in the same loop confuse every compiler I’ve seen, causing the full segment:offset address to be reloaded each time either pointer is used.

-
This particularly affects performance in 286 protected mode (under OS/2 1.X or the Rational DOS Extender, for example) because segment loads in protected mode take a minimum of 17 cycles, versus a mere 2 cycles in real mode. +
This particularly affects performance in 286 protected mode (under OS/2 1.X or the Rational DOS Extender, for example) because segment loads in protected mode take a minimum of 17 cycles, versus a mere 2 cycles in real mode.

In assembly language you have full control over segments. Use it, and, if necessary, reorganize your code to minimize segment loading.

Why Speeding Up Is Hard to Do

You might think that the most obvious advantage assembly language has over C is that it allows the use of all forms of instructions and all registers in all ways, whereas C compilers tend to use a subset of registers and instructions in a limited number of ways. Yes and no. It’s true that C compilers typically don’t generate instructions such as XLAT, rotates, or the string instructions. On the other hand, XLAT and rotates are useful in a limited set of circumstances, and string instructions are used in the C library functions. In fact, C library code is likely to be carefully optimized by experts, and may be much better than equivalent code you’d produce yourself.

Am I saying that C compilers produce better code than you do? No, I’m saying that they can, unless you use assembly language properly. Writing code in assembly language rather than C guarantees nothing.

-
You can write good assembly, bad assembly, or assembly that is virtually indistinguishable from compiled code; you are more likely than not to write the latter if you think that optimization consists of tweaking compiled C code. +
You can write good assembly, bad assembly, or assembly that is virtually indistinguishable from compiled code; you are more likely than not to write the latter if you think that optimization consists of tweaking compiled C code.

Sure, you can probably use the registers more efficiently and take advantage of an instruction or two that the compiler missed, but the code isn’t going to get a whole lot faster that way.

@@ -72,7 +72,7 @@ jz Match

Taking It to the Limit

The ultimate in assembly language optimization comes when you change the rules; that is, when you reorganize the entire program to allow the use of better assembly language code in the small section of code that most affects overall performance. For example, consider that the data searched in the last example is stored in an array of structures, with each structure in the array containing other information as well. In this situation, REP SCASW couldn’t be used because the data searched through wouldn’t be contiguous.

However, if the need for performance in searching the array is urgent enough, there’s no reason why you can’t reorganize the data. This might mean removing the array elements from the structures and storing them in their own array so that REP SCASW could be used.

-
Organizing a program’s data so that the performance of the critical sections can be optimized is a key part of design, and one that’s easily shortchanged unless, during the design stage, you thoroughly understand and work to bring together your data needs, the critical sections of your program, and potential assembly language optimizations. +
Organizing a program’s data so that the performance of the critical sections can be optimized is a key part of design, and one that’s easily shortchanged unless, during the design stage, you thoroughly understand and work to bring together your data needs, the critical sections of your program, and potential assembly language optimizations.

More on this shortly.

diff --git a/09-02.html b/09-02.html index 2bf87d7..df52f2c 100644 --- a/09-02.html +++ b/09-02.html @@ -78,13 +78,13 @@ ADD AX,BX ;*80

Speeding Up Multiplication

That brings us to multiplication, one of the slowest of x86 operations and one that allows for considerable optimization. One way to speed up multiplication is to use shift and add, LEA, or a lookup table to hard-code a multiplication operation for a fixed multiplier, as shown above. Another is to take advantage of the early-out feature of the 386 (and the 486, but in the interests of brevity I’ll just say “386” from now on) by arranging your operands so that the multiplier (always the rightmost operand following MUL or IMUL) is no larger than the other operand.

-
Why? Because the 386 processes one multiplier bit per cycle and immediately ends a multiplication when all significant bits of the multiplier have been processed, so fewer cycles are required to multiply a large multiplicand times a small multiplier than a small multiplicand times a large multiplier, by a factor of about 1 cycle for each significant multiplier bit eliminated. +
Why? Because the 386 processes one multiplier bit per cycle and immediately ends a multiplication when all significant bits of the multiplier have been processed, so fewer cycles are required to multiply a large multiplicand times a small multiplier than a small multiplicand times a large multiplier, by a factor of about 1 cycle for each significant multiplier bit eliminated.

(There’s a minimum execution time on this trick; below 3 significant multiplier bits, no additional cycles are saved.) For example, multiplication of 32,767 times 1 is 12 cycles faster than multiplication of 1 times 32,727.

Choosing the right operand as the multiplier can work wonders. According to published specs, the 386 takes 38 cycles to multiply by a multiplier with 32 significant bits but only 9 cycles to multiply by a multiplier of 2, a performance improvement of more than four times! (My tests regularly indicate that multiplication takes 3 to 4 cycles longer than the specs indicate, but the cycle-per-bit advantage of smaller multipliers holds true nonetheless.)

This highlights another interesting point: MUL and IMUL on the 386 are so fast that alternative multiplication approaches, while generally still faster, are worthwhile only in truly time-critical code.

-
On 386SXs and uncached 386s, where code size can significantly affect performance due to instruction prefetching, the compact MUL and IMUL instructions can approach and in some cases even outperform the “optimized” alternatives. +
On 386SXs and uncached 386s, where code size can significantly affect performance due to instruction prefetching, the compact MUL and IMUL instructions can approach and in some cases even outperform the “optimized” alternatives.

All in all, MUL and IMUL are reasonable performers on the 386, no longer to be avoided in most cases—and you can help that along by arranging your code to make the smaller operand the multiplier whenever you know which operand is smaller.

That doesn’t mean that your code should test and swap operands to make sure the smaller one is the multiplier; that rarely pays off. I’m speaking more of the case where you’re scaling an array up by a value that’s always in the range of, say, 2 to 10; because the scale value will always be small and the array elements may have any value, the scale value is the logical choice for the multiplier.

@@ -94,7 +94,7 @@ ADD AX,BX ;*80
-->Figure 9.1  Simple searching method for locating a text string.

Rob’s revelation, which he credits without explanation to Edgar Allen Poe (search nevermore?), was that by far the slowest part of the whole deal is handling REPNZ SCASB matches, which require checking the remainder of the string with REPZ CMPS and restarting REPNZ SCASB if no match is found.

-
Rob points out that the number of REPNZ SCASB matches can easily be reduced simply by scanning for the character in the searched-for string that appears least often in the buffer being searched. +
Rob points out that the number of REPNZ SCASB matches can easily be reduced simply by scanning for the character in the searched-for string that appears least often in the buffer being searched.

Imagine, if you will, that you’re searching for the string “EQUAL.” By my approach, you’d use REPNZ SCASB to scan for each occurrence of “E,” which crops up quite often in normal text. Rob points out that it would make more sense to scan for “Q,” then back up one character and check the whole string when a “Q” is found, as shown in Figure 9.2. “Q” is likely to occur much less often, resulting in many fewer whole-string checks and much faster processing.


diff --git a/09-03.html b/09-03.html index 7573fc6..1d53727 100644 --- a/09-03.html +++ b/09-03.html @@ -38,7 +38,7 @@


Listing 9.1 implements the scan-on-first-character approach. Listing 9.2 scans for whatever character the caller specifies. Listing 9.3 is a test program used to compare the two approaches. How much difference does Rob’s revelation make? Plenty. Even when the entire C function call to FindString is timed—strlen calls, parameter pushing, calling, setup, and all—the version of FindString in Listing 9.2, which is directed by Listing 9.3 to scan for the infrequently-occurring “Q,” is about 40 percent faster on a 20 MHz cached 386 for the test search of Listing 9.3 than is the version of FindString in Listing 9.1, which always scans for the first character, in this case “E.” However, when only the search loops (the code that actually does the searching) in the two versions of FindString are compared, Listing 9.2 is more than twice as fast as Listing 9.1—a remarkable improvement over code that already uses REPNZ SCASB and REPZ CMPS.

What I like so much about Rob’s approach is that it demonstrates that optimization involves much more than instruction selection and cycle counting. Listings 9.1 and 9.2 use pretty much the same instructions, and even use the same approach of scanning with REPNZ SCASB and using REPZ CMPS to check scanning matches.

-
The difference between Listings 9.1 and 9.2 (which gives you more than a doubling of performance) is due entirely to understanding the nature of the data being handled, and biasing the code to reflect that knowledge. +
The difference between Listings 9.1 and 9.2 (which gives you more than a doubling of performance) is due entirely to understanding the nature of the data being handled, and biasing the code to reflect that knowledge.


Figure 9.2
  Faster searching method for locating a text string. diff --git a/09-06.html b/09-06.html index ef93281..6ce5f58 100644 --- a/09-06.html +++ b/09-06.html @@ -132,7 +132,7 @@ main() {

Way back in Volume 1, Number 1 of PC TECHNIQUES, (April/May 1990) I wrote the very first of that magazine’s HAX (#1), which extolled the virtues of placing your most commonly-used automatic (stack-based) variables within the stack’s “sweet spot,” the area between +127 to -128 bytes away from BP, the stack frame pointer. The reason was that the 8088 can store addressing displacements that fall within that range in a single byte; larger displacements require a full word of storage, increasing code size by a byte per instruction, and thereby slowing down performance due to increased instruction fetching time.

This takes on new prominence in 386 native mode, where straying from the sweet spot costs not one, but two or three bytes. Where the 8088 had two possible displacement sizes, either byte or word, on the 386 there are three possible sizes: byte, word, or dword. In native mode (32-bit protected mode), however, a prefix byte is needed in order to use a word-sized displacement, so a variable located outside the sweet spot requires either two extra bytes (an extra displacement byte plus a prefix byte) or three extra bytes (a dword displacement rather than a byte displacement). Either way, instructions grow alarmingly.

Performance may or may not suffer from missing the sweet spot, depending on the processor, the memory architecture, and the code mix. On a 486, prefix bytes often cost a cycle; on a 386SX, increased code size often slows performance because instructions must be fetched through the half-pint 16-bit bus; on a 386, the effect depends on the instruction mix and whether there’s a cache.

-
On balance, though, it’s as important to keep your most-used variables in the stack’s sweet spot in 386 native mode as it was on the 8088. +
On balance, though, it’s as important to keep your most-used variables in the stack’s sweet spot in 386 native mode as it was on the 8088.

In assembly, it’s easy to control the organization of your stack frame. In C, however, you’ll have to figure out the allocation scheme your compiler uses to allocate automatic variables, and declare automatics appropriately to produce the desired effect. It can be done: I did it in Turbo C some years back, and trimmed the size of a program (admittedly, a large one) by several K—not bad, when you consider that the “sweet spot” optimization is essentially free, with no code reorganization, change in logic, or heavy thinking involved.


diff --git a/10-01.html b/10-01.html index 7fb4e21..5d0ff55 100644 --- a/10-01.html +++ b/10-01.html @@ -46,7 +46,7 @@

It goes without saying that pattern matching is good; more than that, it’s a large part of what we are, and, generally, the faster we are at it, the better. Not always, though. Sometimes insufficient information really is insufficient, and, in our haste to get the heady rush of coming up with a solution, incorrect or less-than-optimal conclusions are reached, as anyone who has ever done the Times Sunday crossword will attest. Still, my grandfather does that puzzle every Sunday in ink. What’s his secret? Patience and discipline. He never fills a word in until he’s confirmed it in his head via intersecting words, no matter how strong the urge may be to put something down where he can see it and feel like he’s getting somewhere.

There’s a surprisingly close parallel to programming here. Programming is certainly a sort of pattern matching in the sense I’ve described above, and, as with crossword puzzles, following your programming instincts too quickly can be a liability. For many programmers, myself included, there’s a strong urge to find a workable approach to a particular problem and start coding it right now, what some people call “hacking” a program. Going with the first thing your programming pattern matcher comes up with can be a lot of fun; there’s instant gratification and a feeling of unbounded creativity. Personally, I’ve always hungered to get results from my work as soon as possible; I gravitated toward graphics for its instant and very visible gratification. Over time, however, I’ve learned patience.

-
I’ve come to spend an increasingly large portion of my time choosing algorithms, designing, and simply giving my mind quiet time in which to work on problems and come up with non-obvious approaches before coding; and I’ve found that the extra time up front more than pays for itself in both decreased coding time and superior programs. +I’ve come to spend an increasingly large portion of my time choosing algorithms, designing, and simply giving my mind quiet time in which to work on problems and come up with non-obvious approaches before coding; and I’ve found that the extra time up front more than pays for itself in both decreased coding time and superior programs.

In this chapter, I’m going to walk you through a simple but illustrative case history that nicely points up the wisdom of delaying gratification when faced with programming problems, so that your mind has time to chew on the problems from other angles. The alternative solutions you find by doing this may seem obvious, once you’ve come up with them. They may not even differ greatly from your initial solutions. Often, however, they will be much better—and you’ll never even have the chance to decide whether they’re better or not if you take the first thing that comes into your head and run with it.

diff --git a/10-03.html b/10-03.html index a074fd6..366b340 100644 --- a/10-03.html +++ b/10-03.html @@ -91,7 +91,7 @@ static unsigned int gcd_recurs(unsigned int larger_int,

As you can see from Table 10.1, Euclid’s algorithm is superior, especially for large numbers (and imagine if we were working with large longs!).

-
Had I been implementing GCD determination without Sedgewick’s help, I would surely not have settled for Listing 10.1—but I might well have ended up with Listing 10.2 in my enthusiasm over the “brilliant” discovery of subtracting the lesser Using Euclid’s algorithm to find a GCD number from the greater. In a commercial product, my lack of patience and discipline could have been costly indeed. +
Had I been implementing GCD determination without Sedgewick’s help, I would surely not have settled for Listing 10.1—but I might well have ended up with Listing 10.2 in my enthusiasm over the “brilliant” discovery of subtracting the lesser Using Euclid’s algorithm to find a GCD number from the greater. In a commercial product, my lack of patience and discipline could have been costly indeed.


Figure 10.3
  Using Euclid’s algorithm to find a GCD. diff --git a/10-04.html b/10-04.html index 51afa7e..e89b07b 100644 --- a/10-04.html +++ b/10-04.html @@ -129,7 +129,7 @@ _gcd endp

Assembly language optimization is pattern matching on a local scale. Frankly, it’s also the sort of boring, brute-force work that people are lousy at; compilers could out-optimize you at this level with one pass tied behind their back if they knew as much about the code you’re writing as you do, which they don’t.

-
Design optimization—conceptual breakthroughs in understanding the relationships between the needs of an application, the nature of the data the application works with, and what the computer can do—is global pattern matching. +
Design optimization—conceptual breakthroughs in understanding the relationships between the needs of an application, the nature of the data the application works with, and what the computer can do—is global pattern matching.

Computers are much worse at that sort of pattern matching than humans; computers have no way to integrate vast amounts of disparate information, much of it only vaguely defined or subject to change. People, oddly enough, are better at global optimization than at local optimization. For one thing, it’s more interesting. For another, it’s complex and imprecise enough to allow intuition and inspiration, two vastly underrated programming tools, to come to the fore. And, as I pointed out earlier, people tend to perform instantaneous solutions to even the most complex problems, while computers bog down in geometrically or exponentially increasing execution times. Oh, it may take days or weeks for a person to absorb enough information to be able to reach a solution, and the solution may only be near-optimal—but the solution itself (or, at least, each of the pieces of the solution) arrives in a flash.

Those flashes are your programming pattern matcher doing its job. Your job is to give your pattern matcher the opportunity to get to know each problem and run through it two or three times, from different angles, to see what unexpected solutions it can come up with.

diff --git a/11-02.html b/11-02.html index 08d0356..ef6966d 100644 --- a/11-02.html +++ b/11-02.html @@ -45,7 +45,7 @@

System Wait States

The 286 and 386 were designed to lose relatively little performance to the prefetch queue cycle-eater...when used with zero-wait-state memory: memory that can complete memory accesses so rapidly that no wait states are needed. However, true zero-wait-state memory is almost never used with those processors. Why? Because memory that can keep up with a 286 is fairly expensive, and memory that can keep up with a 386 is very expensive. Instead, computer designers use alternative memory architectures that offer more performance for the dollar—but less performance overall—than zero-wait-state memory. (It is possible to build zero-wait-state systems for the 286 and 386; it’s just so expensive that it’s rarely done.)

The IBM AT and true compatibles use one-wait-state memory (some AT clones use zero-wait-state memory, but such clones are less common than one-wait-state AT clones). The 386 systems use a wide variety of memory systems—including high-speed caches, interleaved memory, and static-column RAM—that insert anywhere from 0 to about 5 wait states (and many more if 8 or 16-bit memory expansion cards are used); the exact number of wait states inserted at any given time depends on the interaction between the code being executed and the memory system it’s running on.

-
The performance of most 386 memory systems can vary greatly from one memory access to another, depending on factors such as what data happens to be in the cache and which interleaved bank and/or RAM column was accessed last. +
The performance of most 386 memory systems can vary greatly from one memory access to another, depending on factors such as what data happens to be in the cache and which interleaved bank and/or RAM column was accessed last.

The many memory systems in use make it impossible for us to optimize for 286/386 computers with the precision that’s possible on the 8088. Instead, we must write code that runs reasonably well under the varying conditions found in the 286/386 arena.

diff --git a/11-03.html b/11-03.html index 731feca..7fbd55c 100644 --- a/11-03.html +++ b/11-03.html @@ -60,7 +60,7 @@

Figure 11.1 illustrates this phenomenon. The conversion of word-sized accesses to odd addresses into double byte-sized accesses is transparent to memory-accessing instructions; all any instruction knows is that the requested word has been accessed, no matter whether 1 word-sized access or 2 byte-sized accesses were required to accomplish it.

The penalty for performing a word-sized access starting at an odd address is easy to calculate: Two accesses take twice as long as one access.

-
In other words, the effective capacity of the 286’s external data bus is halved when a word-sized access to an odd address is performed. +
In other words, the effective capacity of the 286’s external data bus is halved when a word-sized access to an odd address is performed.

That, in a nutshell, is the data alignment cycle-eater, the one new cycle-eater of the 286 and 386. (The data alignment cycle-eater is a close relative of the 8088’s 8-bit bus cycle-eater, but since it behaves differently—occurring only at odd addresses—and is avoided with a different workaround, we’ll consider it to be a new cycle-eater.)

diff --git a/11-04.html b/11-04.html index 63589aa..afe43ec 100644 --- a/11-04.html +++ b/11-04.html @@ -67,7 +67,7 @@ LoopTop:

While word-aligning branch destinations can improve branching performance, it’s a nuisance and can increase code size a good deal, so it’s not worth doing in most code. Besides, EVEN inserts a NOP instruction if necessary, and the time required to execute a NOP can sometimes cancel the performance advantage of having a word-aligned branch destination.

-
Consequently, it’s best to word-align only those branch destinations that can be reached solely by branching. +
Consequently, it’s best to word-align only those branch destinations that can be reached solely by branching.

I recommend that you only go out of your way to word-align the start offsets of your subroutines, as in:

@@ -88,7 +88,7 @@ FindChar proc near

Alignment and the Stack

One side-effect of the data alignment cycle-eater of the 286 and 386 is that you should never allow the stack pointer to become odd. (You can make the stack pointer odd by adding an odd value to it or subtracting an odd value from it, or by loading it with an odd value.) An odd stack pointer on the 286 or 386 (or a non-doubleword-aligned stack in 32-bit protected mode on the 386, 486, or Pentium) will significantly reduce the performance of PUSH, POP, CALL, and RET, as well as INT and IRET, which are executed to invoke DOS and BIOS functions, handle keystrokes and incoming serial characters, and manage the mouse. I know of a Forth programmer who vastly improved the performance of a complex application on the AT simply by forcing the Forth interpreter to maintain an even stack pointer at all times.

An interesting corollary to this rule is that you shouldn’t INC SP twice to add 2, even though that takes fewer bytes than ADD SP,2. The stack pointer is odd between the first and second INC, so any interrupt occurring between the two instructions will be serviced more slowly than it normally would. The same goes for decrementing twice; use SUB SP,2 instead.

-
Keep the stack pointer aligned at all times. +
Keep the stack pointer aligned at all times.

The DRAM Refresh Cycle-Eater: Still an Act of God

The DRAM refresh cycle-eater is the cycle-eater that’s least changed from its 8088 form on the 286 and 386. In the AT, DRAM refresh uses a little over five percent of all available memory accesses, slightly less than it uses in the PC, but in the same ballpark. While the DRAM refresh penalty varies somewhat on various AT clones and 386 computers (in fact, a few computers are built around static RAM, which requires no refresh at all; likewise, caches are made of static RAM so cached systems generally suffer less from DRAM refresh), the 5 percent figure is a good rule of thumb. diff --git a/11-05.html b/11-05.html index 1adca75..48bda24 100644 --- a/11-05.html +++ b/11-05.html @@ -42,7 +42,7 @@

In other words, instructions that access display memory won’t run a whole lot faster on ATs and faster computers than they do on PCs. That explains one of the two viewpoints expressed at the beginning of this section: The display adapter cycle-eater is just about the same on high-end computers as it is on the PC, in the sense that it allows instructions that access display memory to run at just about the same speed on all computers.

Of course, the picture is quite a bit different when you compare the performance of instructions that access display memory to the maximum performance of those instructions. Instructions that access display memory receive many more wait states when running on a 286 than they do on an 8088. Why? While the 286 is capable of accessing memory much more often than the 8088, we’ve seen that the frequency of access to display memory is determined not by processor speed but by the display adapter itself. As a result, both processors are actually allowed just about the same maximum number of accesses to display memory in any given time. By definition, then, the 286 must spend many more cycles waiting than does the 8088.

And that explains the second viewpoint expressed above regarding the display adapter cycle-eater vis-a-vis the 286 and 386. The display adapter cycle-eater, as measured in cycles lost to wait states, is indeed much worse on AT-class computers than it is on the PC, and it’s worse still on more powerful computers.

-
How bad is the display adapter cycle-eater on an AT? It’s this bad: Based on my (not inconsiderable) experience in timing display adapter access, I’ve found that the display adapter cycle-eater can slow an AT—or even a 386 computer—to near-PC speeds when display memory is accessed. +
How bad is the display adapter cycle-eater on an AT? It’s this bad: Based on my (not inconsiderable) experience in timing display adapter access, I’ve found that the display adapter cycle-eater can slow an AT—or even a 386 computer—to near-PC speeds when display memory is accessed.

I know that’s hard to believe, but the display adapter cycle-eater gives out just so many display memory accesses in a given time, and no more, no matter how fast the processor is. In fact, the faster the processor, the more the display adapter cycle-eater hurts the performance of instructions that access display memory. The display adapter cycle-eater is not only still present in 286/386 computers, it’s worse than ever.

diff --git a/11-06.html b/11-06.html index 5ea8201..2395c0a 100644 --- a/11-06.html +++ b/11-06.html @@ -37,7 +37,7 @@


Finally, it’s possible in real mode to use the 386’s new addressing modes, in which any 32-bit general-purpose register or pair of registers can be used to address memory. What’s more, multiplication of memory-addressing registers by 2, 4, or 8 for look-ups in word, doubleword, or quadword tables can be built right into the memory addressing mode. (The 32-bit addressing modes are discussed further in later chapters.) In protected mode, these new addressing modes allow you to address a full 4 gigabytes per segment, but in real mode you’re still limited to 64K, even with 32-bit registers and the new addressing modes, unless you play some unorthodox tricks with the segment registers.

-
Note well: Those tricks don’t necessarily work with system software such as Windows, so I’d recommend against using them. If you want 4-gigabyte segments, use a 32-bit environment such as Win32. +
Note well: Those tricks don’t necessarily work with system software such as Windows, so I’d recommend against using them. If you want 4-gigabyte segments, use a 32-bit environment such as Win32.

Optimization Rules: The More Things Change...

Let’s see what we’ve learned about 286/386 optimization. Mostly what we’ve learned is that our familiar PC cycle-eaters still apply, although in somewhat different forms, and that the major optimization rules for the PC hold true on ATs and 386-based computers. You won’t go wrong on any of these computers if you keep your instructions short, use the registers heavily and avoid memory, don’t branch, and avoid accessing display memory like the plague. diff --git a/12-01.html b/12-01.html index a760451..0818577 100644 --- a/12-01.html +++ b/12-01.html @@ -50,7 +50,7 @@

An encyclopedic approach to 486 optimization would take a book all by itself, so in this chapter I’m only going to hit the highlights of 486 optimization, touching on several optimization rules, some documented, some not. You might also want to check out the following sources of 486 information: i486 Microprocessor Programmer’s Reference Manual, from Intel; “8086 Optimization: Aim Down the Middle and Pray,” in the March, 1991 Dr. Dobb’s Journal; and “Peak Performance: On to the 486,” in the November, 1990 Programmer’s Journal.

Rules to Optimize By

In Appendix G of the i486 Microprocessor Programmer’s Reference Manual, Intel lists a number of optimization techniques for the 486. While neither exhaustive (we’ll look at two undocumented optimizations shortly) nor entirely accurate (we’ll correct two of the rules here), Intel’s list is certainly a good starting point. In particular, the list conveys the extent to which 486 optimization differs from optimization for earlier x86 processors. Generally, I’ll be discussing optimization for real mode (it being the most widely used mode at the moment), although many of the rules should apply to protected mode as well.

-
486 optimization is generally more precise and less frustrating than optimization for other x86 processors because every 486 has an identical internal cache. Whenever both the instructions being executed and the data the instructions access are in the cache, those instructions will run in a consistent and calculatable number of cycles on all 486s, with little chance of interference from the prefetch queue and without regard to the speed of external memory. +
486 optimization is generally more precise and less frustrating than optimization for other x86 processors because every 486 has an identical internal cache. Whenever both the instructions being executed and the data the instructions access are in the cache, those instructions will run in a consistent and calculatable number of cycles on all 486s, with little chance of interference from the prefetch queue and without regard to the speed of external memory.

In other words, for cached code (which time-critical code almost always is), performance is predictable and can be calculated with good precision, and those calculations will apply on any 486. However, “predictable” doesn’t mean “trivial”; the cycle times printed for the various instructions are not the whole story. You must be aware of all the rules, documented and undocumented, that go into calculating actual execution times—and uncovering some of those rules is exactly what this chapter is about.

@@ -59,7 +59,7 @@

Intel cautions against using indexing to address memory because there’s a one-cycle penalty for indexed addressing. True enough—but “indexed addressing” might not mean what you expect.

Traditionally, SI and DI are considered the index registers of the x86 CPUs. That is not the sense in which “indexed addressing” is meant here, however. In real mode, indexed addressing means that two registers, rather than one or none, are used to point to memory. (In this context, the use of one register to address memory is “base addressing,” no matter what register is used.) MOV AX, [BX+DI] and MOV CL, [BP+SI+10] perform indexed addressing; MOV AX,[BX] and MOV DL, [SI+1] do not.

-
Therefore, in real mode, the rule is to avoid using two registers to point to memory whenever possible. Often, this simply means adding the two registers together outside a loop before memory is actually addressed. +
Therefore, in real mode, the rule is to avoid using two registers to point to memory whenever possible. Often, this simply means adding the two registers together outside a loop before memory is actually addressed.

As an example, you might adhere to this rule by replacing the code

diff --git a/12-02.html b/12-02.html index 0f577b0..e4e204e 100644 --- a/12-02.html +++ b/12-02.html @@ -76,7 +76,7 @@ LoopTop:

Now that we understand what Intel means by this rule, let me make a very important comment: My observations indicate that for real-mode code, the documentation understates the extent of the penalty for interrupting the address calculation pipeline by loading a memory pointer just before it’s used.

-
The truth of the matter appears to be that if a register is the destination of one instruction and is then used by the next instruction to address memory in real mode, not one but two cycles are lost! +
The truth of the matter appears to be that if a register is the destination of one instruction and is then used by the next instruction to address memory in real mode, not one but two cycles are lost!

In 32-bit protected mode, however, the penalty is, in fact, the 1 cycle that Intel .

diff --git a/12-03.html b/12-03.html index ffca8db..fc15920 100644 --- a/12-03.html +++ b/12-03.html @@ -118,7 +118,7 @@ xlat

even though AL must be converted to a word by XLAT before it can be added to BX and used to address memory. In fact, none of the penalties mentioned in this chapter apply to XLAT, apparently because XLAT is so slow—4 cycles—that it gives the 486 time to perform addressing calculations during the course of the instruction.

-
While it’s nice that XLAT doesn’t suffer from the various 486 addressing penalties, the reason for that is basically that XLAT is slow, so there’s still no compelling reason to use XLAT on the 486. +
While it’s nice that XLAT doesn’t suffer from the various 486 addressing penalties, the reason for that is basically that XLAT is slow, so there’s still no compelling reason to use XLAT on the 486.

In general, penalties for interrupting the 486’s pipeline apply primarily to the fast core instructions of the 486, most notably register-only instructions and MOV, although arithmetic and logical operations that access memory are also often affected. I don’t know all the performance dependencies, and I don’t plan to; figuring all of them out would be a big, boring job of little value. Basically, on the 486 you should concentrate on using those fast core instructions when performance matters, and all the rules I’ll discuss do indeed apply to those instructions.


diff --git a/12-04.html b/12-04.html index f504c8f..d9d499c 100644 --- a/12-04.html +++ b/12-04.html @@ -76,7 +76,7 @@ mov ax,[bx]

A more sophisticated programmer would expect to lose one cycle, because BX is loaded two cycles before being used to address memory. In fact, though, this code takes 5 cycles—2 cycles, or 67 percent, longer than normal. Why? Well, under normal conditions, loading a byte register—CL in this case—one cycle before using a register to address memory produces no penalty; loading 2 cycles ahead is the only case that normally incurs a penalty. However, think of Rule #4 as meaning that loading a byte register disrupts the memory addressing pipeline as it starts up. Viewed that way, we can see that MOV BX,OFFSET MemVar interrupts the addressing pipeline, forcing it to start again, and then, presumably, MOV CL,AL interrupts the pipeline again because the pipeline is now on its first cycle: the one that loading a byte register can affect.

-
I know—it seems awfully complicated. It isn’t, really. Generally, try not to use byte destinations exactly two cycles before using a register to address memory, and try not to load a register either one or two cycles before using it to address memory, and you’ll be fine. +
I know—it seems awfully complicated. It isn’t, really. Generally, try not to use byte destinations exactly two cycles before using a register to address memory, and try not to load a register either one or two cycles before using it to address memory, and you’ll be fine.

Timing Your Own 486 Code

In case you want to do some 486 performance analysis of your own, let me show you how I arrived at one of the above conclusions; at the same time, I can warn you of the timing hazards of the cache. Listings 12.1 and 12.2 show the code I ran through the Zen timer in order to establish the effects of loading a byte register before using a register to address memory. Listing 12.1 ran in 120 µs on a 33 MHz 486, or 4 cycles per repetition (120 µs/1000 repetitions = 120 ns per repetition; 120 ns per repetition/30 ns per cycle = 4 cycles per repetition); Listing 12.2 ran in 90 µs, or 3 cycles, establishing that loading a byte register costs a cycle only when it’s performed exactly 2 cycles before addressing memory. @@ -126,7 +126,7 @@ Done:

Note that Listings 12.1 and 12.2 each repeat the timing of the code under test a second time, to make sure that the instructions are in the cache on the second pass, the one for which results are displayed. Also note that the code is less than 8K in size, so that it can all fit in the 486’s 8K internal cache. If I double the REPT value in Listing 12.2 to 2,000, making the test code larger than 8K, the execution time more than doubles to 224 µs, or 3.7 cycles per repetition; the extra seven-tenths of a cycle comes from fetching non-cached instruction bytes.

-
Whenever you see non-integral timing results of this sort, it’s a good bet that the test code or data isn’t cached. +
Whenever you see non-integral timing results of this sort, it’s a good bet that the test code or data isn’t cached.

The Story Continues

There’s certainly plenty more 486 lore to explore, including the 486’s unique prefetch queue, more optimization rules, branching optimizations, performance implications of the cache, the cost of cache misses for reads, and the implications of cache write-through for writes. Nonetheless, we’ve covered quite a bit of ground in this chapter, and I trust you’ve gotten a feel for the considerable extent to which 486 optimization differs from what you’re used to. Odd as 486 optimization is, though, it’s well worth mastering, for the 486 is, at its best, so staggeringly fast that carefully crafted 486 code can do more than twice as much per cycle as the best 386 code—which makes it perhaps 50 times as fast as optimized code for the original PC. diff --git a/13-01.html b/13-01.html index 68b08d8..f7fdea8 100644 --- a/13-01.html +++ b/13-01.html @@ -61,7 +61,7 @@ add dx,[bx+8000h] ;increment word and line count
-->Figure 13.1  Cycle-eaters in the original WC.

-
Remember, pipeline penalties diminish with increasing number of cycles, not instructions, between the pipeline disrupter and the potentially affected instruction. +Remember, pipeline penalties diminish with increasing number of cycles, not instructions, between the pipeline disrupter and the potentially affected instruction.

LISTING 13.2 L13-2.ASM

diff --git a/13-03.html b/13-03.html index 2d1cfbf..dee74e6 100644 --- a/13-03.html +++ b/13-03.html @@ -72,7 +72,7 @@ push word ptr [bx]

Likewise, popping a memory location takes six cycles, but popping a register and writing it to memory takes only two cycles combined. The i486 Microprocessor Programmer’s Reference Manual lists a 4-cycle execution time for popping a register, but pay that no mind; popping a register takes only 1 cycle.

Why is it that such a convenient operation as pushing or popping memory is so slow? The rule on the 486 is that simple operations, which can be executed in a single cycle by the 486’s RISC core, are fast; whereas complex operations, which must be carried out in microcode just as they were on the 386, are almost all relatively slow. Slow, complex operations include all the string instructions except REP MOVS, as well as XLAT, LOOP, and, of course, PUSH mem and POP mem.

-
Whenever possible, try to use the 486’s 1-cycle instructions, including MOV, ADD, SUB, CMP, ADC, SBB, XOR, AND, OR, TEST, LEA, and PUSH reg and POP reg. These instructions have an added benefit in that it’s often possible to rearrange them for maximum pipeline efficiency, as is the case with Terje’s optimization described earlier in this chapter. +
Whenever possible, try to use the 486’s 1-cycle instructions, including MOV, ADD, SUB, CMP, ADC, SBB, XOR, AND, OR, TEST, LEA, and PUSH reg and POP reg. These instructions have an added benefit in that it’s often possible to rearrange them for maximum pipeline efficiency, as is the case with Terje’s optimization described earlier in this chapter.

Optimal 1-Bit Shifts and Rotates

On a 486, the n-bit forms of the shift and rotate instructions—as in ROR AX,2 and SHL BX,9—are 2-cycle instructions, but the 1-bit forms—as in ROR AX,1 and SHL BX,1—are 3-cycle instructions. Go figure.

diff --git a/13-04.html b/13-04.html index 124ee83..e2b23f3 100644 --- a/13-04.html +++ b/13-04.html @@ -48,7 +48,7 @@ mov al,BaseTable[ecx+edx*4]

Any register may serve as the base register component of an address. Any register except ESP may also serve as the index register, which can be scaled by 1, 2, 4, or 8. (Scaling is very handy for performing lookups in arrays and tables.) The same register may serve as both base and index register, except for ESP, which can only be the base. Incidentally, it makes sense that ESP can’t be scaled; ESP presumably always points to a valid stack, and I can’t think of any reason you’d want to use the stack pointer times 2, 4, or 8 in an address. ESP is, by its nature, a base rather than index pointer.

That’s all there is to the functionality of 32-bit addressing; it’s very simple, much simpler than 16-bit addressing, with its sharply limited memory addressing register combinations. The costs of 32-bit addressing are a bit more subtle. The only performance cost (apart from the aforementioned 1-cycle penalty for using 32-bit addressing in real mode) is a 1-cycle penalty imposed for using an index register. In this context, you use an index register when you use a register that’s scaled, or when you use the sum of two registers to point to memory. MOV BL,[EBX*2] uses an index register and takes an extra cycle, as does MOV CL,[EAX+EDX]; MOV CL,[EAX+100H] is not indexed, however.

The other cost of 32-bit addressing is in instruction size. Old-style 16-bit addressing usually (except in a few special cases) uses one extra byte, which Intel calls the Mod-R/M byte, which is placed immediately after each instruction’s opcode to describe the memory addressing mode, plus 1 or 2 optional bytes of addressing displacement—that is, a constant value to add into the address. In many cases, 32-bit addressing continues to use the Mod-R/M byte, albeit with a different interpretation; in these cases, 32-bit addressing is no larger than 16-bit addressing, except when a 32-bit displacement is involved. For example, MOV AL, [EBX] is a 2-byte instruction; MOV AL, [EBX+10H] is a 3-byte instruction; and MOV AL, [EBX+10000H] is a 6-byte instruction.

-
Note that 1 and 4-byte displacements, but not 2-byte displacements, are supported for 32-bit addressing. Code size can be greatly improved by keeping stack frame variables within 128 bytes of EBP, and variables in pointed-to structures within 127 bytes of the start of the structure, so that displacements can be 1 rather than 4 bytes. +
Note that 1 and 4-byte displacements, but not 2-byte displacements, are supported for 32-bit addressing. Code size can be greatly improved by keeping stack frame variables within 128 bytes of EBP, and variables in pointed-to structures within 127 bytes of the start of the structure, so that displacements can be 1 rather than 4 bytes.

However, because 32-bit addressing supports many more addressing combinations than 16-bit addressing, the Mod-R/M byte can’t describe all the combinations. Therefore, whenever an index register (as described above) is involved, a second byte, the SIB byte, follows the Mod-R/M byte to provide additional address information. Consequently, whenever you use a scaled memory addressing register or use the sum of two registers to point to memory, you automatically add 1 cycle and 1 byte to that instruction. This is not to say that you shouldn’t use index registers when they’re needed, but if you find yourself using them inside key loops, you should see if it’s possible to move the index calculation outside the loop as, for example, in a loop like this:

diff --git a/14-01.html b/14-01.html index 9c24503..29c67dc 100644 --- a/14-01.html +++ b/14-01.html @@ -52,7 +52,7 @@

String searching is the simple matter of finding the first occurrence of a particular sequence of bytes (the pattern) within another sequence of bytes (the buffer). The obvious, brute-force approach is to try every possible match location, starting at the beginning of the buffer and advancing one position after each mismatch, until either a match is found or the buffer is exhausted. There’s even a nifty string instruction, REPZ CMPS, that’s perfect for comparing the pattern to the contents of the buffer at each location. What could be simpler?

We have some important information that we’re not yet using, though. Typically, the buffer will contain a wide variety of bytes. Let’s assume that the buffer contains text, in which case there will be dozens of different characters; and although the distribution of characters won’t usually be even, neither will any one character constitute half the buffer, or anything close. A reasonable conclusion is that the first character of the pattern will rarely match the first character of the buffer location currently being checked. This allows us to use the speedy REPNZ SCASB to whiz through the buffer, eliminating most potential match locations with single repetitions of SCASB. Only when that first character does (infrequently) match must we drop back to the slower REPZ CMPS approach.

It’s important to understand that we’re assuming that the buffer is typical text. That’s what I meant at the outset, when I said that the information you need may be under your nose.

-
Formally, you don’t know a blessed thing about the search buffer, but experience, common sense, and your knowledge of the application give you a great deal of useful, if somewhat imprecise, information. +
Formally, you don’t know a blessed thing about the search buffer, but experience, common sense, and your knowledge of the application give you a great deal of useful, if somewhat imprecise, information.

If the buffer contains the letter ‘A’ repeated 1,000 times, followed by the letter ‘B,’ then the REPNZ SCASB/REPZ CMPS approach will be much slower than the brute-force REPZ CMPS approach when searching for the pattern “AB,” because REPNZ SCASB would match at every buffer location. You could construct a horrendous worst-case scenario for almost any good optimization; the key is understanding the usual conditions under which your code will work.

As discussed in Chapter 9, we also know that certain characters have lower probabilities of matching than others. In a normal buffer, ‘T’ will match far more often than ‘X.’ Therefore, if we use REPNZ SCASB to scan for the least common letter in the search string, rather than the first letter, we’ll greatly decrease the number of times we have to drop back to REPZ CMPS, and the search time will become very close to the time it takes REPNZ SCASB to go from the start of the buffer to the match location. If the distance to the first match is N bytes, the least-common REPNZ SCASB approach will take about as long as N repetitions of REPNZ SCASB.

diff --git a/14-02.html b/14-02.html index 7d3578b..9c38659 100644 --- a/14-02.html +++ b/14-02.html @@ -41,7 +41,7 @@

If that makes your head hurt, it should—and don’t worry. This line of thinking, which is the basis of the Knuth-Morris-Pratt algorithm and half the basis of the Boyer-Moore algorithm, is what gives Boyer-Moore its reputation for inscrutability. That reputation is well deserved for this aspect (which I will not discuss further in this book), but there’s another part of Boyer-Moore that’s easily understood, easily implemented, and highly effective.

Consider this: We’re searching for the pattern “ABC,” beginning the search at the start (offset 0) of a buffer containing “ABZABC.” We match on ‘A,’ we match on ‘B,’ and we mismatch on ‘C’; the buffer contains a ‘Z’ in this position. What have we learned? Why, we’ve learned not only that the pattern doesn’t match the buffer starting at offset 0, but also that it can’t possibly match starting at offset 1 or offset 2, either! After all, there’s a ‘Z’ in the buffer at offset 2; since the pattern doesn’t contain a single ‘Z,’ there’s no way that the pattern can match starting at any location from which it would span the ‘Z’ at offset 2. We can just skip straight from offset 0 to offset 3 and continue, saving ourselves two comparisons.

Unfortunately, this approach only pays off big when a near-complete partial match is found; if the comparison fails on the first pattern character, as often happens, we can only skip ahead 1 byte, as usual. Look at it differently, though: What if we compare the pattern starting with the last (rightmost) byte, rather than the first (leftmost) byte? In other words, what if we compare from high memory toward low, in the direction in which string instructions go after the STD instruction? After all, we’re comparing one set of bytes (the pattern) to another set of bytes (a portion of the buffer); it doesn’t matter in the least in what order we compare them, so long as all the bytes in one set are compared to the corresponding bytes in the other set.

-
Why on earth would we want to start with the rightmost character? Because a mismatch on the rightmost character tells us a great deal more than a mismatch on the leftmost character. +
Why on earth would we want to start with the rightmost character? Because a mismatch on the rightmost character tells us a great deal more than a mismatch on the leftmost character.

We learn nothing new from a mismatch on the leftmost character, except that the pattern can’t match starting at that location. A mismatch on the rightmost character, however, tells us about the possibilities of the pattern matching starting at every buffer location from which the pattern spans the mismatch location. If the mismatched character in the buffer doesn’t appear in the pattern, then we’ve just eliminated not one potential match, but as many potential matches as there are characters in the pattern; that’s how many locations there are in the buffer that might have matched, but have just been shown not to, because they overlap the mismatched character that doesn’t belong in the pattern. In this case, we can skip ahead by the full pattern length in the buffer! This is how we can outperform even REPNZ SCASB; REPNZ SCASB has to check every byte in the buffer, but Boyer-Moore doesn’t.

Figure 14.1 illustrates the operation of a Boyer-Moore search when the rightcharacter of the search pattern (which is the first character that’s compared at each location because we’re comparing backwards) mismatches with a buffer character that appears nowhere in the pattern. Figure 14.2 illustrates the operation of a partial match when the mismatch occurs with a character that’s not a pattern member. In this case, we can only skip ahead past the mismatch location, resulting in an advance of fewer bytes than the pattern length, and potentially as little as the same single byte distance by which the standard search approach advances.

diff --git a/15-02.html b/15-02.html index 3014016..1e3962e 100644 --- a/15-02.html +++ b/15-02.html @@ -106,7 +106,7 @@ struct LinkNode *DeleteNodeAfter(struct LinkNode **HeadOfListPtr,
-->Figure 15.2  Using a dummy head and tail node with a linked list.

-
The next-node pointer of the head node, which points to the first real node, is the only part of the head node that’s actually used. This way the same code works on the head node as on the rest of the list, so there are no special cases. +The next-node pointer of the head node, which points to the first real node, is the only part of the head node that’s actually used. This way the same code works on the head node as on the rest of the list, so there are no special cases.

Likewise, there should be a separate node for the tail of the list, so that every node that contains real data is guaranteed to have a node on either side of it. In this scheme, an empty list contains two nodes, as shown in Figure 15.3. Although it is not necessary, the tail node may point to itself as its own next node, rather than contain a NULL pointer. This way, a deletion operation on an empty list will have no effect—quite unlike the same operation performed on a list terminated with a NULL pointer. The tail node of a list terminated like this can be detected because it will be the only node for which the next-node pointer equals the current-node pointer.

Figure 15.3 is a giant step in the right direction, but we can still make a few refinements. The inner loop of any code that scans through such a list has to perform a special test on each node to determine whether the tail has been reached. So, for example, code to find the first node containing a value field greater than or equal to a certain value has to perform two tests in the inner loop, as shown in Listing 15.4.

diff --git a/16-03.html b/16-03.html index 779b3ac..0a0591a 100644 --- a/16-03.html +++ b/16-03.html @@ -135,7 +135,7 @@

Listing 16.4 features several interesting tricks. First, it uses LODSB and XLAT in succession, a very neat way to get a pointed-to byte, advance the pointer, and look up the value indexed by the byte in a table, all with just two instruction bytes. (Interestingly, Listing 16.4 would probably run quite a bit better still on an 8088, where LODSB and XLAT have a greater advantage over conventional instructions. On the 486 and Pentium, however, LODSB and XLAT lose much of their appeal, and should be replaced with MOV instructions.) Better yet, LODSB and XLAT don’t alter the flags, so the Zero flag status set before LODSB is still around to be tested after XLAT .

Finally, if you look closely, you will see that Listing 16.4 jumps out of the loop to increment the word count in the case where a word is actually found, with a duplicate of the loop-bottom code placed after the code that increments the word count, to avoid an extra branch back into the loop; this replaces the more intuitive approach of jumping around the incrementing code to the loop bottom when a word isn’t found. Although this incurs a branch every time a word is found, a word is typically found only once every 5 or 6 bytes; on average, then, a branch is saved about two-thirds of the time. This is an excellent example of how understanding the nature of the data you’re processing allows you to optimize in ways the compiler can’t. Know your data!

So, gosh, Listing 16.4 is the best word-counting code in the universe, right? Not hardly. If there’s one thing my years of toil in this vale of silicon have taught me, it’s that there’s never a lack of potential for further optimization. Never! Off the top of my head, I can think of at least three ways to speed up Listing 16.4; and, since Turbo Profiler reports that even in Listing 16.4, 88 percent of the time is spent scanning the buffer (as opposed to reading the file), there’s potential for those further optimizations to improve performance significantly. (However, it is true that when access is performed to a hard rather than RAM disk, disk access jumps to about half of overall execution time.) One possible optimization is unrolling the loop, although that is truly a last resort because it tends to make further changes extremely difficult.

-
Exhaust all other optimizations before unrolling loops. +
Exhaust all other optimizations before unrolling loops.

Challenges and Hazards

The challenge I put to the readers of PC TECHNIQUES was to write a faster module to replace Listing 16.4. The author of the code that counted the words in my secret test file fastest on my 20 MHz cached 386 would be the winner and receive Numerous Valuable Prizes.

diff --git a/16-04.html b/16-04.html index 925c643..621163d 100644 --- a/16-04.html +++ b/16-04.html @@ -50,14 +50,14 @@

Harmless enough, save for two things. First, EBX happened to be zero at this point (a leftover from an earlier version of the code, as it turned out), so it was superfluous as a memory-addressing component; this made it possible to use base-only addressing ([EAX]) rather than base+index addressing ([EBX+EAX]), which saves a cycle on the 386. Second: Changing the instruction to CMP [EAX],DH saved 2 cycles—just enough, by good fortune, to speed up the whole program by 5 percent.

-
CMP reg,[mem] takes 6 cycles on the 386, but CMP [ mem ],reg takes only 5 cycles; you should always performCMP with the memory operand on the left on the 386. +
CMP reg,[mem] takes 6 cycles on the 386, but CMP [ mem ],reg takes only 5 cycles; you should always performCMP with the memory operand on the left on the 386.

(Granted, CMP [mem],reg is 1 cycle slower than CMP reg,[mem] on the 286, and they’re both the same on the 8088; in this case, though, the code was specific to the 386. In case you’re curious, both forms take 2 cycles on the 486; quite a lot faster, eh?)

Watch Out for Luggable Assumptions!

The first lesson to be learned here is not to lug assumptions that may no longer be valid from the 8088/286 world into the wonderful new world of 386 native-mode programming. The second lesson is that after you’ve slaved over your code for a while, you’re in no shape to see its flaws, or to be able to get the new perspectives needed to speed it up. I’ll bet Terje looked at that [EBX+EAX] addressing a hundred times while trying to speed up his code, but he didn’t really see what it did; instead, he saw what it was supposed to do. Mental shortcuts like this are what enable us to deal with the complexities of assembly language without overloading after about 20 instructions, but they can be a major problem when looking over familiar code.

The third, and most interesting, lesson is that a far more fruitful optimization came of all this, one that nicely illustrates that cycle counting is not the key to happiness, riches, and wondrous performance. After getting my 5 percent speedup, I mentioned to Terje the possibility of using a 64K lookup table. (This predated the arrival of entries for the optimization contest.) He said that he had considered it, but it didn’t seem to him to be worthwhile. He couldn’t shake the thought, though, and started to poke around, and one day, voila, he posted a new version of his word count program, WC50, that was much faster than the old version. I don’t have exact numbers, but Terje’s preliminary estimate was 80 percent faster, and word counting—including disk cache access time—proceeds at more than 3 MB per second on a 33 MHz 486. Even allowing for the speed of the 486, those are very impressive numbers indeed.

The point I want to make, though, is that the biggest optimization barrier that Terje faced was that he thought he had the fastest code possible. Once he opened up the possibility that there were faster approaches, and looked beyond the specific approach that he had so carefully optimized, he was able to come up with code that was a lot faster. Consider the incongruity of Terje’s willingness to consider a 5 percent speedup significant in light of his later near-doubling of performance.

-
Don’t get stuck in the rut of instruction-by-instruction optimization. It’s useful in key loops, but very often, a change in approach will work far greater wonders than any amount of cycle counting can. +
Don’t get stuck in the rut of instruction-by-instruction optimization. It’s useful in key loops, but very often, a change in approach will work far greater wonders than any amount of cycle counting can.

By the way, Terje’s WC50 program is a full-fledged counting program; it counts characters, words, and lines, can handle multiple files, and lets you specify the characters that separate words, should you so desire. Source code is provided as part of the archive WC50 comes in. All in all, it’s a nice piece of work, and you might want to take a look at it if you’re interested in really fast assembly code. I wouldn’t call it the fastest word-counting code, though, because I would of course never be so foolish as to call anything the fastest.

The Astonishment of Right-Brain Optimization

diff --git a/16-06.html b/16-06.html index 1d1017c..327863a 100644 --- a/16-06.html +++ b/16-06.html @@ -39,7 +39,7 @@

Levels of Optimization

Three levels of optimization were evident in the word-counting entries I received in response to my challenge. I’d briefly describe them as “fine-tuning,” “new perspective,” and “table-driven state machine.” The latter categories produce faster code, but, by the same token, they are harder to design, harder to implement, and more difficult to understand, so they’re suitable for only the most demanding applications. (Heck, I don’t even guarantee that David Stafford’s entry works perfectly, although, knowing him, it probably does; the more complex and cryptic the code, the greater the chance for obscure bugs.)

-
Remember, optimize only when needed, and stop when further optimization will not be noticed. Optimization that’s not perceptible to the user is like buying Telly Savalas a comb; it’s not going to do any harm, but it’s nonetheless a waste of time. +
Remember, optimize only when needed, and stop when further optimization will not be noticed. Optimization that’s not perceptible to the user is like buying Telly Savalas a comb; it’s not going to do any harm, but it’s nonetheless a waste of time.

Optimization Level 1: Good Code

The first level of optimization involves fine-tuning and clever use of the instruction set. The basic framework is still the same as my code (which in turn is basically the same as that of the original C code), but that framework is implemented more efficiently. diff --git a/17-03.html b/17-03.html index d2f0bbf..518ce4b 100644 --- a/17-03.html +++ b/17-03.html @@ -96,12 +96,12 @@


I was wrong. Wrong, wrong, wrong. (But at least I was smart enough to use a profiler before actually writing any new code.) Table 17.1 shows where the time actually goes in Listings 17.1 and 17.2. As you can see, the time taken by draw_pixel(), copy_cells(), and everything other than calculating the next generation is nothing more than noise. We could optimize these routines right down to executing instantaneously, and you know what? It wouldn’t make the slightest perceptible difference in how fast the program runs. Given the present state of our Game of Life implementation, the only areas worth looking at for possible optimizations are cell_state() and next_generation().

-
It’s worth noting, though, that one reason draw_pixel() doesn’t much affect performance is that in Listing 17.1, we’re smart enough to redraw pixels only when their states change, rather than during every generation. Detecting and eliminating redundant operations is part of knowing the nature of your data, and is a potent optimization technique that will be extremely useful a little later in this chapter. +
It’s worth noting, though, that one reason draw_pixel() doesn’t much affect performance is that in Listing 17.1, we’re smart enough to redraw pixels only when their states change, rather than during every generation. Detecting and eliminating redundant operations is part of knowing the nature of your data, and is a potent optimization technique that will be extremely useful a little later in this chapter.

The Hazards and Advantages of Abstraction

How can we speed up cell_state() and next_generation()? I’ll tell you how not to do it: By writing those member functions in assembly. It’s tempting to say that cell_state() is taking all the time, so we need to speed it up with assembly, but what we really need to do is figure out why cell_state() is taking all the time, then address that aspect of the program directly.

Once you know where you need to optimize, the one word to keep in mind isn’t assembly, it’s...plastics. No, actually, it’s abstraction. Well-written C and especially C++ programs are highly abstract models. For example, Listing 17.1 essentially creates a new programming language in which cells are tangible things, with built-in manipulation instructions. Given the cellmap member functions, you don’t even need to know the cell storage format! This is a wonderful thing, in general; it saves programming time and bugs, and frees you to work on the application’s needs, rather than implementation details.

-
However, if you never look beneath the surface of the abstract model at the implementation details, you have no idea of what the true performance cost of various operations is, and, without that, you have largely surrendered control over performance. +
However, if you never look beneath the surface of the abstract model at the implementation details, you have no idea of what the true performance cost of various operations is, and, without that, you have largely surrendered control over performance.

Having said that, let me hasten to add that algorithmic improvements can make a big difference even when working at a purely abstract level. For a large unordered data set, a high-level Quicksort will beat the pants off the best-implemented insertion sort you can imagine. Still, you can optimize your algorithm from here ’til doomsday, and if you have a fast algorithm running on top of a highly abstract programming model, you’ll almost certainly end up with a slow program. In Listing 17.1, the abstraction that’s killing us is that of looking at the eight neighbors with eight completely independent operations, requiring eight calls to cell_state() and eight calculations of cell address and cell mask. In fact, given the nature of cell storage, the eight neighbors are in a fixed relationship to one another, and the addresses and masks of all eight can generally be found very easily via hard-wired offsets and shifts once the address and mask of any one is known.


diff --git a/17-05.html b/17-05.html index 7c1431a..f38b4ca 100644 --- a/17-05.html +++ b/17-05.html @@ -130,7 +130,7 @@ neighbor_count++;

The neighbor-counting code is brought into next_generation, eliminating many function calls and from-scratch address/mask calculations; all multiplies are eliminated by using pointers and addition; and all cells are accessed directly via pointers and masks, eliminating all remaining function calls and from-scratch address/mask calculations.

The net effect of these optimizations is that Listing 17.4 is more than twice as fast as Listing 17.3; we’ve achieved the desired 18 generations per second, albeit only on a 486, and only at 96×96. (The #define that enables code limiting the speed to 18 Hz, which seemed ridiculous in Listing 17.1, is actually useful for keeping the generations from iterating too quickly when Listing 17.4 is running on a 486, especially with a small cellmap like 48×48.) We’ve sped things up by about eight times so far; we need to increase our speed another ten times to reach our goal of 200×200 at 18 generations per second on a 20 MHz 386.

It’s undoubtedly possible to improve the performance of Listing 17.4 further by fine-tuning the code, but no tremendous improvement is possible that way.

-
Once you’ve reached the point of fine-tuning pointer usage and register variables and the like in C or C++, you’ve become compiler-dependent; you therefore might as well go to assembly and get the real McCoy. +
Once you’ve reached the point of fine-tuning pointer usage and register variables and the like in C or C++, you’ve become compiler-dependent; you therefore might as well go to assembly and get the real McCoy.


diff --git a/17-06.html b/17-06.html index a9831be..c962b0f 100644 --- a/17-06.html +++ b/17-06.html @@ -51,7 +51,7 @@

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?

-
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. +
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.

diff --git a/17-08.html b/17-08.html index 32616b6..94cd3a7 100644 --- a/17-08.html +++ b/17-08.html @@ -41,7 +41,7 @@

Anyway, using the large model helps illustrate that it’s the data representation and the data processing approach you choose that matter most. Optimization details like memory models and segments and in-line functions and assembly language are important but secondary. Let your mind roam creatively before you start coding. Otherwise, you may find you’re writing well-tuned slow code, which is by no means the same thing as fast code.

Take a close look at Listing 17.5. You will see that it’s quite a bit simpler than Listing 17.4. To some extent, that’s because I decided to hard-wire the program to wrap around from one edge of the cellmap to the other (it’s much more interesting that way), but the main reason is that it’s a lot easier to work with the neighbor-count model. There’s no complex mask and pointer management, and the only thing that really needs to be optimized is scanning for zero bytes. (And, in fact, I haven’t optimized even that because it’s done in a C++ loop; it should really be REPZ SCASB.)

In truth, none of the code in Listing 17.5 is particularly well-optimized, and, as I noted, the program must be compiled with the large model for large cellmaps. Also, of course, the entire program is still in C++; note well that there’s not a whit of assembly here.

-
We’ve gotten more than a 30-times speedup simply by removing a little of the abstraction that C++ encourages, and by storing and processing the data in a manner appropriate for the typical nature of the data itself. In other words, we’ve done some linear, left-brained optimization (using pointers and reducing calls) and some non-linear, right-brained optimization (understanding the real problem and listening for the creative whisper of non-obvious solutions). +
We’ve gotten more than a 30-times speedup simply by removing a little of the abstraction that C++ encourages, and by storing and processing the data in a manner appropriate for the typical nature of the data itself. In other words, we’ve done some linear, left-brained optimization (using pointers and reducing calls) and some non-linear, right-brained optimization (understanding the real problem and listening for the creative whisper of non-obvious solutions).

No doubt we could get another two to five times improvement with good assembly code—but that’s dwarfed by a 30-times improvement, so optimization at a conceptual level must come first.

The Challenge That Ate My Life

diff --git a/18-01.html b/18-01.html index 216275c..60cc5fe 100644 --- a/18-01.html +++ b/18-01.html @@ -43,7 +43,7 @@

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.

-
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. +
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....

diff --git a/19-02.html b/19-02.html index cd020cd..e1a9df7 100644 --- a/19-02.html +++ b/19-02.html @@ -37,14 +37,14 @@


-
The Pentium, on the other hand, has two separate 8K caches, one for code and one for data, so code prefetches can never collide with data fetches; the prefetch queue can stall only when the code being fetched isn’t in the internal code cache. +The Pentium, on the other hand, has two separate 8K caches, one for code and one for data, so code prefetches can never collide with data fetches; the prefetch queue can stall only when the code being fetched isn’t in the internal code cache.

(And yes, self-modifying code still works; as with all Pentium changes, the dual caches introduce no incompatibilities with 386/486 code.) Also, because the code and data caches are separate, code can’t be driven out of the cache in a tight loop that accesses a lot of data, unlike the 486. In addition, the Pentium expands the 486’s 32-byte prefetch queue to 128 bytes. In conjunction with the branch prediction feature (described next), which allows the Pentium to prefetch properly at most branches, this larger prefetch queue means that the Pentium’s two pipes should be better fed than those of any previous x86 processor.

Crossing Cache Lines

There are three other characteristics of the Pentium that make for a healthy supply of instruction bytes. One is that the Pentium can prefetch instructions across cache lines. Unlike the 486, where there is a 3-cycle penalty for branching to an instruction that spans a cache line, there’s no such penalty on the Pentium. The second is that the cache line size (the number of bytes fetched from the external cache or main memory on a cache miss) on the Pentium is 32 bytes, twice the size of the 486’s cache line, so a cache miss causes a longer run of instructions to be placed in the cache than on the 486. The third is that the Pentium’s external bus is twice as wide as the 486’s, at 64 bits, and runs twice as fast, at 66 MHz, so the Pentium can fetch both instruction and data bytes from the external cache four times as fast as the 486.

-
Even when the Pentium is running flat-out with both pipes in use, it can generally consume only about twice as many bytes as the 486; so the ratio of external memory bandwidth to processing power is much improved, although real-world performance is heavily dependent on the size and speed of the external cache. +
Even when the Pentium is running flat-out with both pipes in use, it can generally consume only about twice as many bytes as the 486; so the ratio of external memory bandwidth to processing power is much improved, although real-world performance is heavily dependent on the size and speed of the external cache.

The upshot of all this is that at the same clock speed, with code and data that are mostly in the internal caches, the Pentium maxes out somewhere around twice as fast as a 486. (When the caches are missed a lot, the Pentium can get as much as three to four times faster, due to the superior external bus and bigger caches.) Most of this won’t affect how you program, but it is useful to know that you don’t have to worry about instruction fetching. It’s also useful to know the sizes of the caches because a high cache hit rate is crucial to Pentium performance. Cache misses are vastly slower than cache hits (anywhere from two to 50 or more times as slow, depending on the speed of the external cache and whether the external cache misses as well), and the Pentium can’t use the V-pipe on code that hasn’t already been executed out of the cache at least once. This means that it is very important to get the working sets of critical loops to fit in the internal caches.

One change in the Pentium that you definitely do have to worry about is superscalar execution. Utilization of the V-pipe can range from near zero percent to 100 percent, depending on the code being executed, and careful rearrangement of code can have amazing effects. Maxing out V-pipe use is not a trivial task; I’ll spend all of the next chapter discussing it so as to have time to cover it properly. In the meantime, two good references for superscalar programming and other Pentium information are Intel’s Pentium Processor User’s Manual: Volume 3: Architecture and Programming Manual (ISBN 1-55512-195-0; Intel order number 241430-001), and the article “Optimizing Pentium Code” by Mike Schmidt, in Dr. Dobb’s Journal for January 1994.

diff --git a/19-03.html b/19-03.html index 1567a68..eab3a6a 100644 --- a/19-03.html +++ b/19-03.html @@ -48,11 +48,11 @@ mov eax,[ebx]

costs three cycles. On the other hand, as noted above, branch targets can now span cache lines with impunity, so on the Pentium there’s no good argument for the paragraph (that is, 16-byte) alignment that Intel recommends for 486 jump targets. The 32-byte alignment might make for slightly more efficient Pentium cache usage, but would make code much bigger overall.

-
In fact, given that most jump targets aren’t in performance-critical code, it’s hard to make a compelling argument for aligning branch targets even on the 486. I’d say that no alignment (except possibly where you know a branch target lies in a key loop), or at most dword alignment (for the 386) is plenty, and can shrink code size considerably. +
In fact, given that most jump targets aren’t in performance-critical code, it’s hard to make a compelling argument for aligning branch targets even on the 486. I’d say that no alignment (except possibly where you know a branch target lies in a key loop), or at most dword alignment (for the 386) is plenty, and can shrink code size considerably.

Instruction prefixes are awfully expensive; avoid them if you can. (These include size and addressing prefixes, segment overrides, LOCK, and the 0FH prefixes that extend the instruction set with instructions such as MOVSX. The exceptions are conditional jumps, a fast special case.) At a minimum, a prefix byte generally takes an extra cycle and shuts down the V-pipe for that cycle, effectively costing as much as two normal instructions (although prefix cycles can overlap with previous multicycle instructions, or AGIs, as on the 486). This means that using 32-bit addressing or 32-bit operands in a 16-bit segment, or vice versa, makes for bigger code that’s significantly slower. So, for example, you should generally avoid 16-bit variables (shorts, in C) in 32-bit code, although if using 32-bit variables where they’re not needed makes your data space get a lot bigger, you may want to stick with shorts, especially since longs use the cache less efficiently than shorts. The trade-off depends on the amount of data and the number of instructions that reference that data. (eight-bit variables, such as chars, have no extra overhead and can be used freely, although they may be less desirable than longs for compilers that tend to promote variables to longs when performing calculations.) Likewise, you should if possible avoid putting data in the code segment and referring to it with a CS: prefix, or otherwise using segment overrides.

LOCK is a particularly costly instruction, especially on multiprocessor machines, because it locks the bus and requires that the hardware be brought into a synchronized state. The cost varies depending on the processor and system, but LOCK can make an INC [mem] instruction (which normally takes 3 cycles) 5, 10, or more cycles slower. Most programmers will never use LOCK on purpose—it’s primarily an operating system instruction—but there’s a hidden gotcha here because the XCHG instruction always locks the bus when used with a memory operand.

-
XCHG is a tempting instruction that’s often used in assembly language; for example, exchanging with video memory is a popular way to read and write VGA memory in a single instruction—but it’s now a bad idea. As it happens, on the 486 and Pentium, using MOVs to read and write memory is faster, anyway; and even on the 486, my measurements indicate a five-cycle tax for LOCK in general, and a nine-cycle execution time for XCHG with memory. Avoid XCHG with memory if you possibly can. +
XCHG is a tempting instruction that’s often used in assembly language; for example, exchanging with video memory is a popular way to read and write VGA memory in a single instruction—but it’s now a bad idea. As it happens, on the 486 and Pentium, using MOVs to read and write memory is faster, anyway; and even on the 486, my measurements indicate a five-cycle tax for LOCK in general, and a nine-cycle execution time for XCHG with memory. Avoid XCHG with memory if you possibly can.

As with the 486, don’t use ENTER or LEAVE, which are slower than the equivalent discrete instructions. Also, start using TEST reg,reg instead of AND reg,reg or OR reg,reg to test whether a register is zero. The reason, as we’ll see in Chapter 21, is that TEST, unlike AND and OR, never modifies the target register. Although in this particular case AND and OR don’t modify the target register either, the Pentium has no way of knowing that ahead of time, so if AND or OR goes through the U-pipe, the Pentium may have to shut down the V-pipe for a cycle to avoid potential dependencies on the result of the AND or OR. TEST suffers from no such potential dependencies.


diff --git a/19-04.html b/19-04.html index 4ccc75c..8045ef1 100644 --- a/19-04.html +++ b/19-04.html @@ -38,13 +38,13 @@


Branch Prediction

One brand-spanking-new feature of the Pentium is branch prediction, whereby the Pentium tries to guess, based on past history, which way (or, for conditional jumps, whether or not), your code will jump at each branch, and prefetches along the likelier path. If the guess is correct, the branch or fall-through takes only 1 cycle—2 cycles less than a branch and the same as a fall-through on the 486; if the guess is wrong, the branch or fall-through takes 4 or 5 cycles (if it executes in the U- or V-pipe, respectively)—1 or 2 cycles more than a branch and 3 or 4 cycles more than a fall-through on the 486.

-
Branch prediction is unprecedented in the x86, and fundamentally alters the nature of pedal-to-the-metal optimization, for the simple reason that it renders unrolled loops largely obsolete. Rare indeed is the loop that can’t afford to spare even 1 or 0 (yes, zero!) cycles per iteration for loop counting, and that’s how low the cost can go for maintaining a loop on the Pentium. +
Branch prediction is unprecedented in the x86, and fundamentally alters the nature of pedal-to-the-metal optimization, for the simple reason that it renders unrolled loops largely obsolete. Rare indeed is the loop that can’t afford to spare even 1 or 0 (yes, zero!) cycles per iteration for loop counting, and that’s how low the cost can go for maintaining a loop on the Pentium.

Also, unrolled loops are bigger than normal loops, so there are extra (and expensive) cache misses the first time through the loop if the entire loop isn’t already in the cache; then, too, an unrolled loop will shoulder other code out of the internal and external caches. If in a critical loop you absolutely need the time taken by the loop control instructions, or if you need an extra register that can be freed by unrolling a loop, then by all means unroll the loop. Don’t expect the sort of speed-up you get from this on the 486 or especially the 386, though, and watch out for the cache effects.

You may well wonder exactly when the Pentium correctly predicts branching. Alas, this is one area that Intel has declined to document, beyond saying that you should endeavor to fall through branches when you have a choice. That’s good advice on every other x86 processor, anyway, so it’s well worth following. Also, it’s a pretty safe bet that in a tight loop, the Pentium will start guessing the right branch direction at the bottom of the loop pretty quickly, so you can treat loop branches as one-cycle instructions.

It’s an equally safe bet that it’s a bad move to have in a loop a conditional branch that goes both ways on a random basis; it’s hard to see how the Pentium could consistently predict such branches correctly, and mispredicted branches are more expensive than they might appear to be. Not only does a mispredicted branch take 4 or 5 cycles, but the Pentium can potentially execute as many as 8 or 10 instructions in that time—3 times as many as the 486 can execute during its branch time—so correct branch prediction (or eliminating branch instructions, if possible) is very important in inner loops. Note that on the 486 you can count on a branch to take 1 cycle when it falls through, but on the Pentium you can’t be sure whether it will take 1 or either 4 or 5 cycles on any given iteration.

-
As things currently stand, branch prediction is an annoyance for assembly language optimization because it’s impossible to be certain exactly how code will perform until you measure it, and even then it’s difficult to be sure exactly where the cycles went. All I can say is try to fall through branches if possible, and try to be consistent in your branching if not. +
As things currently stand, branch prediction is an annoyance for assembly language optimization because it’s impossible to be certain exactly how code will perform until you measure it, and even then it’s difficult to be sure exactly where the cycles went. All I can say is try to fall through branches if possible, and try to be consistent in your branching if not.

Miscellaneous Pentium Topics

The Pentium has all the instructions of the 486, plus a few new ones. One much-needed instruction that has finally made it into the instruction set is CPUID, which allows your code to determine what processor it’s running on. CPUID is 15 years late, but at least it’s finally here. Another new instruction is CMPXCHG8B, which does a compare and conditional exchange on a qword. CMPXCHG8B doesn’t seem to me to be a particularly useful instruction, but I’m sure Intel wouldn’t have added it without a reason; if you know of a use for it, please pass it along to me.

diff --git a/20-01.html b/20-01.html index 03bebe9..d842a06 100644 --- a/20-01.html +++ b/20-01.html @@ -53,7 +53,7 @@
-->Figure 20.1  The Pentium’s two pipes.

Getting two instructions executing simultaneously in the two pipes is trickier than it sounds, not only because the V-pipe can handle only a relatively small subset of the Pentium’s instruction set, but also because those instructions that the V-pipe can handle are able to pair only with certain U-pipe instructions. For example, MOVSD uses both pipes, so no instruction can be executed in parallel with MOVSD.

-
The use of both pipes does make MOVSD nearly twice as fast on the Pentium as on the 486, but it’s nonetheless slower than using equivalent simpler instructions that allow for superscalar execution. Stick to the Pentium’s RISC-like instructions—the pairable instructions I’ll discuss next—when you’re seeking maximum performance, with just a few exceptions such as REP MOVS and REP STOS. +
The use of both pipes does make MOVSD nearly twice as fast on the Pentium as on the 486, but it’s nonetheless slower than using equivalent simpler instructions that allow for superscalar execution. Stick to the Pentium’s RISC-like instructions—the pairable instructions I’ll discuss next—when you’re seeking maximum performance, with just a few exceptions such as REP MOVS and REP STOS.

Trickier yet, register contention can shut down the V-pipe on any given cycle, and Address Generation Interlocks (AGIs) can stall either pipe at any time, as we’ll see in the next chapter.

diff --git a/20-02.html b/20-02.html index c2f8bc4..ac9fd85 100644 --- a/20-02.html +++ b/20-02.html @@ -123,7 +123,7 @@ ROL/ROR/RCL/RCR reg,1 (1 cycle)

Table 20.2 Instructions that, when executed in the U-pipe, allow V-pipe-executable instructions to execute simultaneously (pair) in the V-pipe.


-
A fundamental rule of Pentium optimization is that it pays to break complex instructions into equivalent simple instructions, then shuffle the simple instructions for maximum use of the V-pipe. This is true partly because most of the pairable instructions are simple instructions, and partly because breaking instructions into pieces allows more freedom to rearrange code to avoid the AGIs and register contention I’ll discuss in the next chapter. +
A fundamental rule of Pentium optimization is that it pays to break complex instructions into equivalent simple instructions, then shuffle the simple instructions for maximum use of the V-pipe. This is true partly because most of the pairable instructions are simple instructions, and partly because breaking instructions into pieces allows more freedom to rearrange code to avoid the AGIs and register contention I’ll discuss in the next chapter.


Figure 20.2
  Instruction flow through the two pipes. @@ -163,7 +163,7 @@ mov [MemVar],edx

The single complex instruction takes 3 cycles and is 6 bytes long; with proper sequencing, interleaving the simple instructions with other instructions that don’t use EDX or Mem Var, the three-instruction sequence can be reduced to 1.5 cycles, but it is 14 bytes long.

-
It’s not unusual for Pentium optimization to approximately double both performance and code size at the same time. In an important loop, go for performance and ignore the size, but on a program-wide basis, the size bears watching. +
It’s not unusual for Pentium optimization to approximately double both performance and code size at the same time. In an important loop, go for performance and ignore the size, but on a program-wide basis, the size bears watching.


diff --git a/20-03.html b/20-03.html index aeb7e26..47eb58c 100644 --- a/20-03.html +++ b/20-03.html @@ -39,7 +39,7 @@

Lockstep Execution

You may wonder why anyone would bother breaking ADD [MemVar],EAX into three instructions, given that this instruction can go through either pipe with equal ease. The answer is that while the memory-accessing instructions other than MOV, PUSH, and POP listed in Table 20.1 (that is, INC/DEC [mem], ADD/SUB/XOR/AND/OR/CMP/ADC/SBB reg,[mem], and ADD/SUB/XOR/AND/OR/CMP/ADC/SBB [mem],reg/immed) can be paired, they do not provide the 100 percent overlap that we seek. If you look at Tables 20.1 and 20.2, you will see that instructions taking from 1 to 3 cycles can pair. However, any pair of instructions goes through the two pipes in lockstep. This means, for example, that if ADD [EBX],EDX is going through the U-pipe, and INC EAX is going through the V-pipe, the V-pipe will be idle for 2 of the 3 cycles that the U-pipe takes to execute its instruction, as shown in Figure 20.4. Out of the theoretical 6 cycles of work that can be done during this time, we actually get only 4 cycles of work, or 67 percent utilization. Even though these instructions pair, then, this sequence fails to make maximum use of the Pentium’s horsepower.

The key here is that when two instructions pair, both execution units are tied up until both instructions have finished (which means at least for the amount of time required for the longer of the two to execute, plus possibly some extra cycles for pairable instructions that can’t fully overlap, as described below). The logical conclusion would seem to be that we should strive to pair instructions of the same lengths, but that is often not correct.

-
The actual rule is that we should strive to pair one-cycle instructions (or, at most, two-cycle instructions, but not three-cycle instructions), which in turn leads to the corollary that we should, in general, use mostly one-cycle instructions when optimizing. +
The actual rule is that we should strive to pair one-cycle instructions (or, at most, two-cycle instructions, but not three-cycle instructions), which in turn leads to the corollary that we should, in general, use mostly one-cycle instructions when optimizing.


Figure 20.4
  Lockstep execution and idle time in the V-pipe. diff --git a/20-04.html b/20-04.html index d520d58..abb1ab2 100644 --- a/20-04.html +++ b/20-04.html @@ -67,7 +67,7 @@ mov [ebx],dl

Register Starvation

The above examples should make it pretty clear that effective superscalar programming puts a lot of strain on the Pentium’s relatively small register set. There are only seven general-purpose registers (I strongly suggest using EBP in critical loops), and it does not help to have to sacrifice one of those registers for temporary storage on each complex memory operation; in pre-superscalar days, we used to employ those handy CISC memory instructions to do all that stuff without using any extra registers.

-
More problematic still is that for maximum pairing, you’ll typically have two operations proceeding at once, one in each pipe, and trying to keep two operations in registers at once is difficult indeed. There’s not much to be done about this, other than clever and Spartan register usage, but be aware that it’s a major element of Pentium performance programming. +
More problematic still is that for maximum pairing, you’ll typically have two operations proceeding at once, one in each pipe, and trying to keep two operations in registers at once is difficult indeed. There’s not much to be done about this, other than clever and Spartan register usage, but be aware that it’s a major element of Pentium performance programming.

Also be aware that prefixes of every sort, with the sole exception of the 0FH prefix on non-short conditional jumps, always execute in the U-pipe, and that Intel’s documentation indicates that no pairing can happen while a prefix byte executes. (As I’ll discuss in the next chapter, my experiments indicate that this rule doesn’t always apply to multiple-cycle instructions, but you still won’t go far wrong by assuming that the above rule is correct and trying to eliminate prefix bytes.) A prefix byte takes one cycle to execute; after that cycle, the actual prefixed instruction itself will go through the U-pipe, and if it and the following instruction are mutually pairable, then they will pair. Nonetheless, prefix bytes are very expensive, effectively taking at least as long as two normal instructions, and possibly, if a prefixed instruction could otherwise have paired in the V-pipe with the previous instruction, taking as long as three normal instructions, as shown in Figure 20.8.

diff --git a/23-02.html b/23-02.html index b4f5cfd..cce0ac2 100644 --- a/23-02.html +++ b/23-02.html @@ -106,7 +106,7 @@

The method used in the VGA BIOS to set registers is to point DX to the desired Index register, load AL with the index, perform a byte OUT, increment DX to point to the Data register (except in the case of the AC, where DX remains the same), load AL with the desired data, and perform a byte OUT. A handy shortcut is to point DX to the desired Index register, load AL with the index, load AH with the data, and perform a word OUT. Since the high byte of the OUT value goes to port DX+1, this is equivalent to the first method but is faster. However, this technique does not work for programming the AC Index and Data registers; both AC registers are addressed at 3C0H, so two separate byte OUTs must be used to program the AC. (Actually, word OUTs to the AC do work in the EGA, but not in the VGA, so they shouldn’t be used.) As mentioned above, you must be sure which mode—Index or Data—the AC is in before you do an OUT to 3C0H; you can read the Input Status 1 register at any time to force the AC to Index mode.

How safe is the word-OUT method of addressing VGA registers? I have, in the past, run into adapter/computer combinations that had trouble with word OUTs; however, all such problems I am aware of have been fixed. Moreover, a great deal of graphics software now uses word OUTs, so any computer or VGA that doesn’t properly support word OUTs could scarcely be considered a clone at all.

-
A speed tip: The setting of each chip’s Index register remains the same until it is reprogrammed. This means that in cases where you are setting the same internal register repeatedly, you can set the Index register to point to that internal register once, then write to the Data register multiple times. For example, the Bit Mask register (GC register 8) is often set repeatedly inside a loop when drawing lines. The standard code for this is: +
A speed tip: The setting of each chip’s Index register remains the same until it is reprogrammed. This means that in cases where you are setting the same internal register repeatedly, you can set the Index register to point to that internal register once, then write to the Data register multiple times. For example, the Bit Mask register (GC register 8) is often set repeatedly inside a loop when drawing lines. The standard code for this is:
diff --git a/23-04.html b/23-04.html
index 4a5aea0..193eff0 100644
--- a/23-04.html
+++ b/23-04.html
@@ -49,7 +49,7 @@
 

Smooth horizontal panning is provided by the Horizontal Pel Panning register, AC register 13H, working in conjunction with the start address. Up to 7 pixels worth of single pixel panning of the displayed image to the left is performed by increasing the Horizontal Pel Panning register from 0 to 7. This exhausts the range of motion possible via the Horizontal Pel Panning register; the next pixel’s worth of smooth panning is accomplished by incrementing the start address by one and resetting the Horizontal Pel Panning register to 0. Smooth horizontal panning should be viewed as a series of fine adjustments in the 8-pixel range between coarse byte-sized adjustments.

A horizontal panning oddity: Alone among VGA modes, text mode (in most cases) has 9 dots per character clock. Smooth panning in this mode requires cycling the Horizontal Pel Panning register through the values 8, 0, 1, 2, 3, 4, 5, 6, and 7. 8 is the “no panning” setting.

There is one annoying quirk about programming the AC. When the AC Index register is set, only the lower five bits are used as the internal index. The next most significant bit, bit 5, controls the source of the video data sent to the monitor by the VGA. When bit 5 is set to 1, the output of the palette RAM, derived from display memory, controls the displayed pixels; this is normal operation. When bit 5 is 0, video data does not come from the palette RAM, and the screen becomes a solid color. The only time bit 5 of the AC Index register should be 0 is during the setting of a palette RAM register, since the CPU is only able to write to palette RAM when bit 5 is 0. (Some VGAs do not enforce this, but you should always set bit 5 to 0 before writing to the palette RAM just to be safe.) Immediately after setting palette RAM, however, 20h (or any other value with bit 5 set to 1) should be written to the AC Index register to restore normal video, and at all other times bit 5 should be set to 1.

-
By the way, palette RAM can be set via the BIOS video interrupt (interrupt 10H), function 10H. Whenever an VGA function can be performed reasonably well through a BIOS function, as it can in the case of setting palette RAM, it should be, both because there is no point in reinventing the wheel and because the BIOS may well mask incompatibilities between the IBM VGA and VGA clones. +
By the way, palette RAM can be set via the BIOS video interrupt (interrupt 10H), function 10H. Whenever an VGA function can be performed reasonably well through a BIOS function, as it can in the case of setting palette RAM, it should be, both because there is no point in reinventing the wheel and because the BIOS may well mask incompatibilities between the IBM VGA and VGA clones.


diff --git a/23-06.html b/23-06.html index 8efdca2..3567cd5 100644 --- a/23-06.html +++ b/23-06.html @@ -38,7 +38,7 @@


One possible solution to this problem is to pick a second page start address that has a 0 value for the lower byte, so only the Start Address High register ever needs to be set, but in the sample program in Listing 23.1 I’ve gone for generality and always set both bytes. To avoid mismatched start address bytes, the sample program waits for pixel data to be displayed, as indicated by the Display Enable status; this tells us we’re somewhere in the displayed portion of the frame, far enough away from vertical sync so we can be sure the new start address will get used at the next vertical sync. Once the Display Enable status is observed, the program sets the new start address, waits for vertical sync to happen, sets the new pel panning state, and then continues drawing. Don’t worry about the details right now; page flipping will come up again, at considerably greater length, in later chapters.

-
As an interesting side note, be aware that if you run DOS software under a multitasking environment such as Windows NT, timeslicing delays can make mismatched start address bytes or mismatched start address and pel panning settings much more likely, for the graphics code can be interrupted at any time. This is also possible, although much less likely, under non-multitasking environments such as DOS, because strategically placed interrupts can cause the same sorts of problems there. For maximum safety, you should disable interrupts around the key portions of your page-flipping code, although here we run into the problem that if interrupts are disabled from the time we start looking for Display Enable until we set the Pel Panning register, they will be off for far too long, and keyboard, mouse, and network events will potentially be lost. Also, disabling interrupts won’t help in true multitasking environments, which never let a program hog the entire CPU. This is one reason that pel panning, although indubitably flashy, isn’t widely used and should be reserved for only those cases where it’s absolutely necessary. +
As an interesting side note, be aware that if you run DOS software under a multitasking environment such as Windows NT, timeslicing delays can make mismatched start address bytes or mismatched start address and pel panning settings much more likely, for the graphics code can be interrupted at any time. This is also possible, although much less likely, under non-multitasking environments such as DOS, because strategically placed interrupts can cause the same sorts of problems there. For maximum safety, you should disable interrupts around the key portions of your page-flipping code, although here we run into the problem that if interrupts are disabled from the time we start looking for Display Enable until we set the Pel Panning register, they will be off for far too long, and keyboard, mouse, and network events will potentially be lost. Also, disabling interrupts won’t help in true multitasking environments, which never let a program hog the entire CPU. This is one reason that pel panning, although indubitably flashy, isn’t widely used and should be reserved for only those cases where it’s absolutely necessary.

Waiting for the sync pulse has the side effect of causing program execution to synchronize to the VGA’s frame rate of 60 or 70 frames per second, depending on the display mode. This synchronization has the useful consequence of causing the program to execute at the same speed on any CPU that can draw fast enough to complete the drawing in a single frame; the program just idles for the rest of each frame that it finishes before the VGA is finished displaying the previous frame.

diff --git a/24-03.html b/24-03.html index 51be3c4..d094735 100644 --- a/24-03.html +++ b/24-03.html @@ -60,7 +60,7 @@ MOV DX,(VALUE2 SHL 8) OR VALUE1

which takes only three bytes and is faster, being a single instruction. (Note, though, that in 32-bit protected mode, there’s a size and performance penalty for 16-bit instructions such as the MOV above; see the first part of this book for details.) As shown, a macro is an ideal place to use this technique; the macro invocation can refer to two separate byte values, making matters easier for the programmer, while the macro itself can combine the values into a single word-sized constant.

-
A minor optimization tip illustrated in the listing is the use of INC AX and DEC AX in the DrawVerticalBox subroutine when only AL actually needs to be modified. Word-sized register increment and decrement instructions (or dword-sized instructions in 32-bit protected mode) are only one byte long, while byte-size register increment and decrement instructions are two bytes long. Consequently, when size counts, it is worth using a whole 16-bit (or 32-bit) register instead of the low 8 bits of that register for INC and DEC—if you don’t need the upper portion of the register for any other purpose, or if you can be sure that the INC or DEC won’t affect the upper part of the register. +
A minor optimization tip illustrated in the listing is the use of INC AX and DEC AX in the DrawVerticalBox subroutine when only AL actually needs to be modified. Word-sized register increment and decrement instructions (or dword-sized instructions in 32-bit protected mode) are only one byte long, while byte-size register increment and decrement instructions are two bytes long. Consequently, when size counts, it is worth using a whole 16-bit (or 32-bit) register instead of the low 8 bits of that register for INC and DEC—if you don’t need the upper portion of the register for any other purpose, or if you can be sure that the INC or DEC won’t affect the upper part of the register.

The latches and ALUs are central to high-performance VGA code, since they allow programs to process across all four memory planes without a series of OUTs and read/write operations. It is not always easy to arrange a program to exploit this power, however, because the ALUs are far more limited than a CPU. In many instances, however, additional hardware in the VGA, including the bit mask, the set/reset features, and the barrel shifter, can assist the ALUs in controlling data, as we’ll see in the next few chapters.


diff --git a/25-03.html b/25-03.html index d9a3838..55db99c 100644 --- a/25-03.html +++ b/25-03.html @@ -37,7 +37,7 @@


It’s worth pointing out again that the bit mask operates on the data in the latches, not on the data in display memory. This makes the bit mask a flexible resource that with a little imagination can be used for some interesting purposes. For example, you could fill the latches with a solid background color (by writing the color somewhere in display memory, then reading that location to load the latches), and then use the Bit Mask register (or write mode 3, as we’ll see later) as a mask through which to draw a foreground color stencilled into the background color without reading display memory first. This only works for writing whole bytes at a time (clipped bytes require the use of the bit mask; unfortunately, we’re already using it for stencilling in this case), but it completely eliminates reading display memory and does foreground-plus-background drawing in one blurry-fast pass.

-
This last-described example is a good illustration of how I’d suggest you approach the VGA: As a rich collection of hardware resources that can profitably be combined in some non-obvious ways. Don’t let yourself be limited by the obvious applications for the latches, bit mask, write modes, read modes, map mask, ALUs, and set/reset circuitry. Instead, try to imagine how they could work together to perform whatever task you happen to need done at any given time. I’ve made my code as much as four times faster by doing this, as the discussion of Mode X in Chapters 47–49 demonstrates. +
This last-described example is a good illustration of how I’d suggest you approach the VGA: As a rich collection of hardware resources that can profitably be combined in some non-obvious ways. Don’t let yourself be limited by the obvious applications for the latches, bit mask, write modes, read modes, map mask, ALUs, and set/reset circuitry. Instead, try to imagine how they could work together to perform whatever task you happen to need done at any given time. I’ve made my code as much as four times faster by doing this, as the discussion of Mode X in Chapters 47–49 demonstrates.

The example code in Listing 25.1 is designed to illustrate the use of the Data Rotate and Bit Mask registers, and is not as fast or as complete as it might be. The case where text is byte-aligned could be detected and performed much faster, without the use of the Bit Mask or Data Rotate registers and with only one display memory access per font byte (to write the font byte), rather than four (to read display memory and write the font byte to each of the two bytes the character spans). Likewise, non-aligned text drawing could be streamlined to one display memory access per byte by having the CPU rotate and combine the font data directly, rather than setting up the VGA’s hardware to do it. (Listing 25.1 was designed to illustrate VGA data rotation and bit masking rather than the fastest way to draw text. We’ll see faster text-drawing code soon.) One excellent rule of thumb is to minimize display memory accesses of all types, especially reads, which tend to be considerably slower than writes. Also, in Listing 25.1 it would be faster to use a table lookup to calculate the bit masks for the two halves of each character rather than the shifts used in the example.

For another (and more complex) example of drawing bit-mapped text on the VGA, see John Cockerham’s article, “Pixel Alignment of EGA Fonts,” PC Tech Journal, January, 1987. Parenthetically, I’d like to pass along John’s comment about the VGA: “When programming the VGA, everything is complex.”

diff --git a/25-06.html b/25-06.html index db8d08d..d641c9e 100644 --- a/25-06.html +++ b/25-06.html @@ -41,7 +41,7 @@

Notes on Set/Reset

The set/reset circuitry is not active in write modes 1 or 2. The Enable Set/Reset register is inactive in write mode 3, but the Set/Reset register provides the primary drawing color in write mode 3, as discussed in the next chapter.

-
Be aware that because set/reset directly replaces CPU data, it does not necessarily have to force an entire display memory byte to 0 or 0FFH, even when set/reset is replacing CPU data for all planes. For example, if the Bit Mask register is set to 80H, the set/reset circuitry can only modify bit 7 of the destination byte in each plane, since the other seven bits will come from the latches for each plane. Similarly, the set/reset value for each plane can be modified by that plane’s ALU. Once again, this illustrates that set/reset merely replaces the CPU data for selected planes; the set/reset value is then processed in exactly the same way that CPU data normally is. +
Be aware that because set/reset directly replaces CPU data, it does not necessarily have to force an entire display memory byte to 0 or 0FFH, even when set/reset is replacing CPU data for all planes. For example, if the Bit Mask register is set to 80H, the set/reset circuitry can only modify bit 7 of the destination byte in each plane, since the other seven bits will come from the latches for each plane. Similarly, the set/reset value for each plane can be modified by that plane’s ALU. Once again, this illustrates that set/reset merely replaces the CPU data for selected planes; the set/reset value is then processed in exactly the same way that CPU data normally is.

A Brief Note on Word OUTs

In the early days of the EGA and VGA, there was considerable debate about whether it was safe to do word OUTs (OUT DX,AX) to set Index/Data register pairs in a single instruction. Long ago, there were a few computers with buses that weren’t quite PC-compatatible, in that the two bytes in each word OUT went to the VGA in the wrong order: Data register first, then Index register, with predictably disastrous results. Consequently, I generally wrote my code in those days to use two 8-bit OUTs to set indexed registers. Later on, I made it a habit to use macros that could do either one 16-bit OUT or two 8-bit OUTs, depending on how I chose to assemble the code, and in fact you’ll find both ways of dealing with OUTs sprinkled through the code in this part of the book. Using macros for word OUTs is still not a bad idea in that it does no harm, but in my opinion it’s no longer necessary. Word OUTs are standard now, and it’s been a long time since I’ve heard of them causing any problems.


diff --git a/26-03.html b/26-03.html index 26f1229..92e825f 100644 --- a/26-03.html +++ b/26-03.html @@ -38,7 +38,7 @@


The key to understanding Listing 26.1 is understanding the effect of ANDing the rotated CPU data with the contents of the Bit Mask register. The CPU data is the pattern for the character to be drawn, with bits equal to 1 indicating where character pixels are to appear. The Data Rotate register is set to rotate the CPU data to pixel-align it, since without rotation characters could only be drawn on byte boundaries.

-
As I pointed out in Chapter 25, the CPU is perfectly capable of rotating the data itself, and it’s often the case that that’s more efficient. The problem with using the Data Rotate register is that the OUT that sets that register is time-consuming, especially for proportional text, which requires a different rotation for each character. Also, if the code performs full-byte accesses to display memory—that is, if it combines pieces of two adjacent characters into one byte—whenever possible for efficiency, the CPU generally has to do extra work to prepare the data so the VGA’s rotator can handle it. +
As I pointed out in Chapter 25, the CPU is perfectly capable of rotating the data itself, and it’s often the case that that’s more efficient. The problem with using the Data Rotate register is that the OUT that sets that register is time-consuming, especially for proportional text, which requires a different rotation for each character. Also, if the code performs full-byte accesses to display memory—that is, if it combines pieces of two adjacent characters into one byte—whenever possible for efficiency, the CPU generally has to do extra work to prepare the data so the VGA’s rotator can handle it.

At the same time that the Data Rotate register is set, the Bit Mask register is set to allow the CPU to modify only that portion of the display memory byte accessed that the pixel-aligned character falls in, so that other characters and/or graphics data won’t be wiped out. The result of ANDing the rotated CPU data byte with the contents of the Bit Mask register is a bit mask that allows only the bits equal to 1 in the original character pattern (rotated and masked to provide pixel alignment) to be modified by the CPU; all other bits come straight from the latches. The latches should have previously been loaded from the target address, so the effect of the ultimate synthesized bit mask value is to allow the CPU to modify only those pixels in display memory that correspond to the 1 bits in that part of the pixel-aligned character that falls in the currently addressed byte. The color of the pixels set by the CPU is determined by the contents of the Set/Reset register.

diff --git a/27-01.html b/27-01.html index 09e59a1..87038d0 100644 --- a/27-01.html +++ b/27-01.html @@ -55,7 +55,7 @@
-->Figure 27.1  VGA data flow in write mode 2.

-
It’s worth noting two differences between write mode 2 and write mode 0, the standard write mode of the VGA. First, rotation of the CPU data byte does not take place in write mode 2. Second, the Set/Reset and Enable Set/Reset registers have no effect in write mode 2. +It’s worth noting two differences between write mode 2 and write mode 0, the standard write mode of the VGA. First, rotation of the CPU data byte does not take place in write mode 2. Second, the Set/Reset and Enable Set/Reset registers have no effect in write mode 2.

Now that we understand the mechanics of write mode 2, we can step back and get a feel for what it might be useful for. View bits 3-0 of the CPU byte as a single pixel in one of 16 colors. Next imagine that nibble turned sideways and written across the four planes, one bit to a plane. Finally, expand each of the bits to a byte, as shown in Figure 27.2, so that 8 pixels are drawn in the color selected by bits 3-0 of the CPU byte. Within the constraints of the VGA’s data paths, that’s exactly what write mode 2 does.

diff --git a/27-03.html b/27-03.html index 295cb17..dfca4f7 100644 --- a/27-03.html +++ b/27-03.html @@ -38,7 +38,7 @@


“That’s an interesting application of write mode 2,” you may well say, “but is it really useful?” While the ability to convert chunky bitmaps into VGA bitmaps does have its uses, Listing 27.1 is primarily intended to illustrate the mechanics of write mode 2.

-
For performance, it’s best to store 16-color bitmaps in pre-separated four-plane format in system memory, and copy one plane at a time to the screen. Ideally, such bitmaps should be copied one scan line at a time, with all four planes completed for one scan line before moving on to the next. I say this because when entire images are copied one plane at a time, nasty transient color effects can occur as one plane becomes visibly changed before other planes have been modified. +
For performance, it’s best to store 16-color bitmaps in pre-separated four-plane format in system memory, and copy one plane at a time to the screen. Ideally, such bitmaps should be copied one scan line at a time, with all four planes completed for one scan line before moving on to the next. I say this because when entire images are copied one plane at a time, nasty transient color effects can occur as one plane becomes visibly changed before other planes have been modified.

Drawing Color-Patterned Lines Using Write Mode 2

A more serviceable use of write mode 2 is shown in the program presented in Listing 27.2. The program draws multicolored horizontal, vertical, and diagonal lines, basing the color patterns on passed color tables. Write mode 2 is ideal because in this application color can vary from one pixel to the next, and in write mode 2 all that’s required to set pixel color is a change of the lower nibble of the byte written by the CPU. Set/reset could be used to achieve the same result, but an index/data pair of OUTs would be required to set the Set/Reset register to each new color. Similarly, the Map Mask register could be used in write mode 0 to set pixel color, but in this case not only would an index/data pair of OUTs be required but there would also be no guarantee that data already in display memory wouldn’t interfere with the color of the pixel being drawn, since the Map Mask register allows only selected planes to be drawn to.

diff --git a/28-03.html b/28-03.html index 622868d..30498d4 100644 --- a/28-03.html +++ b/28-03.html @@ -39,7 +39,7 @@

By the way, the code in Listing 28.1 is intended only to illustrate read mode 0, and is, in general, a poor way to perform animation, since it’s slow and tends to flicker. Later in this book, we’ll take a look at some far better VGA animation techniques.

As you’d expect, neither the read mode nor the setting of the Read Map register affects CPU writes to VGA memory in any way.

-
An important point regarding reading VGA memory involves the VGA’s latches. (Remember that each of the four latches stores a byte for one plane; on CPU writes, the latches can provide some or all of the data written to display memory, allowing fast copying and efficient pixel masking.) Whenever the CPU reads a given address in VGA memory, each of the four latches is loaded with the contents of the byte at that address in its respective plane. Even though the CPU only receives data from one plane in read mode 0, all four planes are always read, and the values read are stored in the latches. This is true in read mode 1 as well. In short, whenever the CPU reads VGA memory in any read mode, all four planes are read and all four latches are always loaded. +
An important point regarding reading VGA memory involves the VGA’s latches. (Remember that each of the four latches stores a byte for one plane; on CPU writes, the latches can provide some or all of the data written to display memory, allowing fast copying and efficient pixel masking.) Whenever the CPU reads a given address in VGA memory, each of the four latches is loaded with the contents of the byte at that address in its respective plane. Even though the CPU only receives data from one plane in read mode 0, all four planes are always read, and the values read are stored in the latches. This is true in read mode 1 as well. In short, whenever the CPU reads VGA memory in any read mode, all four planes are read and all four latches are always loaded.

Read Mode 1

Read mode 0 is the workhorse read mode, but it’s got an annoying limitation: Whenever you want to determine the color of a given pixel in read mode 0, you have to perform four VGA memory reads, one for each plane, and then interpret the four bytes you’ve read as eight 16-color pixels. That’s a lot of programming. The code is also likely to run slowly, all the more so because a standard IBM VGA takes an average of 1.1 microseconds to complete each memory read, and read mode 0 requires four reads in order to read the four planes, not to mention the even greater amount of time taken by the OUTs required to switch between the planes. (1.1 microseconds may not sound like much, but on a 66-MHz 486, it’s 73 clock cycles! Local-bus VGAs can be a good deal faster, but a read from the fastest local-bus adapter I’ve yet seen would still cost in the neighborhood of 10 486/66 cycles.)

diff --git a/29-03.html b/29-03.html index 17f8025..d5840d0 100644 --- a/29-03.html +++ b/29-03.html @@ -37,7 +37,7 @@


-
While these requirements are no problem if you’re simply calling a subroutine in order to save an image from your program, they pose a considerable problem if you’re designing a hot-key operated TSR that can capture a screen image at any time. With the EGA specifically, there’s never any way to tell what state the registers are currently in, since the registers aren’t readable. (More on this issue later in this chapter.) As a result, any TSR that sets the Bit Mask to 0FFH, the Data Rotate register to 0, and so on runs the risk of interfering with the drawing code of the program that’s already running. +While these requirements are no problem if you’re simply calling a subroutine in order to save an image from your program, they pose a considerable problem if you’re designing a hot-key operated TSR that can capture a screen image at any time. With the EGA specifically, there’s never any way to tell what state the registers are currently in, since the registers aren’t readable. (More on this issue later in this chapter.) As a result, any TSR that sets the Bit Mask to 0FFH, the Data Rotate register to 0, and so on runs the risk of interfering with the drawing code of the program that’s already running.

What’s the solution? Frankly, the solution is to get VGA-specific. A TSR designed for the VGA can simply read out and save the state of the registers of interest, program those registers as needed, save the screen image, and restore the original settings. From a programmer’s perspective, readable registers are certainly near the top of the list of things to like about the VGA! The remaining installed base of EGAs is steadily dwindling, and you may be able to ignore it as a market today, as you couldn’t even a year or two ago.

@@ -51,7 +51,7 @@ DISPLAYED_SCREEN_SIZEequ(640/8)*480

Finally, be aware that the screen capture and restore programs in Listings 29.1 and 29.2 are only appropriate for EGA/VGA modes 0DH, 0EH, 0FH, 010H, and 012H, since they assume a fourconfiguration of EGA/VGA memory. In all text modes and in CGA graphics modes, and in VGA modes 11H and 13H as well, display memory can simply be written to disk and read back as a linear block of memory, just like a normal array.

While Listings 29.1 and 29.2 are written in assembly, the principles they illustrate apply equally well to high-level languages. In fact, there’s no need for any assembly at all when saving an EGA/VGA screen, as long as the high-level language you’re using can perform direct port I/O to set up the adapter and can read and write display memory directly.

-
One tip if you’re saving and restoring the screen from a high-level language on an EGA, though: After you’ve completed the save or restore operation, be sure to put any registers that you’ve changed back to their default settings. Some high-level languages (and the BIOS as well) assume that various registers are left in a certain state, so on the EGA it’s safest to leave the registers in their most likely state. On the VGA, of course, you can just read the registers out before you change them, then put them back the way you found them when you’re done. +
One tip if you’re saving and restoring the screen from a high-level language on an EGA, though: After you’ve completed the save or restore operation, be sure to put any registers that you’ve changed back to their default settings. Some high-level languages (and the BIOS as well) assume that various registers are left in a certain state, so on the EGA it’s safest to leave the registers in their most likely state. On the VGA, of course, you can just read the registers out before you change them, then put them back the way you found them when you’re done.

16 Colors out of 64

How does one produce the 64 colors from which the 16 colors displayed by the EGA can be chosen? The answer is simple enough: There’s a BIOS function that lets you select the mapping of the 16 possible pixel values to the 64 possible colors. Let’s lay out a bit of background before proceeding, however. diff --git a/29-05.html b/29-05.html index 2d16015..037c296 100644 --- a/29-05.html +++ b/29-05.html @@ -39,7 +39,7 @@

Overscan

While we’re at it, I’m going to touch on overscan. Overscan is the color of the border of the display, the rectangular area around the edge of the monitor that’s outside the region displaying active video data but inside the blanking area. The overscan (or border) color can be programmed to any of the 64 possible colors by either setting Attribute Controller register 11H directly or calling video function 10H, subfunction 1.

-
On ECD-compatible monitors, however, there’s too little scan time to display a proper border when the EGA is in 350-scan-line mode, so overscan should always be 0 (black) unless you’re in 200-scanmode. Note, though, that a VGA can easily display a border on a VGA-compatible monitor, and VGAs are in fact programmed at mode set for an 8-pixel-wide border in all modes; all you need do is set the overscan color on any VGA to see the border. +
On ECD-compatible monitors, however, there’s too little scan time to display a proper border when the EGA is in 350-scan-line mode, so overscan should always be 0 (black) unless you’re in 200-scanmode. Note, though, that a VGA can easily display a border on a VGA-compatible monitor, and VGAs are in fact programmed at mode set for an 8-pixel-wide border in all modes; all you need do is set the overscan color on any VGA to see the border.

A Bonus Blanker

An interesting bonus: The Attribute Controller provides a very convenient way to blank the screen, in the form of the aforementioned bit 5 of the Attribute Controller Index register (at address 3C0H after the Input Status 1 register—3DAH in color, 3BAH in monochrome—has been read and on every other write to 3C0H thereafter). Whenever bit 5 of the AC Index register is 0, video data is cut off, effectively blanking the screen. Setting bit 5 of the AC Index back to 1 restores video data immediately. Listing 29.4 illustrates this simple but effective form of screen blanking. diff --git a/30-03.html b/30-03.html index 049b136..d0dfe47 100644 --- a/30-03.html +++ b/30-03.html @@ -44,7 +44,7 @@

Setting the split-screen-related registers is not as simple a matter as merely outputting the right values to the right registers; timing is also important. The split screen start scan line value is checked against the number of each scan line as that scan line is displayed, which means that the split screen start scan line potentially takes effect the moment it is set. In other words, if the screen is displaying scan line 15 and you set the split screen start to 16, that change will be picked up immediately and the split screen will start after the next scan line. This is markedly different from changes to the start address, which take effect only at the start of the next frame.

The instantly-effective nature of the split screen is a bit of a problem, not because the changed screen appears as soon as the new split screen start scan line is set—that seems to me to be an advantage—but because the changed screen can appear before the new split screen start scan line is set.

-
Remember, the split screen start scan line is spread out over two or three registers. What if the incompletely-changed value matches the current scan line after you’ve set one register but before you’ve set the rest? For one frame, you’ll see the split screen in a wrong place—possibly a very wrong place—resulting in jumping and flicker. +
Remember, the split screen start scan line is spread out over two or three registers. What if the incompletely-changed value matches the current scan line after you’ve set one register but before you’ve set the rest? For one frame, you’ll see the split screen in a wrong place—possibly a very wrong place—resulting in jumping and flicker.

The solution is simple: Set the split screen start scan line at a time when it can’t possibly match the currently displayed scan line. The easy way to do that is to set it when there isn’t any currently displayed scan line—during vertical non-display time. One safe time that’s easy to find is the start of the vertical sync pulse, which is typically pretty near the middle of vertical non-display time, and that’s the approach I’ve followed in Listing 30.1. I’ve also disabled interrupts during the period when the split screen registers are being set. This isn’t absolutely necessary, but if it’s not done, there’s the possibility that an interrupt will occur between register sets and delay the later register sets until display time, again causing flicker.

@@ -54,7 +54,7 @@

The bug is this: The first scan line of the EGA split screen—the scan line starting at offset zero in display memory—is displayed not once but twice. In other words, the first line of split screen display memory, and only the first line, is replicated one unnecessary time, pushing all the other lines down by one.

That’s not a fatal bug, of course. In fact, if the first few scan lines are identical, it’s not even noticeable. The EGA’s split-screen bug can produce visible distortion given certain patterns, however, so you should try to make the top few lines identical (if possible) when designing split-screen images that might be displayed on EGAs, and you should in any case check how your split-screens look on both VGAs and EGAs.

-
I have an important caution here: Don’t count on the EGA’s split-screen bug; that is, don’t rely on the first scan line being doubled when you design your split screens. IBM designed and made the original EGA, but a lot of companies cloned it, and there’s no guarantee that all EGA clones copy the bug. It is a certainty, at least, that the VGA didn’t copy it. +
I have an important caution here: Don’t count on the EGA’s split-screen bug; that is, don’t rely on the first scan line being doubled when you design your split screens. IBM designed and made the original EGA, but a lot of companies cloned it, and there’s no guarantee that all EGA clones copy the bug. It is a certainty, at least, that the VGA didn’t copy it.

There’s another respect in which the EGA is inferior to the VGA when it comes to the split screen, and that’s in the area of panning when the split screen is on. This isn’t a bug—it’s just one of the many areas in which the VGA’s designers learned from the shortcomings of the EGA and went the EGA one better.


diff --git a/30-04.html b/30-04.html index d5eb6e8..6be2218 100644 --- a/30-04.html +++ b/30-04.html @@ -41,7 +41,7 @@

Horizontal smooth panning works just fine, although I’ve always harbored some doubts that any one horizontal-smooth-panning approach works properly on all display board clones. (More on this later.) There’s a catch when using horizontal smooth panning with the split screen up, though, and it’s a serious catch: You can’t byte-pan the split screen (which always starts at offset zero, no matter what the setting of the start address registers)—but you can pel-pan the split screen.

Put another way, when the normal portion of the screen is horizontally smooth-panned, the split screen portion moves a pixel at a time until it’s time to move to the next byte, then jumps back to the start of the current byte. As the top part of the screen moves smoothly about, the split screen will move and jump, move and jump, over and over. Believe me, it’s not a pretty sight.

-
What’s to be done? On the EGA, nothing. Unless you’re willing to have your users’ eyes doing the jitterbug, don’t use horizontal smooth scrolling while the split screen is up. Byte panning is fine—just don’t change the Pel Panning register from its default setting. +
What’s to be done? On the EGA, nothing. Unless you’re willing to have your users’ eyes doing the jitterbug, don’t use horizontal smooth scrolling while the split screen is up. Byte panning is fine—just don’t change the Pel Panning register from its default setting.

On the VGA, there is recourse. A VGA-only bit, bit 5 of the AC Mode Control register (AC register 10H), turns off pel panning in the split screen. In other words, when this bit is set to 1, pel panning is reset to zero before the first line of the split screen, and remains zero until the end of the frame. This doesn’t allow you to pan the split screen horizontally, mind you—there’s no way to do that—but it does let you pan the normal screen while the split screen stays rock-solid. This can be used to produce an attractive “streaming tape” effect in the normal screen while the split screen is used to display non-moving information.

diff --git a/30-06.html b/30-06.html index 4d540de..7b4e7c9 100644 --- a/30-06.html +++ b/30-06.html @@ -44,7 +44,7 @@

You see, the start address is loaded into the EGA’s or VGA’s internal display memory pointer once per frame. The internal pointer is then advanced, byte-by-byte and line-by-line, until the end of the frame (with a possible resetting to zero if the split screen line is reached), and is then reloaded for the next frame. That’s straightforward enough; the real question is, Exactly when is the start address loaded?

In his excellent book Programmer’s Guide to PC Video Systems (Microsoft Press) Richard Wilton says that the start address is loaded at the start of the vertical sync pulse. (Wilton calls it vertical retrace, which can also be taken to mean vertical non-display time, but given that he’s testing the vertical sync status bit in the Input Status 0 register, I assume he means that the start address is loaded at the start of vertical sync.) Consequently, he waits until the end of the vertical sync pulse to set the start address registers, confident that the start address won’t take effect until the next frame.

I’m sure Richard is right when it comes to the real McCoy IBM VGA and EGA, but I’m less confident that every clone out there loads the start address at the start of vertical sync.

-
For that very reason, I generally advise people not to use horizontal smooth panning unless they can test their software on all the makes of display adapter it might run on. I’ve used Richard’s approach in Listings 30.1 and 30.2, and so far as I’ve seen it works fine, but be aware that there are potential, albeit unproven, hazards to relying on the setting of the start address registers to occur at a specific time in the frame. +
For that very reason, I generally advise people not to use horizontal smooth panning unless they can test their software on all the makes of display adapter it might run on. I’ve used Richard’s approach in Listings 30.1 and 30.2, and so far as I’ve seen it works fine, but be aware that there are potential, albeit unproven, hazards to relying on the setting of the start address registers to occur at a specific time in the frame.

The interaction of the start address registers and the Pel Panning register is worthy of note. After waiting for the end of vertical sync to set the start address in Listing 30.2, I wait for the start of the next vertical sync to set the Pel Panning register. That’s because the start address doesn’t take effect until the start of the next frame, but the pel panning setting takes effect at the start of the next line; if we set the pel panning at the same time we set the start address, we’d get a whole frame with the old start address and the new pel panning settings mixed together, causing the screen to jump. As with the split screen registers, it’s safest to set the Pel Panning register during non-display time. For maximum reliability, we’d have interrupts off from the time we set the start address registers to the time we change the pel planning setting, to make sure an interrupt doesn’t come in and cause us to miss the start of a vertical sync and thus get a mismatched pel panning/start address pair for a frame, although for modularity I haven’t done this in Listing 30.2. (Also, doing so would require disabling interrupts for much too long a time.)

What if you wanted to pan faster? Well, you could of course just move two pixels at a time rather than one; I assure you no one will ever notice when you’re panning at a rate of 10 or more times per second.


diff --git a/32-03.html b/32-03.html index 799d0ac..4743cb2 100644 --- a/32-03.html +++ b/32-03.html @@ -243,7 +243,7 @@ void main()

The first thing you’ll notice when you run this code is that the speed of 360×480 256-color mode is pretty good, especially considering that most of the program is im-plemented in C.

-
Drawing in 360×480 256-color mode can sometimes actually be faster than in the 16-color modes, because the byte-per-pixel display memory organization of 256-color mode eliminates the need to read display memory before writing to it in order to isolate individual pixels coexisting within a single byte. In addition, 360×480 256-color mode is a variant of Mode X, which we’ll encounter in detail in Chapter 47, and supports all the high-performance features of Mode X. +
Drawing in 360×480 256-color mode can sometimes actually be faster than in the 16-color modes, because the byte-per-pixel display memory organization of 256-color mode eliminates the need to read display memory before writing to it in order to isolate individual pixels coexisting within a single byte. In addition, 360×480 256-color mode is a variant of Mode X, which we’ll encounter in detail in Chapter 47, and supports all the high-performance features of Mode X.


diff --git a/34-02.html b/34-02.html index 8466fc3..f777429 100644 --- a/34-02.html +++ b/34-02.html @@ -40,7 +40,7 @@

The DAC can be loaded either directly or through subfunctions 10H (for a single DAC register) or 12H (for a block of DAC registers) of the BIOS video service interrupt 10H, function 10H, described in Chapter 33. For cycling the contents of the entire DAC, the block-load function (invoked by executing INT 10H with AH = 10H and AL = 12H to load a block of CX DAC locations, starting at location BX, from the block of RGB triplets—3 bytes per triplet—starting at ES:DX into the DAC) would be the better of the two, due to the considerably greater efficiency of calling the BIOS once rather than 256 times. At any rate, we’d like to use one or the other of the BIOS functions for color cycling, because we know that whenever possible, one should use a BIOS function in preference to accessing hardware directly, in the interests of avoiding compatibility problems. In the case of color cycling, however, it is emphatically not possible to use either of the BIOS functions, for they have problems. Serious problems.

The difficulty is this: IBM’s BIOS specification describes exactly how the parameters passed to the BIOS control the loading of DAC locations, and all clone BIOSes meet that specification scrupulously, which is to say that if you invoke INT 10H, function 10H, subfunction 12H with a given set of parameters, you can be sure that you will end up with the same values loaded into the same DAC locations on all VGAs from all vendors. IBM’s spec does not, however, describe whether vertical retrace should be waited for before loading the DAC, nor does it mention whether video should be left enabled while loading the DAC, leaving cloners to choose whatever approach they desire—and, alas, every VGA cloner seems to have selected a different approach.

I tested four clone VGAs from different manufacturers, some in a 20 MHz 386 machine and some in a 10 MHz 286 machine. Two of the four waited for vertical retrace before loading the DAC; two didn’t. Two of the four blanked the display while loading the DAC, resulting in flickering bars across the screen. One showed speckled pixels spattered across the top of the screen while the DAC was being loaded. Also, not one was able to load all 256 DAC locations without showing some sort of garbage on the screen for at least one frame, but that’s not the BIOS’s fault; it’s a problem endemic to the VGA.

-
These findings lead me inexorably to the conclusion that the BIOS should not be used to load the DAC dynamically. That is, if you’re loading the DAC just once in preparation for a graphics session—sort of a DAC mode set—by all means load by way of the BIOS. No one will care that some garbage is displayed for a single frame; heck, I have boards that bounce and flicker and show garbage every time I do a mode set, and the amount of garbage produced by loading the DAC once is far less noticeable. If, however, you intend to load the DAC repeatedly for color cycling, avoid the BIOS DAC load functions like the plague. They will bring you only heartache. +
These findings lead me inexorably to the conclusion that the BIOS should not be used to load the DAC dynamically. That is, if you’re loading the DAC just once in preparation for a graphics session—sort of a DAC mode set—by all means load by way of the BIOS. No one will care that some garbage is displayed for a single frame; heck, I have boards that bounce and flicker and show garbage every time I do a mode set, and the amount of garbage produced by loading the DAC once is far less noticeable. If, however, you intend to load the DAC repeatedly for color cycling, avoid the BIOS DAC load functions like the plague. They will bring you only heartache.

As but one example of the unsuitability of the BIOS DAC-loading functions for color cycling, imagine that you want to cycle all 256 colors 70 times a second, which is once per frame. In order to accomplish that, you would normally wait for the start of the vertical sync signal (marking the end of the frame), then call the BIOS to load the DAC. On some boards—boards with BIOSes that don’t wait for vertical sync before loading the DAC—that will work pretty well; you will, in fact, load the DAC once a frame. On other boards, however, it will work very poorly indeed; your program will wait for the start of vertical sync, and then the BIOS will wait for the start of the next vertical sync, with the result being that the DAC gets loaded only once every two frames. Sadly, there’s no way, short of actually profiling the performance of BIOS DAC loads, for you to know which sort of BIOS is installed in a particular computer, so unless you can always control the brand of VGA your software will run on, you really can’t afford to color cycle by calling the BIOS.

Which is not to say that loading the DAC directly is a picnic either, as we’ll see next.

diff --git a/34-04.html b/34-04.html index 9aa4081..adea73f 100644 --- a/34-04.html +++ b/34-04.html @@ -51,7 +51,7 @@

First of all, I’d like to point out that when color cycling does work, it’s a thing of beauty. Assemble Listing 34.1 so that it doesn’t use the BIOS to load the DAC, doesn’t guard against interrupts, and uses 286-specific instructions if your computer supports them. Then tinker with CYCLE_SIZE until the color cycling is perfectly clean on your computer. Color cycling looks stunningly smooth, doesn’t it? And this is crude color cycling, working with the default color set; switch over to a color set that gradually works its way through various hues and saturations, and you could get something that looks for all the world like true-color animation (albeit working with a small subset of the full spectrum at any one time).

Given that, how can we take advantage of color cycling within the limitations of loading the DAC? The simplest approach, and my personal favorite, is that of cycling a portion of the DAC while using the rest of the DAC locations for other, non-cycling purposes. For example, you might allocate 32 DAC locations to the aforementioned sunset, reserve 160 additional locations for use in drawing a static mountain scene, and employ the remaining 64 locations to draw images of planes, cars, and the like in the foreground. The 32 sunset colors could be cycled cleanly, and the other 224 colors would remain the same throughout the program, or would change only occasionally.

That suggests a second possibility: If you have several different color sets to be cycled, interleave the loading so that only one color set is cycled per frame. Suppose you are animating a night scene, with stars twinkling in the background, meteors streaking across the sky, and a spaceship moving across the screen with its jets flaring. One way to produce most of the necessary effects with little effort would be to draw the stars in several attributes and then cycle the colors for those attributes, draw the meteor paths in successive attributes, one for each pixel, and then cycle the colors for those attributes, and do much the same for the jets. The only remaining task would be to animate the spaceship across the screen, which is not a particularly difficult task.

-
The key to getting all the color cycling to work in the above example, however, would be to assign each color cycling task a different part of the DAC, with each part cycled independently as needed. If, as is likely, the total number of DAC locations cycled proved to be too great to manage in one frame, you could simply cycle the colors of the stars after one frame, the colors of the meteors after the next, and the colors of the jets after yet another frame, then back around to cycling the colors of the stars. By splitting up the DAC in this manner and interleaving the cycling tasks, you can perform a great deal of seemingly complex color animation without loading very much of the DAC during any one frame. +
The key to getting all the color cycling to work in the above example, however, would be to assign each color cycling task a different part of the DAC, with each part cycled independently as needed. If, as is likely, the total number of DAC locations cycled proved to be too great to manage in one frame, you could simply cycle the colors of the stars after one frame, the colors of the meteors after the next, and the colors of the jets after yet another frame, then back around to cycling the colors of the stars. By splitting up the DAC in this manner and interleaving the cycling tasks, you can perform a great deal of seemingly complex color animation without loading very much of the DAC during any one frame.


diff --git a/35-04.html b/35-04.html index 83284db..36136d7 100644 --- a/35-04.html +++ b/35-04.html @@ -127,7 +127,7 @@ void main()

Looking at EVGALine

The EVGALine function itself performs four operations. EVGALine first sets up the VGA’s hardware so that all pixels drawn will be in the desired color. This is accomplished by setting two of the VGA’s registers, the Enable Set/Reset register and the Set/Reset register. Setting the Enable Set/Reset to the value 0FH, as is done in EVGALine, causes all drawing to produce pixels in the color contained in the Set/Reset register. Setting the Set/Reset register to the passed color, in conjunction with the Enable Set/Reset setting of 0FH, causes all drawing done by EVGALine and the functions it calls to generate the passed color. In summary, setting up the Enable Set/Reset and Set/Reset registers in this way causes the remainder of EVGALine to draw a line in the specified color.

EVGALine next performs a simple check to cut in half the number of line orientations that must be handled separately. Figure 35.4 shows the eight possible line orientations among which a Bresenham’s algorithm implementation must distinguish. (In interpreting Figure 35.4, assume that lines radiate outward from the center of the figure, falling into one of eight octants delineated by the horizontal and vertical axes and the two diagonals.) The need to categorize lines into these octants falls out of the major/minor axis nature of the algorithm; the orientations are distinguished by which coordinate forms the major axis and by whether each of X and Y increases or decreases from the line start to the line end.

-
A moment of thought will show, however, that four of the line orientations are redundant. Each of the four orientations for which DeltaY, the Y component of the line, is less than 0 (that is, for which the line start Y coordinate is greater than the line end Y coordinate) can be transformed into one of the four orientations for which the line start Y coordinate is less than the line end Y coordinate simply by reversing the line start and end coordinates, so that the line is drawn in the other direction. EVGALine does this by swapping (X0,Y0) (the line start coordinates) with (X1,Y1) (the line end coordinates) whenever Y0 is greater than Y1. +
A moment of thought will show, however, that four of the line orientations are redundant. Each of the four orientations for which DeltaY, the Y component of the line, is less than 0 (that is, for which the line start Y coordinate is greater than the line end Y coordinate) can be transformed into one of the four orientations for which the line start Y coordinate is less than the line end Y coordinate simply by reversing the line start and end coordinates, so that the line is drawn in the other direction. EVGALine does this by swapping (X0,Y0) (the line start coordinates) with (X1,Y1) (the line end coordinates) whenever Y0 is greater than Y1.

This accomplished, EVGALine must still distinguish among the four remaining line orientations. Those four orientations form two major categories, orientations for which the X dimension is the major axis of the line and orientations for which the Y dimension is the major axis. As shown in Figure 35.4, octants 1 (where X increases from start to finish) and 2 (where X decreases from start to finish) fall into the latter category, and differ in only one respect, the direction in which the X coordinate moves when it changes. Handling of the running error of the line is exactly the same for both cases, as one would expect given the symmetry of lines differing only in the sign of DeltaX, the X coordinate of the line. Consequently, for those cases where DeltaX is less than zero, the direction of X movement is made negative, and the absolute value of DeltaX is used for error term calculations.

Similarly, octants 0 (where X increases from start to finish) and 3 (where X decreases from start to finish) differ only in the direction in which the X coordinate moves when it changes. The difference between line drawing in octants 0 and 3 and line drawing in octants 1 and 2 is that in octants 0 and 3, since X is the major axis, the X coordinate changes on every pixel of the line and the Y coordinate changes only when the running error of the line dictates. In octants 1 and 2, the Y coordinate changes on every pixel and the X coordinate changes only when the running error dictates, since Y is the major axis.

diff --git a/37-02.html b/37-02.html index 9b95b8b..a982629 100644 --- a/37-02.html +++ b/37-02.html @@ -41,7 +41,7 @@

When I went to speed up run-length slice lines, I initially manually converted the last chapter’s C code into assembly. Then I streamlined the register usage and used REP STOS wherever possible. Listing 37.1 is that code. At that point, line drawing was surely faster, although I didn’t know exactly how much faster. Equally surely, there were significant optimizations yet to be made, and I was itching to get on to them, for they were bound to be a lot more interesting than a basic C-to-assembly port.

Ego intervened at this point, however. I wanted to know how much of a speed-up I had already gotten, so I timed the performance of the C code and compared it to the assembly code. To my horror, I found that I had not gotten even a two-times improvement! I couldn’t understand how that could be—the C code was decidedly unoptimized—until I hit on the idea of measuring the maximum memory speed of the VGA to which I was drawing.

Bingo. The Paradise VGA in my 486/33 is fast for a single display-memory write, because it buffers the data, lets the CPU go on its merry way, and finishes the write when display memory is ready. However, the maximum rate at which data can be written to the adapter turns out to be no more than one byte every microsecond. Put another way, you can only write one byte to this adapter every 33 clock cycles on a 486/33. Therefore, no matter how fast I made the line-drawing code, it could never draw more than 1,000,000 pixels per second in 256-color mode in my system. The C code was already drawing at about half that rate, so the potential speed-up for the assembly code was limited to a maximum of two times, which is pretty close to what Listing 37.1 did, in fact, achieve. When I compared the C and assembly implementations drawing to normal system (nondisplay) memory, I found that the assembly code was actually four times as fast as the C code.

-
In fact, Listing 37.1 draws VGA lines at about 92 percent of the maximum possible rate in my system—that is, it draws very nearly as fast as the VGA hardware will allow. All the optimization in the world would get me less than 10 percent faster line drawing—and only if I eliminated all overhead, an unlikely proposition at best. The code isn’t fully optimized, but so what? +
In fact, Listing 37.1 draws VGA lines at about 92 percent of the maximum possible rate in my system—that is, it draws very nearly as fast as the VGA hardware will allow. All the optimization in the world would get me less than 10 percent faster line drawing—and only if I eliminated all overhead, an unlikely proposition at best. The code isn’t fully optimized, but so what?

Now it’s true that faster line-drawing code would likely be more beneficial on faster VGAs, especially local-bus VGAs, and in slower systems. For that reason, I’ll list a variety of potential optimizations to Listing 37.1. On the other hand, it’s also true that Listing 37.1 is capable of drawing lines at a rate of 2.2 million pixels per second on a 486/ 33, given fast enough VGA memory, so it should be able to drive almost any non-local-bus VGA at nearly full speed. In short, Listing 37.1 is very fast, and, in many systems, further optimization is basically a waste of time.

diff --git a/38-02.html b/38-02.html index dd6fdca..ece32e4 100644 --- a/38-02.html +++ b/38-02.html @@ -49,7 +49,7 @@

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.

-
This sort of non-overlapping polygon filling isn’t ideal for all purposes. Polygons are skewed toward the top and left edges, which not only introduces drawing error relative to the ideal polygon but also means that a filled polygon won’t match the same polygon drawn unfilled. Narrow wedges and one-pixel-wide polygons will show up spottily. All in all, the choice of polygon-filling approach depends entirely on the ways in which the filled polygons must be used. +
This sort of non-overlapping polygon filling isn’t ideal for all purposes. Polygons are skewed toward the top and left edges, which not only introduces drawing error relative to the ideal polygon but also means that a filled polygon won’t match the same polygon drawn unfilled. Narrow wedges and one-pixel-wide polygons will show up spottily. All in all, the choice of polygon-filling approach depends entirely on the ways in which the filled polygons must be used.

For our purposes, nonoverlapping polygons are the way to go, so let’s have at them.

diff --git a/39-02.html b/39-02.html index 0879e81..cc6876d 100644 --- a/39-02.html +++ b/39-02.html @@ -153,7 +153,7 @@ void DrawHorizontalLineList(struct HLineList * HLineListPtr,

There’s no secret as to why last chapter’s ScanEdge was so slow: It used floating point calculations. One secret of fast graphics is using integer or fixed-point calculations, instead. (Sure, the floating point code would run faster if a math coprocessor were installed, but it would still be slower than the alternatives; besides, why require a math coprocessor when you don’t have to?) Both integer and fixed-point calculations are fast. In many cases, fixed-point is faster, but integer calculations have one tremendous virtue: They’re completely accurate. The tiny imprecision inherent in either fixed or floating-point calculations can result in occasional pixels being one position off from their proper location. This is no great tragedy, but after going to so much trouble to ensure that polygons don’t overlap at common edges, why not get it exactly right?

In fact, when I tested out the integer edge tracing code by comparing an integer-based test image to one produced by floating-point calculations, two pixels out of the whole screen differed, leading me to suspect a bug in the integer code. It turned out, however, that’s in those two cases, the floating point results were sufficiently imprecise to creep from just under an integer value to just over it, so that the ceil function returned a coordinate that was one too large.

-
Floating point is very accurate—but it is not precise. Integer calculations, properly performed, are. +Floating point is very accurate—but it is not precise. Integer calculations, properly performed, are.


diff --git a/39-04.html b/39-04.html index ffeb29d..3a7de49 100644 --- a/39-04.html +++ b/39-04.html @@ -38,7 +38,7 @@


And indeed it does. When the test program is modified to draw to a local buffer, both the C and assembly language versions get 0.29 seconds faster, that being a measure of the time taken by display memory wait states. With those wait states factored out, the assembly language version of DrawHorizontalLineList becomes almost three times as fast as the C code.

-
There is a lesson here. An optimization has no fixed payoff; its value fluctuates according to the context in which it is used. There’s relatively little benefit to further optimizing code that already spends half its time waiting for display memory; no matter how good your optimizations, you’ll get only a two-times speedup at best, and generally much less than that. There is, on the other hand, potential for tremendous improvement when drawing to system memory, so if that’s where most of your drawing will occur, optimizations such as Listing 39.3 are well worth the effort. +There is a lesson here. An optimization has no fixed payoff; its value fluctuates according to the context in which it is used. There’s relatively little benefit to further optimizing code that already spends half its time waiting for display memory; no matter how good your optimizations, you’ll get only a two-times speedup at best, and generally much less than that. There is, on the other hand, potential for tremendous improvement when drawing to system memory, so if that’s where most of your drawing will occur, optimizations such as Listing 39.3 are well worth the effort.
Know the environments in which your code will run, and know where the cycles go in those environments. diff --git a/40-05.html b/40-05.html index 4a10c6c..c04c800 100644 --- a/40-05.html +++ b/40-05.html @@ -38,7 +38,7 @@


An insertion sort that scans backward through the AET from the current edge rather than forward from the start of the AET could be quite a bit faster, because edges rarely move more than one or two positions through the AET. However, scanning backward requires a doubly linked list, rather than the singly linked list used in Listing 40.1. I’ve chosen to use a singly linked list partly to minimize memory requirements (double-linking requires an extra pointer field) and partly because supporting back links would complicate the code a good bit. The main reason, though, is that the potential rewards for the complications of back links and insertion sorting aren’t great enough; profiling a variety of polygons reveals that less than ten percent of total time is spent sorting the AET.

-
The potential 1 to 5 percent speedup gained by optimizing AET sorting just isn’t worth it in any but the most demanding application—a good example of the need to keep an overall perspective when comparing the theoretical characteristics of various approaches. +
The potential 1 to 5 percent speedup gained by optimizing AET sorting just isn’t worth it in any but the most demanding application—a good example of the need to keep an overall perspective when comparing the theoretical characteristics of various approaches.

Nonconvex Polygons

Nonconvex polygons can be filled somewhat faster than complex polygons. Because edges never cross or switch positions with other edges once they’re in the AET, the AET for a nonconvex polygon needs to be sorted only when new edges are added. In order for this to work, though, edges must be added to the AET in strict left-to-right order. Complications arise when dealing with two edges that start at the same point, because slopes must be compared to determine which edge is leftmost. This is certainly doable, but because of space limitations and limited performance returns, I haven’t implemented this in Listing 40.1. diff --git a/41-01.html b/41-01.html index f0bbe81..aa2c3df 100644 --- a/41-01.html +++ b/41-01.html @@ -42,7 +42,7 @@

After I wrote the columns on polygons in Dr. Dobb’s Journal that became Chapters 38–40, long-time reader Bill Huber wrote to take me to task—and a well-deserved kick in the fanny it was, I might add—for my use of non-standard polygon terminology in those columns. Unix’s X-Window System (XWS) defines three categories of polygons: complex, nonconvex, and convex. These three categories, each a specialized subset of the preceding category, not-so-coincidentally map quite nicely to three increasingly fast polygon filling techniques. Therefore, I used the XWS names to describe the sorts of polygons that can be drawn with each of the polygon filling techniques.

The problem is that those names don’t accurately describe all the sorts of polygons that the techniques are capable of drawing. Convex polygons are those for which no interior angle is greater than 180 degrees. The “convex” drawing approach described in the previous few chapters actually handles a number of polygons that are not convex; in fact, it can draw any polygon through which no horizontal line can be drawn that intersects the boundary more than twice. (In other words, the boundary reverses the Y direction exactly twice, disregarding polygons that have degenerated into horizontal lines, which I’m going to ignore.)

Bill was kind enough to send me the pages out of Computational Geometry, An Introduction (Springer-Verlag, 1988) that describe the correct terminology; such polygons are, in fact, “monotone with respect to a vertical line” (which unfortunately makes a rather long #define variable). Actually, to be a tad more precise, I’d call them “monotone with respect to a vertical line and simple,” where “simple” means “not self-intersecting.” Similarly, the polygon type I called “nonconvex” is actually “simple,” and I suppose what I called “complex” should be referred to as “nonsimple,” or maybe just “none of the above.”

-
This may seem like nit-picking, but actually, it isn’t; what it’s really about is the tremendous importance of having a shared language. In one of his books, Richard Feynman describes having developed his own mathematical framework, complete with his own notation and terminology, in high school. When he got to college and started working with other people who were at his level, he suddenly understood that people can’t share ideas effectively unless they speak the same language; otherwise, they waste a great deal of time on misunderstandings and explanation. +
This may seem like nit-picking, but actually, it isn’t; what it’s really about is the tremendous importance of having a shared language. In one of his books, Richard Feynman describes having developed his own mathematical framework, complete with his own notation and terminology, in high school. When he got to college and started working with other people who were at his level, he suddenly understood that people can’t share ideas effectively unless they speak the same language; otherwise, they waste a great deal of time on misunderstandings and explanation.

Or, as Bill Huber put it, “You are free to adopt your own terminology when it suits your purposes well. But you risk losing or confusing those who could be among your most astute readers—those who already have been trained in the same or a related field.” Ditto. Likewise. D’accord. And mea culpa ; I shall endeavor to watch my language in the future.

Nomenclature in Action

diff --git a/42-05.html b/42-05.html index c97c2e5..1443fb0 100644 --- a/42-05.html +++ b/42-05.html @@ -312,7 +312,7 @@ _DrawWuLine endp

Notes on Wu Antialiasing

Wu antialiasing can be applied to any curve for which it’s possible to calculate at each step the positions and intensities of two bracketing pixels, although the implementation will generally be nowhere near as efficient as it is for lines. However, Wu’s article in Computer Graphics does describe an efficient algorithm for drawing antialiased circles. Wu also describes a technique for antialiasing solids, such as filled circles and polygons. Wu’s approach biases the edges of filled objects outward. Although this is no good for adjacent polygons of the sort used in rendering, it’s certainly possible to design a more accurate polygon-antialiasing approach around Wu’s basic weighting technique. The results would not be quite so good as more sophisticated antialiasing techniques, but they would be much faster.

-
In general, the results obtained by Wu antialiasing are only so-so, by theoretical measures. Wu antialiasing amounts to a simple box filter placed over a fixed-point step approximation of a line, and that process introduces a good deal of deviation from the ideal. On the other hand, Wu notes that even a 10 percent error in intensity doesn’t lead to noticeable loss of image quality, and for Wu-antialiased lines up to 1K pixels in length, the error is under 10 percent. If it looks good, it is good—and it looks good. +
In general, the results obtained by Wu antialiasing are only so-so, by theoretical measures. Wu antialiasing amounts to a simple box filter placed over a fixed-point step approximation of a line, and that process introduces a good deal of deviation from the ideal. On the other hand, Wu notes that even a 10 percent error in intensity doesn’t lead to noticeable loss of image quality, and for Wu-antialiased lines up to 1K pixels in length, the error is under 10 percent. If it looks good, it is good—and it looks good.

With a 16-bit error accumulator, fixed-point inaccuracy becomes a problem for Wu-antialiased lines longer than 1K. For such lines, you should switch to using 32-bit error values, which would let you handle lines of any practical length.

diff --git a/44-04.html b/44-04.html index 1a0c857..5c52491 100644 --- a/44-04.html +++ b/44-04.html @@ -75,7 +75,7 @@ mov byte ptr es:[di],0ffh -
If you’re familiar with VGA programming, you’re no doubt aware that everything that can be done with write mode 3 can also be accomplished in write mode 0 or write mode 2 by using the Bit Mask register. However, setting the Bit Mask register requires at least one OUT per byte written, in addition to the read and write of display memory, and OUTs are often slower than display memory accesses, especially on 386s and 486s. One of the great virtues of write mode 3 is that it requires virtually no OUTs and is therefore substantially faster for masking than the other write modes. +If you’re familiar with VGA programming, you’re no doubt aware that everything that can be done with write mode 3 can also be accomplished in write mode 0 or write mode 2 by using the Bit Mask register. However, setting the Bit Mask register requires at least one OUT per byte written, in addition to the read and write of display memory, and OUTs are often slower than display memory accesses, especially on 386s and 486s. One of the great virtues of write mode 3 is that it requires virtually no OUTs and is therefore substantially faster for masking than the other write modes.

In short, write mode 3 is a good choice for single-color drawing that modifies individual pixels within display memory bytes. Not coincidentally, the sample application draws only single-color objects within the animation area; this allows write mode 3 to be used for all drawing, in keeping with our desire for speedy screen updates.

diff --git a/44-05.html b/44-05.html index 8023d8e..24dbf1d 100644 --- a/44-05.html +++ b/44-05.html @@ -38,7 +38,7 @@


Given the above assumptions, drawing text is easy; we simply copy each byte of each character to the appropriate location in display memory, and voila, we’re done. Text copying is done in write mode 0, in which the byte written to display memory is copied to all four planes at once; hence, 1-bits turn into white (color value 0FH, with 1-bits in all four planes), and 0-bits turn into black (color value 0). This is faster than using write mode 3 because write mode 3 requires a read/write of display memory (or at least preloading the latches with the background color), while the write mode 0 approach requires only a write to display memory.

-
Is write mode 0 always the best way to do text? Not at all. The write mode 0 approach described above draws both foreground and background pixels within the character box, forcing the background pixels to black at the same time that it forces the foreground pixels to white. If you want to draw transparent text (that is, draw only the character pixels, not the surrounding background box), write mode 3 is ideal. Also, matters get far more complicated if characters that aren’t 8 pixels wide are drawn, or if characters are drawn starting at arbitrary pixel locations, without the multiple-of-8 column restriction, so that rotation and masking are required. Lastly, the Map Mask register can be used to draw text in colors other than white—but only if the background is black. Otherwise, the data remaining in the planes protected by the Map Mask will remain and can interfere with the colors of the text being drawn. +Is write mode 0 always the best way to do text? Not at all. The write mode 0 approach described above draws both foreground and background pixels within the character box, forcing the background pixels to black at the same time that it forces the foreground pixels to white. If you want to draw transparent text (that is, draw only the character pixels, not the surrounding background box), write mode 3 is ideal. Also, matters get far more complicated if characters that aren’t 8 pixels wide are drawn, or if characters are drawn starting at arbitrary pixel locations, without the multiple-of-8 column restriction, so that rotation and masking are required. Lastly, the Map Mask register can be used to draw text in colors other than white—but only if the background is black. Otherwise, the data remaining in the planes protected by the Map Mask will remain and can interfere with the colors of the text being drawn.

I’m not going to delve any deeper into the considerable issues of drawing VGA text; I just want to sensitize you to the existence of approaches other than the ones used in Listings 44.1 and 44.2. On the VGA, the rule is: If there’s something you want to do, there probably are 10 ways to do it, each with unique strengths and weaknesses. Your mission, should you decide to accept it, is to figure out which one is best for your particular application.

diff --git a/45-02.html b/45-02.html index d544bea..97c7f51 100644 --- a/45-02.html +++ b/45-02.html @@ -85,7 +85,7 @@

-
OUTs, in general, are lousy on the 486 (and to think they only took three cycles on the 286!). OUTs to VGAs are particularly lousy. Display memory performance is pretty poor, especially for reads. The conclusions are obvious, I would hope. Structure your graphics code, and, in general, all 486 code, to avoid OUTs. +OUTs, in general, are lousy on the 486 (and to think they only took three cycles on the 286!). OUTs to VGAs are particularly lousy. Display memory performance is pretty poor, especially for reads. The conclusions are obvious, I would hope. Structure your graphics code, and, in general, all 486 code, to avoid OUTs.

For graphics, this especially means using write mode 3 rather than the bit-mask register. When you must use the bit mask, arrange drawing so that you can set the bit mask once, then do a lot of drawing with that mask. For example, draw a whole edge at once, then the middle, then the other edge, rather than setting the bit mask several times on each scan line to draw the edge and middle bytes together. Don’t read from display memory if you don’t have to. Write each pixel once and only once.

diff --git a/45-04.html b/45-04.html index 274e698..2fd1c3c 100644 --- a/45-04.html +++ b/45-04.html @@ -38,7 +38,7 @@


One point I’d like to make is that although the system-memory buffer in Listing 45.1 has exactly the same dimensions as the screen bitmap, that’s not a requirement, and there are some good reasons not to make the two the same size. For example, if the system buffer is bigger than the area displayed on the screen, it’s possible to pan the visible area around the system buffer. Or, alternatively, the system buffer can be just the size of a desired window, representing a window into a larger, virtual buffer. We could then draw the desired portion of the virtual bitmap into the system-memory buffer, then copy the buffer to the screen, and the effect will be of having panned the window to the new location.

-
Another argument in favor of a small viewing window is that it restricts the amount of display memory actually drawn to. Restricting the display memory used for animation reduces the total number of display-memory accesses, which in turn boosts overall performance; it also improves the performance and appearance of panning, in which the whole window has to be redrawn or copied. +
Another argument in favor of a small viewing window is that it restricts the amount of display memory actually drawn to. Restricting the display memory used for animation reduces the total number of display-memory accesses, which in turn boosts overall performance; it also improves the performance and appearance of panning, in which the whole window has to be redrawn or copied.

If you keep a close watch, you’ll notice that many high-performance animation games similarly restrict their full-featured animation area to a relatively small region. Often, it’s hard to tell that this is the case, because the animation region is surrounded by flashy digitized graphics and by items such as scoreboards and status screens, but look closely and see if the animation region in your favorite game isn’t smaller than you thought.

diff --git a/47-05.html b/47-05.html index 3362d9e..20e193c 100644 --- a/47-05.html +++ b/47-05.html @@ -37,7 +37,7 @@


-
In general, performing plane-at-a-time operations can make almost any Mode X operation, at the worst, nearly as fast as the same operation in mode 13H (although this sort of Mode X programming is admittedly fairly complex). In this pursuit, it can help to organize data structures with Mode X in mind. For example, icons could be prearranged in system memory with the pixels organized into four plane-oriented sets (or, again, in four sets per scan line to avoid a fading-in effect) to facilitate copying to the screen a plane at a time with REP MOVS +In general, performing plane-at-a-time operations can make almost any Mode X operation, at the worst, nearly as fast as the same operation in mode 13H (although this sort of Mode X programming is admittedly fairly complex). In this pursuit, it can help to organize data structures with Mode X in mind. For example, icons could be prearranged in system memory with the pixels organized into four plane-oriented sets (or, again, in four sets per scan line to avoid a fading-in effect) to facilitate copying to the screen a plane at a time with REP MOVS

LISTING 47.5 L47-5.ASM

diff --git a/48-03.html b/48-03.html index 56eda57..9a5e15d 100644 --- a/48-03.html +++ b/48-03.html @@ -52,7 +52,7 @@

Copying Pixel Blocks within Display Memory

Another fine use for the latches is copying pixels from one place in display memory to another. Whenever both the source and the destination share the same nibble alignment (that is, their start addresses modulo four are the same), it is not only possible but quite easy to use the latches to copy four pixels at a time. Listing 48.3 shows a routine that copies via the latches. (When the source and destination do not share the same nibble alignment, the latches cannot be used because the source and destination planes for any given pixel differ. In that case, you can set the Read Map register to select a source plane and the Map Mask register to select the corresponding destination plane. Then, copy all pixels in that plane, repeating for all four planes.)

-
Although copying through the latches is, in general, a speedy technique, especially on slower VGAs, it’s not always a win. Reading video memory tends to be quite a bit slower than writing, and on a fast VLB or PCI adapter, it can be faster to copy from main memory to display memory than it is to copy from display memory to display memory via the latches. +
Although copying through the latches is, in general, a speedy technique, especially on slower VGAs, it’s not always a win. Reading video memory tends to be quite a bit slower than writing, and on a fast VLB or PCI adapter, it can be faster to copy from main memory to display memory than it is to copy from display memory to display memory via the latches.


diff --git a/49-03.html b/49-03.html index 321080c..32af990 100644 --- a/49-03.html +++ b/49-03.html @@ -134,7 +134,7 @@ typedef struct {

Listings 49.1 and 49.2, like all Mode X code I’ve presented, perform no clipping, because clipping code would complicate the listings too much. While clipping can be implemented directly in the low-level Mode X routines (at the beginning of Listing 49.1, for instance), another, potentially simpler approach would be to perform clipping at a higher level, modifying the coordinates and dimensions passed to low-level routines such as Listings 49.1 and 49.2 as necessary to accomplish the desired clipping. It is for precisely this reason that the low-level Mode X routines support programmable start coordinates in the source images, rather than assuming (0,0); likewise for the distinction between the width of the image and the width of the area of the image to draw.

Also, it would be more efficient to make up structures that describe the source and destination bitmaps, with dimensions and coordinates built in, and simply pass pointers to these structures to the low level, rather than passing many separate parameters, as is now the case. I’ve used separate parameters for simplicity and flexibility.

-
Be aware that as nifty as Mode X hardware-assisted masked copying is, whether or not it’s actually faster than software-only masked or transparent copying depends upon the processor and the video adapter. The advantage of Mode X masked copying is the 32-bit parallelism; the disadvantages are the need to read display memory and the need to perform an OUT for every four pixels. (OUT is a slow 486/Pentium instruction, and most VGAs respond to OUTs much more slowly than to display memory writes.) +
Be aware that as nifty as Mode X hardware-assisted masked copying is, whether or not it’s actually faster than software-only masked or transparent copying depends upon the processor and the video adapter. The advantage of Mode X masked copying is the 32-bit parallelism; the disadvantages are the need to read display memory and the need to perform an OUT for every four pixels. (OUT is a slow 486/Pentium instruction, and most VGAs respond to OUTs much more slowly than to display memory writes.)


diff --git a/51-01.html b/51-01.html index d41405e..a241a9c 100644 --- a/51-01.html +++ b/51-01.html @@ -41,7 +41,7 @@

Using Backface Removal to Eliminate Hidden Surfaces

As I’m fond of pointing out, computer animation isn’t a matter of mathematically exact modeling or raw technical prowess, but rather of fooling the eye and the mind. That’s especially true for 3-D animation, where we’re not only trying to convince viewers that they’re seeing objects on a screen—when in truth that screen contains no objects at all, only gaggles of pixels—but we’re also trying to create the illusion that the objects exist in three-space, possessing four dimensions (counting movement over time as a fourth dimension) of their own. To make this magic happen, we must provide cues for the eye not only to pick out boundaries, but also to detect depth, orientation, and motion. This involves perspective, shading, proper handling of hidden surfaces, and rapid and smooth screen updates; the whole deal is considerably more difficult to pull off on a PC than 2-D animation.

-
In some senses, however, 3-D animation is easier than 2-D. Because there’s more going on in 3-D animation, the eye and brain tend to make more assumptions, and so are more apt to see what they expect to see, rather than what’s actually there. +
In some senses, however, 3-D animation is easier than 2-D. Because there’s more going on in 3-D animation, the eye and brain tend to make more assumptions, and so are more apt to see what they expect to see, rather than what’s actually there.

If you’re piloting a (virtual) ship through a field of thousands of asteroids at high speed, you’re unlikely to notice if the more distant asteroids occasionally seem to go right through each other, or if the topographic detail on the asteroids’ surfaces sometimes shifts about a bit. You’ll be busy viewing the asteroids in their primary role, as objects to be navigated around, and the mere presence of topographic detail will suffice; without being aware of it, you’ll fill in the blanks. Your mind will see the topography peripherally, recognize it for what it is supposed to be, and, unless the landscape does something really obtrusive such as vanishing altogether or suddenly shooting a spike miles into space, you will see what you expect to see: a bunch of nicely detailed asteroids tumbling around you.

diff --git a/56-03.html b/56-03.html index 63bc916..0c4e2bd 100644 --- a/56-03.html +++ b/56-03.html @@ -39,7 +39,7 @@

Fast Texture Mapping: An Implementation

As you might expect, I’ve implemented DDA texture mapping in X-Sharp, and the changes are reflected in the X-Sharp archive in this chapter’s subdirectory on the listings disk. Listing 56.1 shows the new header file entries, and Listing 56.2 shows the actual texture-mapped polygon drawer. The set-pixel routine that Listing 56.2 calls is a slight modification of the Mode X set-pixel routine from Chapter 47. In addition, INITBALL.C has been modified to create three texture-mapped polygons and define the texture bitmaps, and modifications have been made to allow the user to flip the axis of rotation. You will of course need the complete X-Sharp library to see texture mapping in action, but Listings 56.1 and 56.2 are the actual texture mapping code in its entirety.

-
Here’s a major tip: DDA texture mapping looks best on fast-moving surfaces, where the eye doesn’t have time to pick nits with the shearing and aliasing that’s an inevi table by-product of such a crude approach. Compile DEMO1 from the X-Sharp archive in this chapter’s subdirectory of the listings disk, and run it. The initial display looks okay, but certainly not great, because the rotational speed is so slow. Now press the S key a few times to speed up the rotation and flip between different rotation axes. I think you’ll be amazed at how much better DDA texture mapping looks at high speed. This technique would be great for mapping textures onto hurtling asteroids or jets, but would come up short for slow, finely detailed movements. +
Here’s a major tip: DDA texture mapping looks best on fast-moving surfaces, where the eye doesn’t have time to pick nits with the shearing and aliasing that’s an inevi table by-product of such a crude approach. Compile DEMO1 from the X-Sharp archive in this chapter’s subdirectory of the listings disk, and run it. The initial display looks okay, but certainly not great, because the rotational speed is so slow. Now press the S key a few times to speed up the rotation and flip between different rotation axes. I think you’ll be amazed at how much better DDA texture mapping looks at high speed. This technique would be great for mapping textures onto hurtling asteroids or jets, but would come up short for slow, finely detailed movements.

LISTING 56.1 L56-1.C

diff --git a/57-01.html b/57-01.html index f2fe0b9..558b0d0 100644 --- a/57-01.html +++ b/57-01.html @@ -47,7 +47,7 @@

The obvious lesson here is that adequate speed is important to convincing animation. There’s another, less obvious side to this lesson, though. I’d been running the texture-mapping demo on a 20 MHz 386 with a slow VGA when I discovered the beneficial effects of greater animation speed. When, some time later, I ran the demo on a 33 MHz 486 with a fast VGA, I found that the faster rotation was too fast! The ball spun so rapidly that the eye couldn’t blend successive images together into continuous motion, much like watching a badly flickering movie.

-
So the second lesson is that either too little or too much speed can destroy the illusion. Unless you’re antialiasing, you need to tune the shifting of your images so that they’re in the “sweet spot” of apparent motion, in which the eye is willing to ignore the jumping and aliasing, and blend the images together into continuous motion. Only experience can give you a feel for that sweet spot. +So the second lesson is that either too little or too much speed can destroy the illusion. Unless you’re antialiasing, you need to tune the shifting of your images so that they’re in the “sweet spot” of apparent motion, in which the eye is willing to ignore the jumping and aliasing, and blend the images together into continuous motion. Only experience can give you a feel for that sweet spot.

Fixed-Point Arithmetic, Redux

In the previous chapter I added texture mapping to X-Sharp, but lacked space to explain some of its finer points. I’ll pick up the thread now and cover some of those points here, and discuss the visual and performance enhancements that previous chapter’s code needed—and which are now present in the version of X-Sharp in this chapter’s subdirectory on the CD-ROM. diff --git a/58-03.html b/58-03.html index dd4457e..d9a1cb5 100644 --- a/58-03.html +++ b/58-03.html @@ -38,7 +38,7 @@


I’d like to emphasize that algorithmically and conceptually, there is no difference between scanning out a polygon top to bottom and scanning it out left to right; it is only in conjunction with the hardware organization of Mode X that the scanning direction matters in the least.

-
That’s what Zen programming is all about, though; tying together two pieces of seemingly unrelated information to good effect—and that’s what I had failed to do. Like Robert Heinlein—like all of us—I had viewed the world through a filter composed of my ingrained assumptions, and one of those assumptions, based on all my past experience, was that pixel processing proceeds left to right. Eventually, I might have come up with Chris’s approach; but I would only have come up with it when and if I relaxed and stepped back a little, and allowed myself—almost dared myself—to think of it. When you’re optimizing, be sure to leave quiet, nondirected time in which to conjure up those less obvious solutions, and periodically try to figure out what assumptions you’re making—and then question them! +That’s what Zen programming is all about, though; tying together two pieces of seemingly unrelated information to good effect—and that’s what I had failed to do. Like Robert Heinlein—like all of us—I had viewed the world through a filter composed of my ingrained assumptions, and one of those assumptions, based on all my past experience, was that pixel processing proceeds left to right. Eventually, I might have come up with Chris’s approach; but I would only have come up with it when and if I relaxed and stepped back a little, and allowed myself—almost dared myself—to think of it. When you’re optimizing, be sure to leave quiet, nondirected time in which to conjure up those less obvious solutions, and periodically try to figure out what assumptions you’re making—and then question them!


Figure 58.3
  Texture mapping a single vertical column. diff --git a/59-03.html b/59-03.html index ae0b5a7..01e67ae 100644 --- a/59-03.html +++ b/59-03.html @@ -97,7 +97,7 @@ void WalkBSPTree(NODE *pNode) -
Be aware that BSP trees can often be made smaller and more efficient by detecting collinear surfaces (like aligned wall segments) and generating only one BSP node for each collinear set, with the collinear surfaces stored in, say, a linked list attached to that node. Collinear surfaces partition space identically and can’t occlude one another, so it suffices to generate one splitting node for each collinear set. +Be aware that BSP trees can often be made smaller and more efficient by detecting collinear surfaces (like aligned wall segments) and generating only one BSP node for each collinear set, with the collinear surfaces stored in, say, a linked list attached to that node. Collinear surfaces partition space identically and can’t occlude one another, so it suffices to generate one splitting node for each collinear set.


diff --git a/59-05.html b/59-05.html index 24b9606..874d19d 100644 --- a/59-05.html +++ b/59-05.html @@ -40,7 +40,7 @@

Let’s look at it another way. All the code in Listing 59.2 does is say: “Here I am at a node. First I’ll visit the left subtree if there is one, then I’ll visit this node, then I’ll visit the right subtree if there is one. While I’m visiting the left subtree, I’ll just push a marker on a stack that tells me to come back here when the left subtree is done. If, after visiting a node, there are no right children to visit and nothing left on the stack, I’m finished. The code does this at each node—and that’s all it does. That’s all Listing 59.4 does, too, but people tend to get tangled up in pushes and pops and while loops when they use data recursion. When the implementation model changes to one with which they are unfamiliar, they abandon the perfectly good model they used before and try to rederive it in the new context by the seat of their pants.

-
Here’s a secret when you’re faced with a situation like this: Step back and get a clear picture of what your code has to do. Omit no steps. You should build a model that is so consistent and solid that you can instantly answer any question about how the code should behave in any situation. For example, my interviewees often decide, by trial and error, that there are two distinct types of right children: Right children visited after popping back to visit a node after the left subtree has been visited, and right children visited after descending to a node that has no left child. This makes the traversal code a mass of special cases, each of which has to be detected by the programmer by trying out scenarios. Worse, you can never be sure with this approach that you’ve caught all the special cases. +Here’s a secret when you’re faced with a situation like this: Step back and get a clear picture of what your code has to do. Omit no steps. You should build a model that is so consistent and solid that you can instantly answer any question about how the code should behave in any situation. For example, my interviewees often decide, by trial and error, that there are two distinct types of right children: Right children visited after popping back to visit a node after the left subtree has been visited, and right children visited after descending to a node that has no left child. This makes the traversal code a mass of special cases, each of which has to be detected by the programmer by trying out scenarios. Worse, you can never be sure with this approach that you’ve caught all the special cases.
The alternative is to develop and apply a unifying model. There aren’t really two types of right children; the rule is that all right children are visited after their parents are visited, period. The presence or absence of a left child is irrelevant. The possibility that a right child may be reached via different code paths depending on the presence of a left child does not affect the overall model. While this distinction may seem trivial it is in fact crucial, because if you have the model down cold, you can always tell if the implementation is correct by comparing it with the model. diff --git a/61-03.html b/61-03.html index 7cb3701..98d2bb0 100644 --- a/61-03.html +++ b/61-03.html @@ -48,7 +48,7 @@

(eq. 7)

(Note that the cross product operation is denoted by an X.) Unlike the dot product, the result of the cross product is a vector. Not just any vector, either; the vector generated by the cross product is perpendicular to both of the original vectors. Thus, the cross product can be used to generate a normal to any surface for which you have two vectors that lie within the surface. This means that we can generate the screenspace normals we need by taking the cross product of two adjacent polygon edges, as shown in Figure 61.4.

-
In fact, we can cull with only one-third the work needed to generate a full cross product; because we’re interested only in the sign of the z component of the normal, we can skip entirely calculating the x and y components. The only caveat is to be careful that neither edge you choose is zero-length and that the edges aren’t collinear, because the dot product can’t produce a normal in those cases. +
In fact, we can cull with only one-third the work needed to generate a full cross product; because we’re interested only in the sign of the z component of the normal, we can skip entirely calculating the x and y components. The only caveat is to be careful that neither edge you choose is zero-length and that the edges aren’t collinear, because the dot product can’t produce a normal in those cases.


Figure 61.4
  How the cross product of polygon edge vectors generates a polygon normal. diff --git a/61-04.html b/61-04.html index ef8c5ec..4c2247c 100644 --- a/61-04.html +++ b/61-04.html @@ -97,7 +97,7 @@ void LineIntersectPlane (float *linestart, float *lineend,

Their approach is this: Think of rotation as projecting coordinates onto new axes. That is, given that you have points in, say, worldspace, define the new coordinate space (viewspace, for example) you want to rotate to by a set of three orthogonal unit vectors defining the new axes, and then project each point onto each of the three axes to get the coordinates in the new coordinate space, as shown for the 2-D case in Figure 61.8. In 3-D, this involves three dot products per point, one to project the point onto each axis. Translation can be done separately from rotation by simple addition.

-
Rotation by projection is exactly the same as rotation via matrix multiplication; in fact, the rows of a rotation matrix are the orthogonal unit vectors pointing along the new axes. Rotation by projection buys us no technical advantages, so that’s not what’s important here; the key is that the concept of rotation by projection, together with a separate translation step, gives us a new way to look at transformation that I, for one, find easier to visualize and experiment with. A new frame of reference for how we think about 3-D frames of reference, if you will. +
Rotation by projection is exactly the same as rotation via matrix multiplication; in fact, the rows of a rotation matrix are the orthogonal unit vectors pointing along the new axes. Rotation by projection buys us no technical advantages, so that’s not what’s important here; the key is that the concept of rotation by projection, together with a separate translation step, gives us a new way to look at transformation that I, for one, find easier to visualize and experiment with. A new frame of reference for how we think about 3-D frames of reference, if you will.

Three things I’ve learned over the years are that it never hurts to learn a new way of looking at things, that it helps to have a clearer, more intuitive model in your head of whatever it is you’re working on, and that new tools, or new ways to use old tools, are Good Things. My experience has been that rotation by projection, and dot product tricks in general, offer those sorts of benefits for 3-D.

diff --git a/62-01.html b/62-01.html index 09fdd40..78b452d 100644 --- a/62-01.html +++ b/62-01.html @@ -46,7 +46,7 @@

As it turns out, the memcpy() routine in the DOS version of our compiler (gcc) inexplicably copies memory a byte at a time. With my new routine, the non-page-flipped approach suddenly became slightly faster than page flipping.

The first relevant rule is pretty obvious: Assume nothing. Measure early and often. Know what’s really going on when your program runs, if you catch my drift. To do otherwise is to risk looking mighty foolish.

The second rule: When you do look foolish (and trust me, it will happen if you do challenging work) have a good laugh at yourself, and use it as a reminder of Rule #1. I hadn’t done any extra page-flipping work yet, so I didn’t waste any time due to my faulty assumption that memcpy() performed a maximum-speed copy, but that was just luck. I should have done experiments until I was sure I knew what was going on before drawing any conclusions and acting on them.

-
In general, make it a point not to fall into a tightly focused rut; stay loose and think of alternative possibilities and new approaches, and always, always, always keep asking questions. It’ll pay off big in the long run. If I hadn’t indulged my curiosity by running the Pentium counter test on the copy to the screen, even though there was no specific reason to do so, I would never have discovered the memcpy() problem—and by so doing I doubled the performance of the entire program in five minutes, a rare accomplishment indeed. +
In general, make it a point not to fall into a tightly focused rut; stay loose and think of alternative possibilities and new approaches, and always, always, always keep asking questions. It’ll pay off big in the long run. If I hadn’t indulged my curiosity by running the Pentium counter test on the copy to the screen, even though there was no specific reason to do so, I would never have discovered the memcpy() problem—and by so doing I doubled the performance of the entire program in five minutes, a rare accomplishment indeed.

By the way, I have found the Pentium’s performance counters to be very useful in of information on the performance counters and other aspects of the Pentium is Mike Schmit’s book, Pentium Processor Optimization Tools, AP Professional, ISBN 0-12-627230-1.

Onward to rendering from a BSP tree.

diff --git a/64-01.html b/64-01.html index a63d458..00caa97 100644 --- a/64-01.html +++ b/64-01.html @@ -45,7 +45,7 @@

Tom couldn’t spare the gates or the time to do a full FIFO, but he could implement a one-deep FIFO, allowing the CPU to get one write ahead of the VGA. He wasn’t sure how well it would work, but it was all he could do, so he put it in and taped out the chip.

The one-deep FIFO turned out to work astonishingly well; for a time, Video Seven’s VGAs were the fastest around, a testament to Tom’s ingenuity and creativity under pressure. However, the truly remarkable part of this story is that Paradise’s FIFO design turned out to bear not the slightest resemblance to Tom’s, and didn’t work as well. Paradise had stuck a read FIFO between display memory and the video output stage of the VGA, allowing the video output to read ahead, so that when the CPU wanted to access display memory, pixels could come from the FIFO while the CPU was serviced immediately. That did indeed help performance—but not as much as Tom’s write FIFO.

-
What we have here is as neat a parable about the nature of creative design as one could hope to find. The scrap of news about Paradise’s chip contained almost no actual information, but it forced Tom to push past the limits he had unconsciously set in coming up with his original design. And, in the end, I think that the single most important element of great design, whether it be hardware, software, or any creative endeavor, is precisely what the Paradise news triggered in Tom: the ability to detect the limits you have built into the way you think about your design, and then transcend those limits. +What we have here is as neat a parable about the nature of creative design as one could hope to find. The scrap of news about Paradise’s chip contained almost no actual information, but it forced Tom to push past the limits he had unconsciously set in coming up with his original design. And, in the end, I think that the single most important element of great design, whether it be hardware, software, or any creative endeavor, is precisely what the Paradise news triggered in Tom: the ability to detect the limits you have built into the way you think about your design, and then transcend those limits.

The problem, of course, is how to go about transcending limits you don’t even know you’ve imposed. There’s no formula for success, but two principles can stand you in good stead: simplify and keep on trying new things.

diff --git a/64-04.html b/64-04.html index d405f69..5f072f9 100644 --- a/64-04.html +++ b/64-04.html @@ -46,7 +46,7 @@

Not at all. All really great designs seem simple and even obvious—once they’ve been designed. But the process of getting there requires incredible persistence and a willingness to try lots of different ideas until the right one falls into place, as happened here.

-
My friend Chris Hecker has a theory that all approaches work out to the same thing in the end, since they all reflect the same underlying state and functionality. In terms of underlying theory, I’ve found that to be true; whether you do perspective texture mapping with a divide or with incremental hyperbolic calculations, the numbers do exactly the same thing. When it comes to implementation, however, my experience is that simply time-shifting an approach, or matching hardware capabilities better, or caching can make an astonishing difference. +My friend Chris Hecker has a theory that all approaches work out to the same thing in the end, since they all reflect the same underlying state and functionality. In terms of underlying theory, I’ve found that to be true; whether you do perspective texture mapping with a divide or with incremental hyperbolic calculations, the numbers do exactly the same thing. When it comes to implementation, however, my experience is that simply time-shifting an approach, or matching hardware capabilities better, or caching can make an astonishing difference.

My friend Terje Mathisen likes to say that “almost all programming can be viewed as an exercise in caching,” and that’s exactly what John did. No matter how fast he made his VSD calculations, they could never be as fast as precalculating and looking up the visibility, and his most inspired move was to yank himself out of the “faster code” mindset and realize that it was in fact possible to precalculate (in effect, cache) and look up the PVS.

diff --git a/66-02.html b/66-02.html index 2751c7c..edc34b9 100644 --- a/66-02.html +++ b/66-02.html @@ -38,7 +38,7 @@


As you may recall from Chapter 64, we’re concerned with both raw performance and level performance. That is, we want the drawing code to run as fast as possible, but we also want the difference in drawing speed between the average scene and the slowest-drawing scene to be as small as possible.

-
It does little good to average 30 frames per second if 10 percent of the scenes draw at 5 fps, because the jerkiness in those scenes will be extremely obvious by comparison with the average scene, and highly objectionable. It would be better to average 15 fps 100 percent of the time, even though the average drawing speed is only half as much. +
It does little good to average 30 frames per second if 10 percent of the scenes draw at 5 fps, because the jerkiness in those scenes will be extremely obvious by comparison with the average scene, and highly objectionable. It would be better to average 15 fps 100 percent of the time, even though the average drawing speed is only half as much.

The precalculated PVS was an important step toward both faster and more level performance, because it eliminated the need to identify visible polygons, a relatively slow step that tended to be at its worst in the most complex scenes. Nonetheless, in some spots in real game levels the precalculated PVS contains five times more polygons than are actually visible; together with the back-to-front HSR approach, this created hot spots in which the frame rate bogged down visibly as hundreds of polygons are drawn back-to- front, most of those immediately getting overdrawn by nearer polygons. Raw performance in general was also reduced by the typical 50% overdraw resulting from drawing everything in the PVS. So, although drawing the PVS back-to-front as the final HSR stage worked and was an improvement over previous designs, it was not ideal. Surely, John thought, there’s a better way to leverage the PVS than back-to-front drawing.

diff --git a/70-03.html b/70-03.html index 06c97ee..0393756 100644 --- a/70-03.html +++ b/70-03.html @@ -47,7 +47,7 @@

After the PVS is marked, the BSP is walked front-to-back. At each node, the bounding box of the node’s subspace is clipped against the view frustum; if the bounding box is fully clipped, then that node and all its children are ignored. Likewise, if the node is not in the PVS for the current viewpoint leaf, the node and all its children are ignored. If the bounding box is partially clipped or not clipped at all, that information is passed to the children so that any unnecessary clip tests can be avoided. The children in front of the node are then processed recursively. When a leaf is reached, polygons that touch that leaf are marked as potentially drawable. When recursion in front of a node is finished, all polygons on the front side of the node that are marked as potentially drawable are added to the edge list, and then the children on the back side of that node are similarly processed recursively.

The edge list is a special, intermediate step between polygons and drawing. Each polygon is clipped, transformed, and projected, and its non-horizontal edges are added to a global list of potentially drawable edges. After all the potentially drawable edges in the world have been added, the global edge list is scanned out all at once, and all the visible spans (the nearest spans, as determined by sorting on BSP-walk order) in the world are emitted into span lists linked off the respective surface descriptors (for now, you can think of a surface as being the same as a polygon). Taken together, these spans cover every pixel on the screen once and only once, resulting in zero overdraw; surfaces that are completely hidden by nearer surfaces generate no spans at all. The spans are then drawn; all the spans for one surface are drawn, and then all the spans for the next, so that there’s texture coherency between spans, which is very helpful for processor cache coherency, and also to reduce setup overhead.

The primary purpose of the edge list is to make Quake’s performance as level—that is, as consistent—as possible. Compared to simply drawing all potentially drawable polygons front-to-back, the edge list certainly slows down the best case, that is, when there’s no overdraw. However, by eliminating overdraw, the worst case is helped considerably; in Quake, there’s a ratio of perhaps 4:1 between worst and best case drawing time, versus the 10:1 or more that can happen with straight polygon drawing. Leveling is very important, because cases where a game slows down to the point of being unplayable dictate game and level design, and the fewer constraints placed on design, the better.

-
A corollary is that best case performance can be seductively misleading; it’s a great feeling to see a scene running at 30 or even 60 frames per second, but if the bulk of the game runs at 15 fps, those best cases are just going to make the rest of the game look worse. +
A corollary is that best case performance can be seductively misleading; it’s a great feeling to see a scene running at 30 or even 60 frames per second, but if the bulk of the game runs at 15 fps, those best cases are just going to make the rest of the game look worse.


diff --git a/70-08.html b/70-08.html index 263aa6c..e6d662f 100644 --- a/70-08.html +++ b/70-08.html @@ -40,7 +40,7 @@

However, client-server is not without a cost, because, in its pure form, latency for clients consists of the round trip from the client to the server and back. (In Quake, orientation changes instantly on the client, short-circuiting the trip to the server, but all other events, such as motion and firing, must make the round trip before they happen on the client.) In peer-to-peer games, maximum latency can be just the cost of the one-way trip, because each client is running a simulation of the game, and each peer sees its own actions instantly. What all this means is that latency is the downside of client-server, but in many other respects client-server is very attractive. So the big task with client-server is to reduce latency.

As of the release of QTest1, the first and last prerelease of Quake, John had smoothed net play considerably by actually keeping the client’s virtual time a bit earlier than the time of the last server packet, and interpolating events between the last two packets to the client’s virtual time. This meant that events didn’t snap to whatever packet had arrived last, and got rid of considerable jerking and stuttering. Unfortunately, it actually increased latency, because of the retarding of time needed to make the interpolation possible. This illustrates a common tradeoff, which is that reduced latency often makes for rougher play.

-
Reduced latency also often makes for more frustrating play. It’s actually not hard to reduce the latency perceived by the player, but many of the approaches that reduce latency introduce the potential for paradoxes that can be quite distracting and annoying. For example, a player may see a rocket go by, and think they’ve dodged it, only to find themselves exploding a second later as the difference of opinion between his simulation and the other simulation is resolved to his detriment. +
Reduced latency also often makes for more frustrating play. It’s actually not hard to reduce the latency perceived by the player, but many of the approaches that reduce latency introduce the potential for paradoxes that can be quite distracting and annoying. For example, a player may see a rocket go by, and think they’ve dodged it, only to find themselves exploding a second later as the difference of opinion between his simulation and the other simulation is resolved to his detriment.

Worse, QTest1 was prone to frequent hitching over all but the best connections, because it was built around reliable packet delivery (TCP) provided by the operating system. Whenever a packet didn’t arrive, there was a long pause waiting for the retransmission. After QTest1, John realized that this was a fundamentally wrong assumption, and changed the code to use unreliable packet delivery (UDP), sending the relevant portion of the full state every time (possible only because the PVS can be used to cull most events in a level), and letting the game logic itself deal with packets that didn’t arrive. A reliable sideband was used as well, but only for events like scores, not for gameplay state. However, this was a good example of Carmack’s Law: John did not rewrite the net code to reflect this new fundamental assumption, and wound up with 8,000 lines of messy code that took right up until Quake shipped to debug. For QuakeWorld, John did rewrite the net code from scratch around the assumption of unreliable packet delivery, and it wound up as just 1,500 lines of clean, bug-free code.

diff --git a/images/01-02i.jpg b/images/01-02i.jpg deleted file mode 100644 index 485082d..0000000 Binary files a/images/01-02i.jpg and /dev/null differ diff --git a/images/01-03i.jpg b/images/01-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/01-03i.jpg and /dev/null differ diff --git a/images/01-04i.jpg b/images/01-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/01-04i.jpg and /dev/null differ diff --git a/images/01-05i.jpg b/images/01-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/01-05i.jpg and /dev/null differ diff --git a/images/01-06i.jpg b/images/01-06i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/01-06i.jpg and /dev/null differ diff --git a/images/02-01i.jpg b/images/02-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/02-01i.jpg and /dev/null differ diff --git a/images/02-02i.jpg b/images/02-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/02-02i.jpg and /dev/null differ diff --git a/images/02-03i.jpg b/images/02-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/02-03i.jpg and /dev/null differ diff --git a/images/03-01i.jpg b/images/03-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/03-01i.jpg and /dev/null differ diff --git a/images/04-01i.jpg b/images/04-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/04-01i.jpg and /dev/null differ diff --git a/images/04-02i.jpg b/images/04-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/04-02i.jpg and /dev/null differ diff --git a/images/04-03i.jpg b/images/04-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/04-03i.jpg and /dev/null differ diff --git a/images/04-04i.jpg b/images/04-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/04-04i.jpg and /dev/null differ diff --git a/images/05-01i.jpg b/images/05-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/05-01i.jpg and /dev/null differ diff --git a/images/05-02i.jpg b/images/05-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/05-02i.jpg and /dev/null differ diff --git a/images/05-03i.jpg b/images/05-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/05-03i.jpg and /dev/null differ diff --git a/images/05-04i.jpg b/images/05-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/05-04i.jpg and /dev/null differ diff --git a/images/06-01i.jpg b/images/06-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/06-01i.jpg and /dev/null differ diff --git a/images/07-01i.jpg b/images/07-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-01i.jpg and /dev/null differ diff --git a/images/07-02i.jpg b/images/07-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-02i.jpg and /dev/null differ diff --git a/images/07-03i.jpg b/images/07-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-03i.jpg and /dev/null differ diff --git a/images/07-04i.jpg b/images/07-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-04i.jpg and /dev/null differ diff --git a/images/07-05i.jpg b/images/07-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-05i.jpg and /dev/null differ diff --git a/images/07-06i.jpg b/images/07-06i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-06i.jpg and /dev/null differ diff --git a/images/07-07i.jpg b/images/07-07i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-07i.jpg and /dev/null differ diff --git a/images/07-08i.jpg b/images/07-08i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/07-08i.jpg and /dev/null differ diff --git a/images/08-01i.jpg b/images/08-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/08-01i.jpg and /dev/null differ diff --git a/images/08-02i.jpg b/images/08-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/08-02i.jpg and /dev/null differ diff --git a/images/08-03i.jpg b/images/08-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/08-03i.jpg and /dev/null differ diff --git a/images/08-04i.jpg b/images/08-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/08-04i.jpg and /dev/null differ diff --git a/images/08-05i.jpg b/images/08-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/08-05i.jpg and /dev/null differ diff --git a/images/09-01i.jpg b/images/09-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/09-01i.jpg and /dev/null differ diff --git a/images/09-02i.jpg b/images/09-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/09-02i.jpg and /dev/null differ diff --git a/images/09-03i.jpg b/images/09-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/09-03i.jpg and /dev/null differ diff --git a/images/09-04i.jpg b/images/09-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/09-04i.jpg and /dev/null differ diff --git a/images/09-05i.jpg b/images/09-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/09-05i.jpg and /dev/null differ diff --git a/images/10-01i.jpg b/images/10-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/10-01i.jpg and /dev/null differ diff --git a/images/10-02i.jpg b/images/10-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/10-02i.jpg and /dev/null differ diff --git a/images/10-03i.jpg b/images/10-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/10-03i.jpg and /dev/null differ diff --git a/images/11-01i.jpg b/images/11-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/11-01i.jpg and /dev/null differ diff --git a/images/11-02i.jpg b/images/11-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/11-02i.jpg and /dev/null differ diff --git a/images/11-03i.jpg b/images/11-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/11-03i.jpg and /dev/null differ diff --git a/images/11-04i.jpg b/images/11-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/11-04i.jpg and /dev/null differ diff --git a/images/11-05i.jpg b/images/11-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/11-05i.jpg and /dev/null differ diff --git a/images/11-06i.jpg b/images/11-06i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/11-06i.jpg and /dev/null differ diff --git a/images/12-01i.jpg b/images/12-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/12-01i.jpg and /dev/null differ diff --git a/images/12-02i.jpg b/images/12-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/12-02i.jpg and /dev/null differ diff --git a/images/12-03i.jpg b/images/12-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/12-03i.jpg and /dev/null differ diff --git a/images/12-04i.jpg b/images/12-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/12-04i.jpg and /dev/null differ diff --git a/images/12-05i.jpg b/images/12-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/12-05i.jpg and /dev/null differ diff --git a/images/12-06i.jpg b/images/12-06i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/12-06i.jpg and /dev/null differ diff --git a/images/13-01i.jpg b/images/13-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/13-01i.jpg and /dev/null differ diff --git a/images/13-02i.jpg b/images/13-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/13-02i.jpg and /dev/null differ diff --git a/images/13-03i.jpg b/images/13-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/13-03i.jpg and /dev/null differ diff --git a/images/14-01i.jpg b/images/14-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/14-01i.jpg and /dev/null differ diff --git a/images/14-02i.jpg b/images/14-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/14-02i.jpg and /dev/null differ diff --git a/images/15-01i.jpg b/images/15-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/15-01i.jpg and /dev/null differ diff --git a/images/16-01i.jpg b/images/16-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/16-01i.jpg and /dev/null differ diff --git a/images/16-02i.jpg b/images/16-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/16-02i.jpg and /dev/null differ diff --git a/images/16-03i.jpg b/images/16-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/16-03i.jpg and /dev/null differ diff --git a/images/16-04i.jpg b/images/16-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/16-04i.jpg and /dev/null differ diff --git a/images/17-01i.jpg b/images/17-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/17-01i.jpg and /dev/null differ diff --git a/images/17-02i.jpg b/images/17-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/17-02i.jpg and /dev/null differ diff --git a/images/17-03i.jpg b/images/17-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/17-03i.jpg and /dev/null differ diff --git a/images/17-04i.jpg b/images/17-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/17-04i.jpg and /dev/null differ diff --git a/images/17-05i.jpg b/images/17-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/17-05i.jpg and /dev/null differ diff --git a/images/18-01i.jpg b/images/18-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/18-01i.jpg and /dev/null differ diff --git a/images/19-01i.jpg b/images/19-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/19-01i.jpg and /dev/null differ diff --git a/images/19-02i.jpg b/images/19-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/19-02i.jpg and /dev/null differ diff --git a/images/19-03i.jpg b/images/19-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/19-03i.jpg and /dev/null differ diff --git a/images/19-04i.jpg b/images/19-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/19-04i.jpg and /dev/null differ diff --git a/images/19-05i.jpg b/images/19-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/19-05i.jpg and /dev/null differ diff --git a/images/19-06i.jpg b/images/19-06i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/19-06i.jpg and /dev/null differ diff --git a/images/20-01i.jpg b/images/20-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/20-01i.jpg and /dev/null differ diff --git a/images/20-02i.jpg b/images/20-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/20-02i.jpg and /dev/null differ diff --git a/images/20-03i.jpg b/images/20-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/20-03i.jpg and /dev/null differ diff --git a/images/20-04i.jpg b/images/20-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/20-04i.jpg and /dev/null differ diff --git a/images/20-05i.jpg b/images/20-05i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/20-05i.jpg and /dev/null differ diff --git a/images/23-01i.jpg b/images/23-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/23-01i.jpg and /dev/null differ diff --git a/images/23-02i.jpg b/images/23-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/23-02i.jpg and /dev/null differ diff --git a/images/23-03i.jpg b/images/23-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/23-03i.jpg and /dev/null differ diff --git a/images/24-01i.jpg b/images/24-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/24-01i.jpg and /dev/null differ diff --git a/images/25-01i.jpg b/images/25-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/25-01i.jpg and /dev/null differ diff --git a/images/25-02i.jpg b/images/25-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/25-02i.jpg and /dev/null differ diff --git a/images/26-01i.jpg b/images/26-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/26-01i.jpg and /dev/null differ diff --git a/images/27-01i.jpg b/images/27-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/27-01i.jpg and /dev/null differ diff --git a/images/27-02i.jpg b/images/27-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/27-02i.jpg and /dev/null differ diff --git a/images/28-01i.jpg b/images/28-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/28-01i.jpg and /dev/null differ diff --git a/images/29-01i.jpg b/images/29-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/29-01i.jpg and /dev/null differ diff --git a/images/29-02i.jpg b/images/29-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/29-02i.jpg and /dev/null differ diff --git a/images/29-03i.jpg b/images/29-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/29-03i.jpg and /dev/null differ diff --git a/images/30-01i.jpg b/images/30-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/30-01i.jpg and /dev/null differ diff --git a/images/30-02i.jpg b/images/30-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/30-02i.jpg and /dev/null differ diff --git a/images/30-03i.jpg b/images/30-03i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/30-03i.jpg and /dev/null differ diff --git a/images/30-04i.jpg b/images/30-04i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/30-04i.jpg and /dev/null differ diff --git a/images/32-01i.jpg b/images/32-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/32-01i.jpg and /dev/null differ diff --git a/images/34-01i.jpg b/images/34-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/34-01i.jpg and /dev/null differ diff --git a/images/34-02i.jpg b/images/34-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/34-02i.jpg and /dev/null differ diff --git a/images/35-01i.jpg b/images/35-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/35-01i.jpg and /dev/null differ diff --git a/images/37-01i.jpg b/images/37-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/37-01i.jpg and /dev/null differ diff --git a/images/38-01i.jpg b/images/38-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/38-01i.jpg and /dev/null differ diff --git a/images/39-01i.jpg b/images/39-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/39-01i.jpg and /dev/null differ diff --git a/images/39-02i.jpg b/images/39-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/39-02i.jpg and /dev/null differ diff --git a/images/40-01i.jpg b/images/40-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/40-01i.jpg and /dev/null differ diff --git a/images/41-01i.jpg b/images/41-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/41-01i.jpg and /dev/null differ diff --git a/images/42-01i.jpg b/images/42-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/42-01i.jpg and /dev/null differ diff --git a/images/44-01i.jpg b/images/44-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/44-01i.jpg and /dev/null differ diff --git a/images/44-02i.jpg b/images/44-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/44-02i.jpg and /dev/null differ diff --git a/images/45-01i.jpg b/images/45-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/45-01i.jpg and /dev/null differ diff --git a/images/45-02i.jpg b/images/45-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/45-02i.jpg and /dev/null differ diff --git a/images/47-01i.jpg b/images/47-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/47-01i.jpg and /dev/null differ diff --git a/images/48-01i.jpg b/images/48-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/48-01i.jpg and /dev/null differ diff --git a/images/49-01i.jpg b/images/49-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/49-01i.jpg and /dev/null differ diff --git a/images/51-01i.jpg b/images/51-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/51-01i.jpg and /dev/null differ diff --git a/images/56-01i.jpg b/images/56-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/56-01i.jpg and /dev/null differ diff --git a/images/57-01i.jpg b/images/57-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/57-01i.jpg and /dev/null differ diff --git a/images/58-01i.jpg b/images/58-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/58-01i.jpg and /dev/null differ diff --git a/images/59-01i.jpg b/images/59-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/59-01i.jpg and /dev/null differ diff --git a/images/59-02i.jpg b/images/59-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/59-02i.jpg and /dev/null differ diff --git a/images/61-01i.jpg b/images/61-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/61-01i.jpg and /dev/null differ diff --git a/images/61-02i.jpg b/images/61-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/61-02i.jpg and /dev/null differ diff --git a/images/62-01i.jpg b/images/62-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/62-01i.jpg and /dev/null differ diff --git a/images/64-01i.jpg b/images/64-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/64-01i.jpg and /dev/null differ diff --git a/images/64-02i.jpg b/images/64-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/64-02i.jpg and /dev/null differ diff --git a/images/66-01i.jpg b/images/66-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/66-01i.jpg and /dev/null differ diff --git a/images/70-01i.jpg b/images/70-01i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/70-01i.jpg and /dev/null differ diff --git a/images/70-02i.jpg b/images/70-02i.jpg deleted file mode 100644 index 63685cc..0000000 Binary files a/images/70-02i.jpg and /dev/null differ diff --git a/images/01-01i.jpg b/images/i.jpg similarity index 100% rename from images/01-01i.jpg rename to images/i.jpg