Normal quotes

This commit is contained in:
James Gregory 2013-12-30 18:34:31 +11:00
commit 18011c595a
352 changed files with 6040 additions and 6040 deletions

View file

@ -18,16 +18,16 @@ proper care, however, and those ugly boxes are capable of miracles. The
key is this: Only on microcomputers do you have the run of the whole key is this: Only on microcomputers do you have the run of the whole
machine, without layers of operating systems, drivers, and the like machine, without layers of operating systems, drivers, and the like
getting in the way. You can do *anything* you want, and you can getting in the way. You can do *anything* you want, and you can
understand everything that’s going on, if you so wish. understand everything that's going on, if you so wish.
As we’ll see shortly, you should indeed so wish. As we'll see shortly, you should indeed so wish.
Is performance still an issue in this era of cheap 486 computers and Is performance still an issue in this era of cheap 486 computers and
super-fast Pentium computers? You bet. How many programs that *you* use super-fast Pentium computers? You bet. How many programs that *you* use
really run so fast that you wouldn’t be happier if they ran faster? really run so fast that you wouldn't be happier if they ran faster?
We’re so used to slow software that when a compile-and-link sequence We're so used to slow software that when a compile-and-link sequence
that took two minutes on a PC takes just ten seconds on a 486 computer, that took two minutes on a PC takes just ten seconds on a 486 computer,
we’re ecstatic—when in truth we should be settling for nothing less than we're ecstatic—when in truth we should be settling for nothing less than
instantaneous response. instantaneous response.
Impossible, you say? Not with the proper design, including incremental Impossible, you say? Not with the proper design, including incremental
@ -35,13 +35,13 @@ compilation and linking, use of extended and/or expanded memory, and
well-crafted code. PCs can do just about anything you can imagine (with well-crafted code. PCs can do just about anything you can imagine (with
a few obvious exceptions, such as applications involving a few obvious exceptions, such as applications involving
super-computer-class number-crunching) if you believe that it can be super-computer-class number-crunching) if you believe that it can be
done, if you understand the computer inside and out, and if you’re done, if you understand the computer inside and out, and if you're
willing to think past the obvious solution to unconventional but willing to think past the obvious solution to unconventional but
potentially more fruitful approaches. potentially more fruitful approaches.
My point is simply this: PCs can work wonders. It’s not easy coaxing My point is simply this: PCs can work wonders. It's not easy coaxing
them into doing that, but it’s rewarding—and it’s sure as heck fun. In them into doing that, but it's rewarding—and it's sure as heck fun. In
this book, we’re going to work some of those wonders, starting... this book, we're going to work some of those wonders, starting...
...now. ...now.
@ -58,14 +58,14 @@ pointless.
Notice that the above definition most emphatically does *not* say Notice that the above definition most emphatically does *not* say
anything about making the software as fast as possible. It also does not anything about making the software as fast as possible. It also does not
say anything about using assembly language, or an optimizing compiler, say anything about using assembly language, or an optimizing compiler,
or, for that matter, a compiler at all. It also doesn’t say anything 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 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*. 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's an important distinction, because all too many programmers think
that assembly language, or the right compiler, or a particular that assembly language, or the right compiler, or a particular
high-level language, or a certain design approach is the answer to high-level language, or a certain design approach is the answer to
creating high-performance code. They’re not, any more than choosing a 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 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 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, need a blueprint, an understanding of everything that goes into a house,
@ -80,13 +80,13 @@ assembly language. The optimization at the end is just the finishing
touch, however. touch, however.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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*. ![](images/i.jpg) *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 "What's a fast slow program?" you ask. That's a good question, and a
brief (true) story is perhaps the best answer. brief (true) story is perhaps the best answer.
#### When Fast Isn’t Fast {#Heading4} #### When Fast Isn't Fast {#Heading4}
In the early 1970s, as the first hand-held calculators were hitting the In the early 1970s, as the first hand-held calculators were hitting the
market, I knew a fellow named Irwin. He was a good student, and was market, I knew a fellow named Irwin. He was a good student, and was
@ -98,21 +98,21 @@ calculator to a duel—and won, becoming a local legend in the process.
When you get right down to it, though, Irwin was spitting into the wind. When you get right down to it, though, Irwin was spitting into the wind.
In a few short years his hard-earned slipstick skills would be In a few short years his hard-earned slipstick skills would be
worthless, and the entire discipline would be essentially wiped from the worthless, and the entire discipline would be essentially wiped from the
face of the earth. What’s more, anyone with half a brain could see that face of the earth. What's more, anyone with half a brain could see that
changeover coming. Irwin had basically wasted the considerable effort changeover coming. Irwin had basically wasted the considerable effort
and time he had spent optimizing his soon-to-be-obsolete skills. and time he had spent optimizing his soon-to-be-obsolete skills.
What does all this have to do with programming? Plenty. When you spend What does all this have to do with programming? Plenty. When you spend
time optimizing poorly-designed assembly code, or when you count on an time optimizing poorly-designed assembly code, or when you count on an
optimizing compiler to make your code fast, you’re wasting the optimizing compiler to make your code fast, you're wasting the
optimization, much as Irwin did. Particularly in assembly, you’ll find optimization, much as Irwin did. Particularly in assembly, you'll find
that without proper up-front design and everything else that goes into that without proper up-front design and everything else that goes into
high-performance design, you’ll waste considerable effort and time on high-performance design, you'll waste considerable effort and time on
making an inherently slow program as fast as possible—which is still making an inherently slow program as fast as possible—which is still
slow—when you could easily have improved performance a great deal more slow—when you could easily have improved performance a great deal more
with just a little thought. As we’ll see, handcrafted assembly language with just a little thought. As we'll see, handcrafted assembly language
and optimizing compilers matter, but less than you might think, in the and optimizing compilers matter, but less than you might think, in the
grand scheme of things—and they scarcely matter at all unless they’re grand scheme of things—and they scarcely matter at all unless they're
used in the context of a good design and a thorough understanding of used in the context of a good design and a thorough understanding of
both the task at hand and the PC. both the task at hand and the PC.

View file

@ -4,9 +4,9 @@
### Rules for Building High-Performance Code {#Heading5} ### Rules for Building High-Performance Code {#Heading5}
We’ve got the following rules for creating high-performance software: We've got the following rules for creating high-performance software:
- Know where you’re going (understand the objective of the software). - Know where you're going (understand the objective of the software).
- Make a big map (have an overall program design firmly in mind, so - Make a big map (have an overall program design firmly in mind, so
the various parts of the program and the data structures work well the various parts of the program and the data structures work well
together). together).
@ -15,29 +15,29 @@ We’ve got the following rules for creating high-performance software:
- Know the territory (understand exactly how the computer carries out - Know the territory (understand exactly how the computer carries out
each task). each task).
- Know when it matters (identify the portions of your programs where - Know when it matters (identify the portions of your programs where
performance matters, and don’t waste your time optimizing the rest). performance matters, and don't waste your time optimizing the rest).
- Always consider the alternatives (don’t get stuck on a single - Always consider the alternatives (don't get stuck on a single
approach; odds are there’s a better way, if you’re clever and approach; odds are there's a better way, if you're clever and
inventive enough). inventive enough).
- Know how to turn on the juice (optimize the code as best you know - Know how to turn on the juice (optimize the code as best you know
how when it *does* matter). how when it *does* matter).
Making rules is easy; the hard part is figuring out how to apply them in Making rules is easy; the hard part is figuring out how to apply them in
the real world. For my money, examining some actual working code is the real world. For my money, examining some actual working code is
always a good way to get a handle on programming concepts, so let’s look always a good way to get a handle on programming concepts, so let's look
at some of the performance rules in action. at some of the performance rules in action.
#### Know Where You’re Going {#Heading6} #### Know Where You're Going {#Heading6}
If we’re going to create high-performance code, first we have to know If we're going to create high-performance code, first we have to know
what that code is going to do. As an example, let’s write a program that what that code is going to do. As an example, let's write a program that
generates a 16-bit checksum of the bytes in a file. In other words, the generates a 16-bit checksum of the bytes in a file. In other words, the
program will add each byte in a specified file in turn into a 16-bit program will add each byte in a specified file in turn into a 16-bit
value. This checksum value might be used to make sure that a file hasn’t value. This checksum value might be used to make sure that a file hasn't
been corrupted, as might occur during transmission over a modem or if a been corrupted, as might occur during transmission over a modem or if a
Trojan horse virus rears its ugly head. We’re not going to do anything Trojan horse virus rears its ugly head. We're not going to do anything
with the checksum value other than print it out, however; right now with the checksum value other than print it out, however; right now
we’re only interested in generating that checksum value as rapidly as we're only interested in generating that checksum value as rapidly as
possible. possible.
#### Make a Big Map {#Heading7} #### Make a Big Map {#Heading7}
@ -50,27 +50,27 @@ bytes and adding them together.
#### Make Lots of Little Maps {#Heading8} #### Make Lots of Little Maps {#Heading8}
Actually, we’re only going to make one little map, because we only have Actually, we're only going to make one little map, because we only have
one program section that requires much thought—the section that reads one program section that requires much thought—the section that reads
the bytes and adds them up. What’s the best way to do this? the bytes and adds them up. What's the best way to do this?
It would be convenient to load the entire file into memory and then sum It would be convenient to load the entire file into memory and then sum
the bytes in one loop. Unfortunately, there’s no guarantee that any the bytes in one loop. Unfortunately, there's no guarantee that any
particular file will fit in the available memory; in fact, it’s a sure particular file will fit in the available memory; in fact, it's a sure
thing that many files *won’t* fit into memory, so that approach is out. thing that many files *won't* fit into memory, so that approach is out.
Well, if the whole file won’t fit into memory, one byte surely will. If Well, if the whole file won't fit into memory, one byte surely will. If
we read the file one byte at a time, adding each byte to the checksum we read the file one byte at a time, adding each byte to the checksum
value before reading the next byte, we’ll minimize memory requirements value before reading the next byte, we'll minimize memory requirements
and be able to handle any size file at all. and be able to handle any size file at all.
Sounds good, eh? Listing 1.1 shows an implementation of this approach. Sounds good, eh? Listing 1.1 shows an implementation of this approach.
Listing 1.1 uses C’s **read()** function to read a single byte, adds the Listing 1.1 uses C's **read()** function to read a single byte, adds the
byte into the checksum value, and loops back to handle the next byte byte into the checksum value, and loops back to handle the next byte
until the end of the file is reached. The code is compact, easy to until the end of the file is reached. The code is compact, easy to
write, and functions perfectly—with one slight hitch: write, and functions perfectly—with one slight hitch:
It’s *slow*. It's *slow*.
**LISTING 1.1 L1-1.C** **LISTING 1.1 L1-1.C**
@ -89,11 +89,11 @@ It’s *slow*.
int ReadLength; int ReadLength;
if ( argc != 2 ) { if ( argc != 2 ) {
printf(“usage: checksum filename\n”); printf("usage: checksum filename\n");
exit(1); exit(1);
} }
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) { if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(“Can’t open file: %s\n”, argv[1]); printf("Can't open file: %s\n", argv[1]);
exit(1); exit(1);
} }
@ -105,13 +105,13 @@ It’s *slow*.
Checksum += (unsigned int) Byte; Checksum += (unsigned int) Byte;
} }
if ( ReadLength == -1 ) { if ( ReadLength == -1 ) {
printf(“Error reading file %s\n”, argv[1]); printf("Error reading file %s\n", argv[1]);
exit(1); exit(1);
} }
/* Report the result */ /* Report the result */
printf(“The checksum is: %u\n”, Checksum); printf("The checksum is: %u\n", Checksum);
exit(0); exit(0);
} }
@ -127,14 +127,14 @@ requires over two and one-half minutes to checksum *one* file!
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 optimization to make your programs fast. Listing 1.1 is simply poorly
designed, and no amount of compiler optimization will compensate for designed, and no amount of compiler optimization will compensate for
that failing. To drive home the point, conListings 1.2 and 1.3, which 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 together are equivalent to Listing 1.1 except that the entire checksum
loop is written in tight assembly code. The assembly language loop is written in tight assembly code. The assembly language
implementation is indeed faster than any of the C versions, as shown in 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 Table 1.1, but it's less than 10 percent faster, and it's still
unacceptably slow. unacceptably slow.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -86,11 +86,11 @@ Ratio best\
57.44 57.44
**Note:** The execution times (in seconds) for this chapter’s listings **Note:** The execution times (in seconds) for this chapter's listings
were timed when the compiled listings were run on the WordPerfect 4.2 were timed when the compiled listings were run on the WordPerfect 4.2
thesaurus file TH.WP (362,293 bytes in size), as compiled in the small thesaurus file TH.WP (362,293 bytes in size), as compiled in the small
model with Borland and Microsoft compilers with optimization on (opt) model with Borland and Microsoft compilers with optimization on (opt)
and off (no opt). All times were measured with Paradigm Systems’ TIMER and off (no opt). All times were measured with Paradigm Systems' TIMER
program on a 10 MHz 1-wait-state AT clone with a 28-ms hard disk, with program on a 10 MHz 1-wait-state AT clone with a 28-ms hard disk, with
disk caching turned off. disk caching turned off.
@ -118,20 +118,20 @@ Table 1.1 Execution Times for WordPerfect Checksum.
int ReadLength; int ReadLength;
if ( argc != 2 ) { if ( argc != 2 ) {
printf(“usage: checksum filename\n”); printf("usage: checksum filename\n");
exit(1); exit(1);
} }
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) { if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(“Can’t open file: %s\n”, argv[1]); printf("Can't open file: %s\n", argv[1]);
exit(1); exit(1);
} }
if ( !ChecksumFile(Handle, &Checksum) ) { if ( !ChecksumFile(Handle, &Checksum) ) {
printf(“Error reading file %s\n”, argv[1]); printf("Error reading file %s\n", argv[1]);
exit(1); exit(1);
} }
/* Report the result */ /* Report the result */
printf(“The checksum is: %u\n”, Checksum); printf("The checksum is: %u\n", Checksum);
exit(0); exit(0);
} }
@ -170,7 +170,7 @@ Table 1.1 Execution Times for WordPerfect Checksum.
_ChecksumFile proc near _ChecksumFile proc near
push bp push bp
mov bp,sp mov bp,sp
push si ;save C’s register variable push si ;save C's register variable
; ;
mov bx,[bp+Handle] ;get file handle mov bx,[bp+Handle] ;get file handle
sub si,si ;zero the checksum ;accumulator sub si,si ;zero the checksum ;accumulator
@ -183,7 +183,7 @@ Table 1.1 Execution Times for WordPerfect Checksum.
int 21h ;read the byte int 21h ;read the byte
jcErrorEnd;an error occurred jcErrorEnd;an error occurred
and ax,ax ;any bytes read? and ax,ax ;any bytes read?
jz Success ;no-end of file reached-we’re done jz Success ;no-end of file reached-we're done
add si,[TempWord] ;add the byte into the add si,[TempWord] ;add the byte into the
;checksum total ;checksum total
jmpChecksumLoop jmpChecksumLoop
@ -196,7 +196,7 @@ Table 1.1 Execution Times for WordPerfect Checksum.
mov ax,1 ;success mov ax,1 ;success
; ;
Done: Done:
pop si ;restore C’s register variable pop si ;restore C's register variable
pop bp pop bp
ret ret
_ChecksumFileendp _ChecksumFileendp
@ -206,7 +206,7 @@ The lesson is clear: Optimization makes code faster, but without proper
design, optimization just creates fast slow code. design, optimization just creates fast slow code.
Well, then, how are we going to improve our design? Before we can do Well, then, how are we going to improve our design? Before we can do
that, we have to understand what’s wrong with the current design. that, we have to understand what's wrong with the current design.
#### Know the Territory {#Heading9} #### Know the Territory {#Heading9}
@ -229,17 +229,17 @@ a *long* time—far, far longer than the rest of the main loop in Listing
**read(),** and most of that time is spent somewhere down in DOS. **read(),** and most of that time is spent somewhere down in DOS.
You can verify this for yourself by watching the code with a debugger or You can verify this for yourself by watching the code with a debugger or
using a code profiler, but take my word for it: There’s a great deal of using a code profiler, but take my word for it: There's a great deal of
overhead to DOS calls, and that’s what’s draining the life out of overhead to DOS calls, and that's what's draining the life out of
Listing 1.1. Listing 1.1.
How can we speed up Listing 1.1? It should be clear that we must somehow How can we speed up Listing 1.1? It should be clear that we must somehow
avoid invoking DOS for every byte in the file, and that means reading avoid invoking DOS for every byte in the file, and that means reading
more than one byte at a time, then buffering the data and parceling it more than one byte at a time, then buffering the data and parceling it
out for examination one byte at a time. By gosh, that’s a description of out for examination one byte at a time. By gosh, that's a description of
C’s stream I/O feature, whereby C reads files in chunks and buffers the C's stream I/O feature, whereby C reads files in chunks and buffers the
bytes internally, doling them out to the application as needed by bytes internally, doling them out to the application as needed by
reading them from memory rather than calling DOS. Let’s try using stream reading them from memory rather than calling DOS. Let's try using stream
I/O and see what happens. I/O and see what happens.
Listing 1.4 is similar to Listing 1.1, but uses **fopen()** and Listing 1.4 is similar to Listing 1.1, but uses **fopen()** and

View file

@ -24,11 +24,11 @@ libraries do their work. In other words, *know the territory*!
unsigned int Checksum; unsigned int Checksum;
if ( argc != 2 ) { if ( argc != 2 ) {
printf(“usage: checksum filename\n”); printf("usage: checksum filename\n");
exit(1); exit(1);
} }
if ( (CheckFile = fopen(argv[1], “rb”)) == NULL ) { if ( (CheckFile = fopen(argv[1], "rb")) == NULL ) {
printf(“Can’t open file: %s\n”, argv[1]); printf("Can't open file: %s\n", argv[1]);
exit(1); exit(1);
} }
@ -41,7 +41,7 @@ libraries do their work. In other words, *know the territory*!
} }
/* Report the result */ /* Report the result */
printf(“The checksum is: %u\n”, Checksum); printf("The checksum is: %u\n", Checksum);
exit(0); exit(0);
} }
@ -51,39 +51,39 @@ The last section contained a particularly interesting phrase: *the
time-critical portions of your code*. Time-critical portions of your time-critical portions of your code*. Time-critical portions of your
code are those portions in which the speed of the code makes a 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 difference in the overall performance of your program—and by
“significant,” I don’t mean that it makes the code 100 percent faster, "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 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 makes the program more responsive and/or usable *from the user's
perspective*. perspective*.
Don’t waste time optimizing non-time-critical code: set-up code, Don't waste time optimizing non-time-critical code: set-up code,
initialization code, and the like. Spend your time improving the initialization code, and the like. Spend your time improving the
performance of the code inside heavily-used loops and in the portions of performance of the code inside heavily-used loops and in the portions of
your programs that directly affect response time. Notice, for example, your programs that directly affect response time. Notice, for example,
that I haven’t bothered to implement a version of the checksum program 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 entirely in assembly; Listings 1.2 and 1.6 call assembly subroutines
that handle the time-critical operations, but C is still used for that handle the time-critical operations, but C is still used for
checking command-line parameters, operning files, printing, and the checking command-line parameters, operning files, printing, and the
like. like.
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 satisfaction, and that won't be the case until we've thought about other
approaches. approaches.
#### Always Consider the Alternatives {#Heading11} #### Always Consider the Alternatives {#Heading11}
Listing 1.4 is good, but let’s see if there are other—perhaps less Listing 1.4 is good, but let's see if there are other—perhaps less
obvious—ways to get the same results faster. Let’s start by considering obvious—ways to get the same results faster. Let's start by considering
why Listing 1.4 is so much better than Listing 1.1. Like **read()**, why Listing 1.4 is so much better than Listing 1.1. Like **read()**,
**getc()** calls DOS to read from the file; the speed improvement of **getc()** calls DOS to read from the file; the speed improvement of
Listing 1.4 over Listing 1.1 occurs because **getc()** eads many bytes Listing 1.4 over Listing 1.1 occurs because **getc()** eads many bytes
at once via DOS, then manages those bytes for us. That’s faster than at once via DOS, then manages those bytes for us. That's faster than
reading them one at a time using **read()**—but there’s no reason to reading them one at a time using **read()**—but there's no reason to
think that it’s faster than having our program read and manage blocks think that it's faster than having our program read and manage blocks
itself. Easier, yes, but not faster. itself. Easier, yes, but not faster.
Consider this: Every invocation of **getc()** involves pushing a Consider this: Every invocation of **getc()** involves pushing a
@ -100,19 +100,19 @@ to improve on Listing 1.4:
**1.**  The code is already fast enough. **1.**  The code is already fast enough.
**2.**  The code works, and some people are content with code that **2.**  The code works, and some people are content with code that
works, even when it’s slow enough to be annoying. works, even when it's slow enough to be annoying.
**3.**  The C library is written in optimized assembly, and it’s likely **3.**  The C library is written in optimized assembly, and it's likely
to be faster than any code that the average programmer could write to to be faster than any code that the average programmer could write to
perform essentially the same function. perform essentially the same function.
**4.**  The C library conveniently handles the buffering of file data, **4.**  The C library conveniently handles the buffering of file data,
and it would be a nuisance to have to implement that capability. and it would be a nuisance to have to implement that capability.
I’ll ignore the first reason, both because performance is no longer an I'll ignore the first reason, both because performance is no longer an
issue if the code is fast enough and because the current application issue if the code is fast enough and because the current application
does *not* run fast enough—13 seconds is a long time. (Stop and wait for does *not* run fast enough—13 seconds is a long time. (Stop and wait for
13 seconds while you’re doing something intense, and you’ll see just how 13 seconds while you're doing something intense, and you'll see just how
long it is.) long it is.)
The second reason is the hallmark of the mediocre programmer. Know when The second reason is the hallmark of the mediocre programmer. Know when

View file

@ -4,8 +4,8 @@
The third reason is often fallacious. C library functions are not always The third reason is often fallacious. C library functions are not always
written in assembly, nor are they always particularly well-optimized. written in assembly, nor are they always particularly well-optimized.
(In fact, they’re often written for *portability*, which has nothing to (In fact, they're often written for *portability*, which has nothing to
do with optimization.) What’s more, they’re general-purpose functions, do with optimization.) What's more, they're general-purpose functions,
and often can be outperformed by well-but-not- brilliantly-written code 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 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. 1.5, which uses internal buffering to handle blocks of bytes at a time.
@ -14,7 +14,7 @@ Table 1.1 shows that Listing 1.5 is 2.5 to 4 times faster than Listing
uses no assembly at all. uses no assembly at all.
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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** **LISTING 1.5 L1-5.C**
@ -38,17 +38,17 @@ uses no assembly at all.
int WorkingLength, LengthCount; int WorkingLength, LengthCount;
if ( argc != 2 ) { if ( argc != 2 ) {
printf(“usage: checksum filename\n”); printf("usage: checksum filename\n");
exit(1); exit(1);
} }
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) { if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(“Can’t open file: %s\n”, argv[1]); printf("Can't open file: %s\n", argv[1]);
exit(1); exit(1);
} }
/* Get memory in which to buffer the data */ /* Get memory in which to buffer the data */
if ( (WorkingBuffer = malloc(BUFFER_SIZE)) == NULL ) { if ( (WorkingBuffer = malloc(BUFFER_SIZE)) == NULL ) {
printf(“Can’t get enough memory\n”); printf("Can't get enough memory\n");
exit(1); exit(1);
} }
@ -59,7 +59,7 @@ uses no assembly at all.
do { do {
if ( (WorkingLength = read(Handle, WorkingBuffer, if ( (WorkingLength = read(Handle, WorkingBuffer,
BUFFER_SIZE)) == -1 ) { BUFFER_SIZE)) == -1 ) {
printf(“Error reading file %s\n”, argv[1]); printf("Error reading file %s\n", argv[1]);
exit(1); exit(1);
} }
/* Checksum this chunk */ /* Checksum this chunk */
@ -72,14 +72,14 @@ uses no assembly at all.
} while ( WorkingLength ); } while ( WorkingLength );
/* Report the result */ /* Report the result */
printf(“The checksum is: %u\n”, Checksum); printf("The checksum is: %u\n", Checksum);
exit(0); exit(0);
} }
That brings us to the fourth reason: avoiding an internal-buffered That brings us to the fourth reason: avoiding an internal-buffered
implementation like Listing 1.5 because of the difficulty of coding such implementation like Listing 1.5 because of the difficulty of coding such
an approach. True, it is easier to let a C library function do the work, an approach. True, it is easier to let a C library function do the work,
but it’s not all that hard to do the buffering internally. The key is but it's not all that hard to do the buffering internally. The key is
the concept of handling data in *restartable blocks;* that is, reading a the concept of handling data in *restartable blocks;* that is, reading a
chunk of data, operating on the data until it runs out, suspending the chunk of data, operating on the data until it runs out, suspending the
operation while more data is read in, and then continuing as though operation while more data is read in, and then continuing as though
@ -90,11 +90,11 @@ because checksumming works with one byte at a time, forgetting about
each byte immediately after adding it into the total. Listing 1.5 reads each byte immediately after adding it into the total. Listing 1.5 reads
in a block of bytes from the file, checksums the bytes in the block, and in a block of bytes from the file, checksums the bytes in the block, and
gets another block, repeating the process until the entire file has been gets another block, repeating the process until the entire file has been
processed. In Chapter 5, we’ll see a more complex restartable block processed. In Chapter 5, we'll see a more complex restartable block
implementation, involving searching for text strings. implementation, involving searching for text strings.
At any rate, Listing 1.5 isn’t much more complicated than Listing At any rate, Listing 1.5 isn't much more complicated than Listing
1.4—and it’s a *lot* faster. Always consider the alternatives; a bit of 1.4—and it's a *lot* faster. Always consider the alternatives; a bit of
clever thinking and program redesign can go a long way. clever thinking and program redesign can go a long way.
#### Know How to Turn On the Juice {#Heading12} #### Know How to Turn On the Juice {#Heading12}
@ -103,7 +103,7 @@ I have said time and again that optimization is pointless until the
design is settled. When that time comes, however, optimization can design is settled. When that time comes, however, optimization can
indeed make a significant difference. Table 1.1 indicates that the indeed make a significant difference. Table 1.1 indicates that the
optimized version of Listing 1.5 produced by Microsoft C outperforms an optimized version of Listing 1.5 produced by Microsoft C outperforms an
unoptimized version of the same code by more than 60 percent. What’s unoptimized version of the same code by more than 60 percent. What's
more, a mostly-assembly version of Listing 1.5, shown in Listings 1.6 more, a mostly-assembly version of Listing 1.5, shown in Listings 1.6
and 1.7, outperforms even the best-optimized C version of List1.5 by 26 and 1.7, outperforms even the best-optimized C version of List1.5 by 26
percent. These are considerable improvements, well worth pursuing—once percent. These are considerable improvements, well worth pursuing—once
@ -131,17 +131,17 @@ the design has been maxed out.
int WorkingLength; int WorkingLength;
if ( argc != 2 ) { if ( argc != 2 ) {
printf(“usage: checksum filename\n”); printf("usage: checksum filename\n");
exit(1); exit(1);
} }
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) { if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(“Can’t open file: %s\n”, argv[1]); printf("Can't open file: %s\n", argv[1]);
exit(1); exit(1);
} }
/* Get memory in which to buffer the data */ /* Get memory in which to buffer the data */
if ( (WorkingBuffer = malloc(BUFFER_SIZE)) == NULL ) { if ( (WorkingBuffer = malloc(BUFFER_SIZE)) == NULL ) {
printf(“Can’t get enough memory\n”); printf("Can't get enough memory\n");
exit(1); exit(1);
} }
@ -152,16 +152,16 @@ the design has been maxed out.
do { do {
if ( (WorkingLength = read(Handle, WorkingBuffer, if ( (WorkingLength = read(Handle, WorkingBuffer,
BUFFER_SIZE)) == -1 ) { BUFFER_SIZE)) == -1 ) {
printf(“Error reading file %s\n”, argv[1]); printf("Error reading file %s\n", argv[1]);
exit(1); exit(1);
} }
/* Checksum this chunk if there’s anything in it */ /* Checksum this chunk if there's anything in it */
if ( WorkingLength ) if ( WorkingLength )
ChecksumChunk(WorkingBuffer, WorkingLength, &Checksum); ChecksumChunk(WorkingBuffer, WorkingLength, &Checksum);
} while ( WorkingLength ); } while ( WorkingLength );
/* Report the result */ /* Report the result */
printf(“The checksum is: %u\n”, Checksum); printf("The checksum is: %u\n", Checksum);
exit(0); exit(0);
} }

View file

@ -34,7 +34,7 @@
_ChecksumChunkprocnear _ChecksumChunkprocnear
push bp push bp
mov bp,sp mov bp,sp
push si ;save C’s register variable push si ;save C's register variable
; ;
cld ;make LODSB increment SI cld ;make LODSB increment SI
mov si,[bp+Buffer] ;point to buffer mov si,[bp+Buffer] ;point to buffer
@ -48,7 +48,7 @@
loop ChecksumLoop ;continue for all bytes in block loop ChecksumLoop ;continue for all bytes in block
mov [bx],dx ;save the new checksum mov [bx],dx ;save the new checksum
; ;
pop si ;restore C’s register variable pop si ;restore C's register variable
pop bp pop bp
ret ret
_ChecksumChunkendp _ChecksumChunkendp
@ -58,7 +58,7 @@ 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. 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 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 and/or the C library, so optimization of the code you write is pretty
much irrelevant. What’s more, while the approximately two-times much irrelevant. What's more, while the approximately two-times
improvement we got by optimizing is not to be sneezed at, it pales improvement we got by optimizing is not to be sneezed at, it pales
against the up-to-50-times improvement we got by redesigning. against the up-to-50-times improvement we got by redesigning.
@ -74,41 +74,41 @@ tends to be considerably faster relative to C than it is in this very
specific case. specific case.
------------------- ----------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Don’t get hung up on optimizing compilers or assembly language—the best optimizer is between your ears.* ![](images/i.jpg) *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 All this is basically a way of saying: Know where you're going, know the
territory, and know when it matters. territory, and know when it matters.
### Where We’ve Been, What We’ve Seen {#Heading13} ### Where We've Been, What We've Seen {#Heading13}
What have we learned? Don’t let other people’s code—even DOS—do the work What have we learned? Don't let other people's code—even DOS—do the work
for you when speed matters, at least not without knowing what that code for you when speed matters, at least not without knowing what that code
does and how well it performs. does and how well it performs.
Optimization only matters after you’ve done your part on the program Optimization only matters after you've done your part on the program
design end. Consider the ratios on the vertical axis of Table 1.1, which design end. Consider the ratios on the vertical axis of Table 1.1, which
show that optimization is almost totally wasted in the checksumming show that optimization is almost totally wasted in the checksumming
application without an efficient design. Optimization is no panacea. application without an efficient design. Optimization is no panacea.
Table 1.1 shows a two-times improvement from optimization—and a Table 1.1 shows a two-times improvement from optimization—and a
50-times-plus improvement from redesign. The longstanding debate about 50-times-plus improvement from redesign. The longstanding debate about
which C compiler optimizes code best doesn’t matter quite so much in which C compiler optimizes code best doesn't matter quite so much in
light of Table 1.1, does it? Your organic optimizer matters much more light of Table 1.1, does it? Your organic optimizer matters much more
than your compiler’s optimizer, and there’s always assembly for those than your compiler's optimizer, and there's always assembly for those
usually small sections of code where performance really matters. usually small sections of code where performance really matters.
#### Where We’re Going {#Heading14} #### Where We're Going {#Heading14}
This chapter has presented a quick step-by-step overview of the design This chapter has presented a quick step-by-step overview of the design
process. I’m not claiming that this is the only way to create process. I'm not claiming that this is the only way to create
high-performance code; it’s just an approach that works for me. Create high-performance code; it's just an approach that works for me. Create
code however you want, but never forget that design matters more than code however you want, but never forget that design matters more than
detailed optimization. Never stop looking for inventive ways to boost detailed optimization. Never stop looking for inventive ways to boost
performance—and never waste time speeding up code that doesn’t need to performance—and never waste time speeding up code that doesn't need to
be sped up. be sped up.
I’m going to focus on specific ways to create high-performance code from I'm going to focus on specific ways to create high-performance code from
now on. In Chapter 5, we’ll continue to look at restartable blocks and now on. In Chapter 5, we'll continue to look at restartable blocks and
internal buffering, in the form of a program that searches files for internal buffering, in the form of a program that searches files for
text strings. text strings.

View file

@ -9,17 +9,17 @@ Chapter 2\
### The Unique Nature of Assembly Language Optimization {#Heading2} ### The Unique Nature of Assembly Language Optimization {#Heading2}
As I showed in the previous chapter, optimization is by no means always As I showed in the previous chapter, optimization is by no means always
a matter of “dropping into assembly.” In fact, in performance tuning a matter of "dropping into assembly." In fact, in performance tuning
high-level language code, assembly should be used rarely, and then only high-level language code, assembly should be used rarely, and then only
after you’ve made sure a badly chosen or clumsily implemented algorithm after you've made sure a badly chosen or clumsily implemented algorithm
isn’t eating you alive. Certainly if you use assembly at all, make isn't eating you alive. Certainly if you use assembly at all, make
absolutely sure you use it *right*. The potential of assembly code to absolutely sure you use it *right*. The potential of assembly code to
run *slowly* is poorly understood by a lot of people, but that potential run *slowly* is poorly understood by a lot of people, but that potential
is great, especially in the hands of the ignorant. is great, especially in the hands of the ignorant.
Truly great optimization, however, happens *only* at the assembly level, Truly great optimization, however, happens *only* at the assembly level,
and it happens in response to a set of dynamics that is totally and it happens in response to a set of dynamics that is totally
different from that governing C/C++ or Pascal optimization. I’ll be different from that governing C/C++ or Pascal optimization. I'll be
speaking of assembly-level optimization time and again in this book, but speaking of assembly-level optimization time and again in this book, but
when I do, I think it will be helpful if you have a grasp of those when I do, I think it will be helpful if you have a grasp of those
assembly specific dynamics. assembly specific dynamics.
@ -32,7 +32,7 @@ Some time ago, I was asked to work over a critical assembly subroutine
in order to make it run as fast as possible. The task of the subroutine in order to make it run as fast as possible. The task of the subroutine
was to construct a nibble out of four bits read from different bytes, was to construct a nibble out of four bits read from different bytes,
rotating and combining the bits so that they ultimately ended up neatly rotating and combining the bits so that they ultimately ended up neatly
aligned in bits 3-0 of a single byte. (In case you’re curious, the aligned in bits 3-0 of a single byte. (In case you're curious, the
object was to construct a 16-color pixel from bits scattered over 4 object was to construct a 16-color pixel from bits scattered over 4
bytes.) I examined the subroutine line by line, saving a cycle here and bytes.) I examined the subroutine line by line, saving a cycle here and
a cycle there, until the code truly seemed to be optimized. When I was a cycle there, until the code truly seemed to be optimized. When I was
@ -47,7 +47,7 @@ done, the key part of the code looked something like this:
dec dx ;count down the number of bits dec dx ;count down the number of bits
jnz LoopTop ;process the next bit, if any jnz LoopTop ;process the next bit, if any
Now, it’s hard to write code that’s much faster than seven instructions, 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 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 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 a bit of time going over the code again. Suddenly, the answer struck
@ -76,9 +76,9 @@ performed just once, rather than four times. While the code may not look
much different from the original, and in fact still contains exactly the much different from the original, and in fact still contains exactly the
same number of instructions, the performance of the entire subroutine same number of instructions, the performance of the entire subroutine
improved by about 10 percent from just this one change. (Incidentally, improved by about 10 percent from just this one change. (Incidentally,
that wasn’t the end of the optimization; I eliminated the **DEC** and that wasn't the end of the optimization; I eliminated the **DEC** and
**JNJ** instructions by expanding the four iterations of the loop—but **JNJ** instructions by expanding the four iterations of the loop—but
that’s a tale for another chapter.) that's a tale for another chapter.)
The point is this: To write truly superior assembly programs, you need The point is this: To write truly superior assembly programs, you need
to know what the various instructions do and which instructions execute to know what the various instructions do and which instructions execute

View file

@ -38,11 +38,11 @@ computer—*but properly constructed assembly programs suffer no
transformation loss*, as shown in Figure 2.2. transformation loss*, as shown in Figure 2.2.
Only one transformation is required when creating an assembler program, Only one transformation is required when creating an assembler program,
and that single transformation is completely under the programmer’s and that single transformation is completely under the programmer's
control. Assemblers perform no transformation from source code to control. Assemblers perform no transformation from source code to
machine language; instead, they merely map assembler instructions to machine language; instead, they merely map assembler instructions to
machine language instructions on a one-to-one basis. As a result, the machine language instructions on a one-to-one basis. As a result, the
programmer is able to produce machine language code that’s precisely programmer is able to produce machine language code that's precisely
tailored to the needs of each task a given application requires. tailored to the needs of each task a given application requires.
![](images/02-02.jpg)\ ![](images/02-02.jpg)\
@ -88,7 +88,7 @@ enough.
The single most critical aspect of the hardware, and the one about which 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, 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 irregular instruction set, and, unlike most processors, they are neither
straightforward nor wellregarding true code performance. What’s more, straightforward nor wellregarding true code performance. What's more,
assembly is so difficult to learn that most articles and books that assembly is so difficult to learn that most articles and books that
present assembly code settle for code that just works, rather than code 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 that pushes the CPU to its limits. In fact, since most articles and
@ -100,7 +100,7 @@ knowledge to gather. A good portion of this book is devoted to seeking
out such knowledge. out such knowledge.
------------------- ------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Be forewarned, though: No matter how much you learn about programming the PC in assembly, there’s always more to discover.* ![](images/i.jpg) *Be forewarned, though: No matter how much you learn about programming the PC in assembly, there's always more to discover.*
------------------- ------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -6,7 +6,7 @@
Is the never-ending collection of information all there is to the Is the never-ending collection of information all there is to the
assembly optimization, then? Hardly. Knowledge is simply a necessary assembly optimization, then? Hardly. Knowledge is simply a necessary
base on which to build. Let’s take a moment to examine the objectives of base on which to build. Let's take a moment to examine the objectives of
good assembly programming, and the remainder of the forces that act on good assembly programming, and the remainder of the forces that act on
assembly optimization will fall into place. assembly optimization will fall into place.
@ -14,12 +14,12 @@ Basically, there are only two possible objectives to high-performance
assembly programming: Given the requirements of the application, keep to assembly programming: Given the requirements of the application, keep to
a minimum either the number of processor cycles the program takes 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. 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 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 concerned with saving cycles than saving bytes, for the PC generally
offers relatively more memory than it does processing horsepower. In offers relatively more memory than it does processing horsepower. In
fact, we’ll find that two-to-three times performance improvements *over fact, we'll find that two-to-three times performance improvements *over
already tight assembly code* are often possible if we’re willing to already tight assembly code* are often possible if we're willing to
spend additional bytes in order to save cycles. It’s not always 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 desirable to use such techniques to speed up code, due to the heavy
memory requirements—but it is almost always *possible*. memory requirements—but it is almost always *possible*.
@ -32,7 +32,7 @@ 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 how it is maintained. These days, developers spend so much time focusing
on such admittedly important issues as code maintainability and on such admittedly important issues as code maintainability and
reusability, source code control, choice of development environment, and reusability, source code control, choice of development environment, and
the like that they often forget rule \#1: From the user’s perspective, the like that they often forget rule \#1: From the user's perspective,
*performance is fundamental*. *performance is fundamental*.
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -41,7 +41,7 @@ the like that they often forget rule \#1: From the user’s perspective,
Knowledge of the sort described earlier is absolutely essential to Knowledge of the sort described earlier is absolutely essential to
fulfilling either of the objectives of assembly programming. What that 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 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 performs to the requirements of the application at hand and also
operates as efficiently as possible in the PC environment. Knowledge operates as efficiently as possible in the PC environment. Knowledge
makes that possible, but your programming instincts make it happen. And makes that possible, but your programming instincts make it happen. And
@ -63,7 +63,7 @@ doing.
Never underestimate the importance of the flexible mind. Good assembly Never underestimate the importance of the flexible mind. Good assembly
code is better than good compiled code. Many people would have you code is better than good compiled code. Many people would have you
believe otherwise, but they’re wrong. That doesn’t mean that high-level believe otherwise, but they're wrong. That doesn't mean that high-level
languages are useless; far from it. High-level languages are the best languages are useless; far from it. High-level languages are the best
choice for the majority of programmers, and for the bulk of the code of choice for the majority of programmers, and for the bulk of the code of
most applications. When the *best* code—the fastest or smallest code most applications. When the *best* code—the fastest or smallest code

View file

@ -8,22 +8,22 @@ Chapter 3\
### Understanding and Using the Zen Timer {#Heading2} ### Understanding and Using the Zen Timer {#Heading2}
When you’re pushing the envelope in writing optimized PC code, you’re When you're pushing the envelope in writing optimized PC code, you're
likely to become more than a little compulsive about finding approaches likely to become more than a little compulsive about finding approaches
that let you wring more speed from your computer. In the process, you’re that let you wring more speed from your computer. In the process, you're
bound to make mistakes, which is fine—as long as you watch for those bound to make mistakes, which is fine—as long as you watch for those
mistakes and *learn* from them. mistakes and *learn* from them.
A case in point: A few years back, I came across an article about 8088 A case in point: A few years back, I came across an article about 8088
assembly language called “Optimizing for Speed.” Now, “optimize” is not assembly language called "Optimizing for Speed." Now, "optimize" is not
a word to be used lightly; *Webster’s Ninth New Collegiate Dictionary* a word to be used lightly; *Webster's Ninth New Collegiate Dictionary*
defines optimize as “to make as perfect, effective, or functional as defines optimize as "to make as perfect, effective, or functional as
possible,” which certainly leaves little room for error. The author had, possible," which certainly leaves little room for error. The author had,
however, chosen a small, well-defined 8088 assembly language routine to however, chosen a small, well-defined 8088 assembly language routine to
refine, consisting of about 30 instructions that did nothing more than refine, consisting of about 30 instructions that did nothing more than
expand 8 bits to 16 bits by duplicating each bit. expand 8 bits to 16 bits by duplicating each bit.
The author of “Optimizing” had clearly fine-tuned the code with care, The author of "Optimizing" had clearly fine-tuned the code with care,
examining alternative instruction sequences and adding up cycles until examining alternative instruction sequences and adding up cycles until
he arrived at an implementation he calculated to be nearly 50 percent he arrived at an implementation he calculated to be nearly 50 percent
faster than the original routine. In short, he had used all the faster than the original routine. In short, he had used all the
@ -44,20 +44,20 @@ code performance is instruction *fetch* time, a topic to which I will
return in later chapters. return in later chapters.
Had the author taken the time to measure the true performance of his 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 code, he wouldn't have put his reputation on the line with relatively
low-performance code. What’s more, had he actually measured the low-performance code. What's more, had he actually measured the
performance of his code and found it to be unexpectedly slow, curiosity 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 might well have led him to experiment further and thereby add to his
store of reliable information about the CPU. store of reliable information about the CPU.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 *Assume nothing*. I cannot emphasize this strongly enough—when you care
about performance, do your best to improve the code and then *measure* about performance, do your best to improve the code and then *measure*
the improvement. If you don’t measure performance, you’re just guessing, 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. 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 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 for a living, I spent days at a time trying to wring more performance
@ -65,7 +65,7 @@ from my graphics drivers. I rewrote whole sections of code just to save
a few cycles, juggled registers, and relied heavily on blurry-fast 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 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 discovered that the program ran perceptibly faster if I used look-up
tables instead of shifts and adds for my calculations. It *shouldn’t* 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, 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 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 the fetching of the shifts and adds was taking as much as four times the
@ -88,7 +88,7 @@ measure of code performance is observing it in action.
Clearly, one key to mastering Zen-class optimization is a tool with Clearly, one key to mastering Zen-class optimization is a tool with
which to measure code performance. The most accurate way to measure which to measure code performance. The most accurate way to measure
performance is with expensive hardware, but reasonable measurements at performance is with expensive hardware, but reasonable measurements at
no cost can be made with the PC’s 8253 timer chip, which counts at a no cost can be made with the PC's 8253 timer chip, which counts at a
rate of slightly over 1,000,000 times per second. The 8253 can be rate of slightly over 1,000,000 times per second. The 8253 can be
started at the beginning of a block of code of interest and stopped at started at the beginning of a block of code of interest and stopped at
the end of that code, with the resulting count indicating how long the the end of that code, with the resulting count indicating how long the
@ -99,8 +99,8 @@ one billionth of a second, and is abbreviated ns.)
Listing 3.1 shows 8253-based timer software, consisting of three Listing 3.1 shows 8253-based timer software, consisting of three
subroutines: **ZTimerOn, ZTimerOff**, and **ZTimerReport**. For the subroutines: **ZTimerOn, ZTimerOff**, and **ZTimerReport**. For the
remainder of this book, I’ll refer to these routines collectively as the remainder of this book, I'll refer to these routines collectively as the
“Zen timer.” C-callable versions of the two precision Zen timers are "Zen timer." C-callable versions of the two precision Zen timers are
presented in Chapter K on the companion CD-ROM. presented in Chapter K on the companion CD-ROM.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -35,10 +35,10 @@
; ;
; Note: These routines can introduce slight inaccuracies into the ; Note: These routines can introduce slight inaccuracies into the
; system clock count for each code section timed even if ; system clock count for each code section timed even if
; timer 0 doesn’t overflow. If timer 0 does overflow, the ; timer 0 doesn't overflow. If timer 0 does overflow, the
; system clock can become slow by virtually any amount of ; system clock can become slow by virtually any amount of
; time, since the system clock can’t advance while the ; time, since the system clock can't advance while the
; precison timer is timing. Consequently, it’s a good idea ; precison timer is timing. Consequently, it's a good idea
; to reboot at the end of each timing session. (The ; to reboot at the end of each timing session. (The
; battery-backed clock, if any, is not affected by the Zen ; battery-backed clock, if any, is not affected by the Zen
; timer.) ; timer.)
@ -49,7 +49,7 @@
; in when ZTimerOn was called. ; in when ZTimerOn was called.
; ;
Code segment word public ‘CODE’ Code segment word public ‘CODE'
assumecs: Code, ds:nothing assumecs: Code, ds:nothing
public ZTimerOn, ZTimerOff, ZTimerReport public ZTimerOn, ZTimerOff, ZTimerReport
@ -117,26 +117,26 @@
OutputStr label byte OutputStr label byte
db 0dh, 0ah, ‘Timed count: ‘, 5 dup (?) db 0dh, 0ah, ‘Timed count: ‘, 5 dup (?)
ASCIICountEnd labelbyte ASCIICountEnd labelbyte
db ‘ microseconds’, 0dh, 0ah db ‘ microseconds', 0dh, 0ah
db ‘$’ db ‘$'
; ;
; String printed to report timer overflow. ; String printed to report timer overflow.
; ;
OverflowStr label byte OverflowStr label byte
db 0dh, 0ah db 0dh, 0ah
db ‘****************************************************’ db ‘****************************************************'
db 0dh, 0ah db 0dh, 0ah
db ‘* The timer overflowed, so the interval timed was *’ db ‘* The timer overflowed, so the interval timed was *'
db 0dh, 0ah db 0dh, 0ah
db ‘* too long for the precision timer to measure. *’ db ‘* too long for the precision timer to measure. *'
db 0dh, 0ah db 0dh, 0ah
db ‘* Please perform the timing test again with the *’ db ‘* Please perform the timing test again with the *'
db0dh, 0ah db0dh, 0ah
db ‘* long-period timer. *’ db ‘* long-period timer. *'
db 0dh, 0ah db 0dh, 0ah
db ‘****************************************************’ db ‘****************************************************'
db 0dh, 0ah db 0dh, 0ah
db ‘$’ db ‘$'
; ******************************************************************** ; ********************************************************************
; * Routine called to start timing. * ; * Routine called to start timing. *
@ -158,7 +158,7 @@
; to 0 ; to 0
push ax push ax
; ;
; Turn on interrupts, so the timer interrupt can occur if it’s ; Turn on interrupts, so the timer interrupt can occur if it's
; pending. ; pending.
; ;
sti sti
@ -171,7 +171,7 @@
mov al,00110100b ;mode 2 mov al,00110100b ;mode 2
out MODE_8253,al out MODE_8253,al
; ;
; Set the timer count to 0, so we know we won’t get another ; Set the timer count to 0, so we know we won't get another
; timer interrupt right away. ; timer interrupt right away.
; Note: this introduces an inaccuracy of up to 54 ms in the system ; Note: this introduces an inaccuracy of up to 54 ms in the system
; clock count each time it is executed. ; clock count each time it is executed.
@ -418,7 +418,7 @@
CTSLoop: CTSLoop:
sub dx, dx sub dx, dx
div bx div bx
add dl,’0’ add dl,'0'
mov [si],dl mov [si],dl
dec si dec si
loop CTSLoop loop CTSLoop

View file

@ -4,18 +4,18 @@
#### The Zen Timer Is a Means, Not an End {#Heading5} #### The Zen Timer Is a Means, Not an End {#Heading5}
We’re going to spend the rest of this chapter seeing what the Zen timer We're going to spend the rest of this chapter seeing what the Zen timer
can do, examining how it works, and learning how to use it. I’ll be can do, examining how it works, and learning how to use it. I'll be
using the Zen timer again and again over the course of this book, so using the Zen timer again and again over the course of this book, so
it’s essential that you learn what the Zen timer can do and how to use it's essential that you learn what the Zen timer can do and how to use
it. On the other hand, it is by no means essential that you understand it. On the other hand, it is by no means essential that you understand
exactly how the Zen timer works. (Interesting, yes; essential, no.) exactly how the Zen timer works. (Interesting, yes; essential, no.)
In other words, the Zen timer isn’t really part of the knowledge we In other words, the Zen timer isn't really part of the knowledge we
seek; rather, it’s one tool with which we’ll acquire that knowledge. seek; rather, it's one tool with which we'll acquire that knowledge.
Consequently, you shouldn’t worry if you don’t fully grasp the inner Consequently, you shouldn't worry if you don't fully grasp the inner
workings of the Zen timer. Instead, focus on learning how to *use* it, workings of the Zen timer. Instead, focus on learning how to *use* it,
and you’ll be on the right road. and you'll be on the right road.
#### Starting the Zen Timer {#Heading6} #### Starting the Zen Timer {#Heading6}
@ -23,9 +23,9 @@ and you’ll be on the right road.
**ZTimerOn** saves the context of the calling code, disables interrupts, **ZTimerOn** saves the context of the calling code, disables interrupts,
sets timer 0 of the 8253 to mode 2 (divide-by-N mode), sets the initial sets timer 0 of the 8253 to mode 2 (divide-by-N mode), sets the initial
timer count to 0, restores the context of the calling code, and returns. timer count to 0, restores the context of the calling code, and returns.
(I’d like to note that while Intel’s documentation for the 8253 seems to (I'd like to note that while Intel's documentation for the 8253 seems to
indicate that a timer won’t reset to 0 until it finishes counting down, indicate that a timer won't reset to 0 until it finishes counting down,
in actual practice, timers seem to reset to 0 as soon as they’re in actual practice, timers seem to reset to 0 as soon as they're
loaded.) loaded.)
Two aspects of **ZTimerOn** are worth discussing further. One point of Two aspects of **ZTimerOn** are worth discussing further. One point of
@ -45,16 +45,16 @@ enable interrupts during that time.
A second interesting point about **ZTimerOn** is that it may introduce A second interesting point about **ZTimerOn** is that it may introduce
some small inaccuracy into the system clock time whenever it is called. some small inaccuracy into the system clock time whenever it is called.
To understand why this is so, we need to examine the way in which both To understand why this is so, we need to examine the way in which both
the 8253 and the PC’s system clock (which keeps the current time) work. the 8253 and the PC's system clock (which keeps the current time) work.
The 8253 actually contains three timers, as shown in Figure 3.1. All The 8253 actually contains three timers, as shown in Figure 3.1. All
three timers are driven by the system board’s 14.31818 MHz crystal, three timers are driven by the system board's 14.31818 MHz crystal,
divided by 12 to yield a 1.19318 MHz clock to the timers, so the timers divided by 12 to yield a 1.19318 MHz clock to the timers, so the timers
count once every 838.1 ns. Each of the three timers counts down in a count once every 838.1 ns. Each of the three timers counts down in a
programmable way, generating a signal on its output pin when it counts programmable way, generating a signal on its output pin when it counts
down to 0. Each timer is capable of being halted at any time via a 0 down to 0. Each timer is capable of being halted at any time via a 0
level on its gate input; when a timer’s gate input is 1, that timer level on its gate input; when a timer's gate input is 1, that timer
counts constantly. All in all, the 8253’s timers are inherently very counts constantly. All in all, the 8253's timers are inherently very
flexible timing devices; unfortunately, much of that flexibility depends flexible timing devices; unfortunately, much of that flexibility depends
on how the timers are connected to external circuitry, and in the PC the on how the timers are connected to external circuitry, and in the PC the
timers are connected with specific purposes in mind. timers are connected with specific purposes in mind.
@ -65,7 +65,7 @@ is the only timer with a programmable gate input in the PC; that is,
timer 2 is the only timer that can be started and stopped under program timer 2 is the only timer that can be started and stopped under program
control in the manner specified by Intel. On the other hand, the control in the manner specified by Intel. On the other hand, the
*output* of timer 2 is connected to nothing other than the speaker. In *output* of timer 2 is connected to nothing other than the speaker. In
particular, timer 2 cannot generate an interrupt to get the 8088’s particular, timer 2 cannot generate an interrupt to get the 8088's
attention. attention.
Timer 1 is dedicated to providing dynamic RAM refresh, and should not be Timer 1 is dedicated to providing dynamic RAM refresh, and should not be
@ -114,16 +114,16 @@ Divide-by-N mode counts down by one from the initial count. When the
count reaches zero, the timer turns over and starts counting down again count reaches zero, the timer turns over and starts counting down again
without stopping, and a pulse is generated for a single clock period. without stopping, and a pulse is generated for a single clock period.
While the pulse is not held for nearly as long as in square wave mode, While the pulse is not held for nearly as long as in square wave mode,
it doesn’t matter, since the 8259 interrupt controller is configured in it doesn't matter, since the 8259 interrupt controller is configured in
the PC to be edgeand hence cares only about the existence of a pulse the PC to be edgeand hence cares only about the existence of a pulse
from timer 0, not the duration of the pulse. As a result, timer 0 from timer 0, not the duration of the pulse. As a result, timer 0
continues to generate timer interrupts in divide-by-N mode, and the continues to generate timer interrupts in divide-by-N mode, and the
system clock continues to maintain good time. system clock continues to maintain good time.
Why not use timer 2 instead of timer 0 for precision timing? After all, Why not use timer 2 instead of timer 0 for precision timing? After all,
timer 2 has a programmable gate input and isn’t used for anything but timer 2 has a programmable gate input and isn't used for anything but
sound generation. The problem with timer 2 is that its output can’t sound generation. The problem with timer 2 is that its output can't
generate an interrupt; in fact, timer 2 can’t do anything but drive the generate an interrupt; in fact, timer 2 can't do anything but drive the
speaker. We need the interrupt generated by the output of timer 0 to speaker. We need the interrupt generated by the output of timer 0 to
tell us when the count has overflowed, and we will see shortly that the tell us when the count has overflowed, and we will see shortly that the
timer interrupt also makes it possible to time much longer periods than timer interrupt also makes it possible to time much longer periods than

View file

@ -22,7 +22,7 @@ interrupt. Recall that **ZTimerOn** initially sets timer 0 to 0, in
order to allow for the longest possible period—about 54 ms—before timer order to allow for the longest possible period—about 54 ms—before timer
0 reaches 0 and generates the timer interrupt. 0 reaches 0 and generates the timer interrupt.
Now we’re ready to look at the ways in which the Zen timer can introduce Now we're ready to look at the ways in which the Zen timer can introduce
inaccuracy into the system clock. Since timer 0 is initially set to 0 by inaccuracy into the system clock. Since timer 0 is initially set to 0 by
the Zen timer, and since the system clock ticks only when timer 0 counts the Zen timer, and since the system clock ticks only when timer 0 counts
off 54.925 ms and reaches 0 again, an average inaccuracy of one-half of off 54.925 ms and reaches 0 again, an average inaccuracy of one-half of
@ -42,10 +42,10 @@ Zen timer. The 8259 interrupt controller is capable of remembering at
most one pending timer interrupt, so all timer interrupts after the most one pending timer interrupt, so all timer interrupts after the
first one during any given Zen timing interval are ignored. first one during any given Zen timing interval are ignored.
Consequently, if a timing interval exceeds 54.9 ms, the system clock Consequently, if a timing interval exceeds 54.9 ms, the system clock
effectively stops 54.9 ms after the timing interval starts and doesn’t effectively stops 54.9 ms after the timing interval starts and doesn't
restart until the timing interval ends, losing time all the while. restart until the timing interval ends, losing time all the while.
The effects on the system time of the Zen timer aren’t a matter for The effects on the system time of the Zen timer aren't a matter for
great concern, as they are temporary, lasting only until the next warm great concern, as they are temporary, lasting only until the next warm
or cold boot. System that have batteryclocks, (AT-style machines; that or cold boot. System that have batteryclocks, (AT-style machines; that
is, virtually all machines in common use) automatically reset the is, virtually all machines in common use) automatically reset the
@ -56,7 +56,7 @@ by at most a total of a few seconds, unless code that takes much longer
than 54 ms to run is timed (in which case the Zen timer will notify you than 54 ms to run is timed (in which case the Zen timer will notify you
that the code is too long to time). that the code is too long to time).
Nonetheless, it’s a good idea to reboot your computer at the end of each Nonetheless, it's a good idea to reboot your computer at the end of each
session with the Zen timer in order to make sure that the system clock session with the Zen timer in order to make sure that the system clock
is correct. is correct.
@ -83,12 +83,12 @@ including the state of the interrupt flag that was in effect when
**ZTimerOn** was called to start timing, and returns. **ZTimerOn** was called to start timing, and returns.
One interesting aspect of **ZTimerOff** is the manner in which timer 0 One interesting aspect of **ZTimerOff** is the manner in which timer 0
is stopped in order to read the timer count. We don’t actually have to is stopped in order to read the timer count. We don't actually have to
stop timer 0 to read the count; the 8253 provides a special latched read stop timer 0 to read the count; the 8253 provides a special latched read
feature for the specific purpose of reading the count while a time is feature for the specific purpose of reading the count while a time is
running. (That’s a good thing, too; we’ve no documented way to stop running. (That's a good thing, too; we've no documented way to stop
timer 0 if we wanted to, since its gate input isn’t connected. Later in timer 0 if we wanted to, since its gate input isn't connected. Later in
this chapter, though, we’ll see that timer 0 can be stopped after all.) this chapter, though, we'll see that timer 0 can be stopped after all.)
We simply tell the 8253 to latch the current count, and the 8253 does so We simply tell the 8253 to latch the current count, and the 8253 does so
without breaking stride. without breaking stride.
@ -113,7 +113,7 @@ ZTimerReport** can be called at any time right up until the next call to
You may want to use the Zen timer to measure several portions of a You may want to use the Zen timer to measure several portions of a
program while it executes normally, in which case it may not be program while it executes normally, in which case it may not be
desirable to have the text printed by **ZTimerReport** interfere with desirable to have the text printed by **ZTimerReport** interfere with
the program’s normal display. There are many ways to deal with this. One the program's normal display. There are many ways to deal with this. One
approach is removal of the invocations of the DOS print string function approach is removal of the invocations of the DOS print string function
(INT 21H with AH equal to 9) from **ZTimerReport**, instead running the (INT 21H with AH equal to 9) from **ZTimerReport**, instead running the
program under a debugger that supports screen flipping (such as Turbo program under a debugger that supports screen flipping (such as Turbo
@ -144,7 +144,7 @@ be stored in a buffer within the driver, to be dumped at a later time.
David Miller for passing the idea on to me.) David Miller for passing the idea on to me.)
You may well want to devise still other approaches better suited to your You may well want to devise still other approaches better suited to your
needs than those I’ve presented. Go to it! I’ve just thrown out a few needs than those I've presented. Go to it! I've just thrown out a few
possibilities to get you started. possibilities to get you started.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -19,10 +19,10 @@ transparent to the calling code.
If you do change the Zen timer routines to far procedures in order to If you do change the Zen timer routines to far procedures in order to
call them from code running in another segment, be sure to make *all* call them from code running in another segment, be sure to make *all*
the Zen timer routines far, including **ReferenceZTimerOn** and the Zen timer routines far, including **ReferenceZTimerOn** and
**ReferenceZTimerOff**. (You’ll have to put **FAR PTR** overrides on the **ReferenceZTimerOff**. (You'll have to put **FAR PTR** overrides on the
calls from **ZTimerOff** to the latter two routines if you do make them calls from **ZTimerOff** to the latter two routines if you do make them
far.) If the reference routines aren’t the same type—near or far—as the far.) If the reference routines aren't the same type—near or far—as the
other routines, they won’t reflect the true overhead incurred by other routines, they won't reflect the true overhead incurred by
starting and stopping the Zen timer. starting and stopping the Zen timer.
Please be aware that the inaccuracy that the Zen timer can introduce Please be aware that the inaccuracy that the Zen timer can introduce
@ -34,16 +34,16 @@ dynamic RAM refresh, and internal timing variations in the 8253 make it
perhaps more accurate to describe the Zen timer as measuring code perhaps more accurate to describe the Zen timer as measuring code
performance with an accuracy of better than 10µs. In fact, the Zen timer performance with an accuracy of better than 10µs. In fact, the Zen timer
is actually most accurate in assessing code performance when timing is actually most accurate in assessing code performance when timing
intervals longer than about 100 µs. At any rate, we’re most interested intervals longer than about 100 µs. At any rate, we're most interested
in using the Zen timer to assess the relative performance of various in using the Zen timer to assess the relative performance of various
code sequences—that is, using it to compare and tweak code—and the timer code sequences—that is, using it to compare and tweak code—and the timer
is more than accurate enough for that purpose. is more than accurate enough for that purpose.
The Zen timer works on all PC-compatible computers I’ve tested it on, The Zen timer works on all PC-compatible computers I've tested it on,
including XTs, ATs, PS/2 computers, and 386, 486, and Pentium-based including XTs, ATs, PS/2 computers, and 386, 486, and Pentium-based
machines. Of course, I haven’t been able to test it on *all* machines. Of course, I haven't been able to test it on *all*
PC-compatibles, but I don’t expect any problems; computers on which the PC-compatibles, but I don't expect any problems; computers on which the
Zen timer doesn’t run can’t truly be called “PC-compatible.” Zen timer doesn't run can't truly be called "PC-compatible."
On the other hand, there is certainly no guarantee that code performance On the other hand, there is certainly no guarantee that code performance
as measured by the Zen timer will be the same on compatible computers as as measured by the Zen timer will be the same on compatible computers as
@ -53,7 +53,7 @@ the opposite is true. For example, every PS/2 computer, even the
relatively slow Model 30, executes code much faster than does a PC or relatively slow Model 30, executes code much faster than does a PC or
XT. As another example, I set out to do the timings for my earlier book XT. As another example, I set out to do the timings for my earlier book
*Zen of Assembly Language* on an XTcomputer, only to find that the *Zen of Assembly Language* on an XTcomputer, only to find that the
computer wasn’t quite IBM-compatible regarding code performance. The computer wasn't quite IBM-compatible regarding code performance. The
differences were minor, mind you, but my experience illustrates the risk differences were minor, mind you, but my experience illustrates the risk
of assuming that a specific make of computer will perform in a certain of assuming that a specific make of computer will perform in a certain
way without actually checking. way without actually checking.
@ -66,7 +66,7 @@ computers.
### A Sample Use of the Zen Timer {#Heading11} ### A Sample Use of the Zen Timer {#Heading11}
Listing 3.2 shows a test-bed program for measuring code performance with Listing 3.2 shows a test-bed program for measuring code performance with
the Zen timer. This program sets DS equal to CS (for reasons we’ll the Zen timer. This program sets DS equal to CS (for reasons we'll
discuss shortly), includes the code to be measured from the file discuss shortly), includes the code to be measured from the file
TESTCODE, and calls **ZTimerReport** to display the timing results. TESTCODE, and calls **ZTimerReport** to display the timing results.
Consequently, the code being measured should be in the file TESTCODE, Consequently, the code being measured should be in the file TESTCODE,
@ -84,11 +84,11 @@ and should contain calls to **ZTimerOn** and **ZTimerOff** .
; ;
; By Michael Abrash ; By Michael Abrash
; ;
mystack segment para stack ‘STACK’ mystack segment para stack ‘STACK'
db 512 dup(?) db 512 dup(?)
mystack ends mystack ends
; ;
Code segment para public ‘CODE’ Code segment para public ‘CODE'
assume cs:Code, ds:Code assume cs:Code, ds:Code
extrnZTimerOn:near, ZTimerOff:near, ZTimerReport:near extrnZTimerOn:near, ZTimerOff:near, ZTimerReport:near
Start proc near Start proc near
@ -147,7 +147,7 @@ after the code in Listing 3.3 has been run.
; ;
call ZTimerOff call ZTimerOff
It’s worth noting that Listing 3.3 begins by jumping around the memory It's worth noting that Listing 3.3 begins by jumping around the memory
variable **MemVar**. This approach lets us avoid reproducing Listing 3.2 variable **MemVar**. This approach lets us avoid reproducing Listing 3.2
in its entirety for each code fragment we want to measure; by defining in its entirety for each code fragment we want to measure; by defining
any needed data right in the code segment and jumping around that data, any needed data right in the code segment and jumping around that data,
@ -155,8 +155,8 @@ each listing becomes self-contained and can be plugged directly into
Listing 3.2 as TESTCODE. Listing 3.2 sets DS equal to CS before doing Listing 3.2 as TESTCODE. Listing 3.2 sets DS equal to CS before doing
anything else precisely so that data can be embedded in code fragments anything else precisely so that data can be embedded in code fragments
being timed. Note that only after the initial jump is performed in being timed. Note that only after the initial jump is performed in
Listing 3.3 is the Zen timer started, since we don’t want to include the Listing 3.3 is the Zen timer started, since we don't want to include the
execution time of start-up code in the timing interval. That’s why the execution time of start-up code in the timing interval. That's why the
calls to **ZTimerOn** and **ZTimerOff** are in TESTCODE, not in calls to **ZTimerOn** and **ZTimerOff** are in TESTCODE, not in
PZTEST.ASM; this way, we have full control over which portion of PZTEST.ASM; this way, we have full control over which portion of
TESTCODE is timed, and we can keep set-up code and the like out of the TESTCODE is timed, and we can keep set-up code and the like out of the

View file

@ -11,8 +11,8 @@ PZTEST.EXE. PZTIME.BAT (Listing 3.4) assumes that the file PZTIMER.ASM
contains Listing 3.1, and the file PZTEST.ASM contains Listing 3.2. The contains Listing 3.1, and the file PZTEST.ASM contains Listing 3.2. The
command-line parameter to PZTIME.BAT is the name of the file to be command-line parameter to PZTIME.BAT is the name of the file to be
copied to TESTCODE and included into PZTEST.ASM. (Note that Turbo copied to TESTCODE and included into PZTEST.ASM. (Note that Turbo
Assembler can be substituted for MASM by replacing “masm” with “tasm” Assembler can be substituted for MASM by replacing "masm" with "tasm"
and “link” with “tlink” in Listing 3.4. The same is true of Listing and "link" with "tlink" in Listing 3.4. The same is true of Listing
3.7.) 3.7.)
**LISTING 3.4 PZTIME.BAT** **LISTING 3.4 PZTIME.BAT**
@ -26,7 +26,7 @@ and “link” with “tlink” in Listing 3.4. The same is true of Listing
rem * Zen timer program PZTEST.EXE to time the code named as the * rem * Zen timer program PZTEST.EXE to time the code named as the *
rem * command-line parameter. Listing 3.1 must be named * rem * command-line parameter. Listing 3.1 must be named *
rem * PZTIMER.ASM, and Listing 3.2 must be named PZTEST.ASM. To * rem * PZTIMER.ASM, and Listing 3.2 must be named PZTEST.ASM. To *
rem * time the code in LST3-3, you’d type the DOS command: * rem * time the code in LST3-3, you'd type the DOS command: *
rem * * rem * *
rem * pztime lst3-3 * rem * pztime lst3-3 *
rem * * rem * *
@ -57,7 +57,7 @@ and “link” with “tlink” in Listing 3.4. The same is true of Listing
:ckexist :ckexist
if exist %1 goto docopy if exist %1 goto docopy
echo *************************************************************** echo ***************************************************************
echo * The specified file, “%1,” doesn’t exist. * echo * The specified file, "%1," doesn't exist. *
echo *************************************************************** echo ***************************************************************
goto end goto end
rem rem
@ -89,16 +89,16 @@ of the code in Listing 3.3.
When the above command is executed on an original 4.77 MHz IBM PC, the When the above command is executed on an original 4.77 MHz IBM PC, the
time reported by the Zen timer is 3619 µs, or about 3.62 µs per load of time reported by the Zen timer is 3619 µs, or about 3.62 µs per load of
AL from memory. (While the exact number is 3.619 µs per load of AL, I’m AL from memory. (While the exact number is 3.619 µs per load of AL, I'm
going to round off that last digit from now on. No matter how many going to round off that last digit from now on. No matter how many
repetitions of a given instruction are timed, there’s just too much repetitions of a given instruction are timed, there's just too much
noise in the timing process—between dynamic RAM refresh, the prefetch noise in the timing process—between dynamic RAM refresh, the prefetch
queue, and the internal state of the processor at the start of queue, and the internal state of the processor at the start of
timing—for that last digit to have any significance.) Given the test timing—for that last digit to have any significance.) Given the test
PC’s 4.77 MHz clock, this works out to about 17 cycles per **MOV**, PC's 4.77 MHz clock, this works out to about 17 cycles per **MOV**,
which is actually a good bit longer than Intel’s specified 10-cycle which is actually a good bit longer than Intel's specified 10-cycle
execution time for this instruction. (See the MASM or TASM execution time for this instruction. (See the MASM or TASM
documentation, or Intel’s processor reference manuals, for official documentation, or Intel's processor reference manuals, for official
execution times.) Fear not, the Zen timer is right—**MOV AL,[MEMVAR]** execution times.) Fear not, the Zen timer is right—**MOV AL,[MEMVAR]**
really does take 17 cycles as used in Listing 3.3. Exactly why that is really does take 17 cycles as used in Listing 3.3. Exactly why that is
so is just what this book is all about. so is just what this book is all about.
@ -110,7 +110,7 @@ listing you wish to run into the file *filename* and enter the command:
pztime <filename> pztime <filename>
In fact, that’s exactly how I timed each of the listings in this book. In fact, that's exactly how I timed each of the listings in this book.
Code fragments you write yourself can be timed in just the same way. If Code fragments you write yourself can be timed in just the same way. If
you wish to time code directly in place in your programs, rather than in you wish to time code directly in place in your programs, rather than in
the test-bed program of Listing 3.2, simply insert calls to **ZTimerOn, the test-bed program of Listing 3.2, simply insert calls to **ZTimerOn,
@ -120,12 +120,12 @@ PZTIMER to your program.
### The Long-Period Zen Timer {#Heading12} ### The Long-Period Zen Timer {#Heading12}
With a few exceptions, the Zen timer presented above will serve us well With a few exceptions, the Zen timer presented above will serve us well
for the remainder of this book since we’ll be focusing on relatively for the remainder of this book since we'll be focusing on relatively
short code sequences that generally take much less than 54 ms to short code sequences that generally take much less than 54 ms to
execute. Occasionally, however, we will need to time longer intervals. execute. Occasionally, however, we will need to time longer intervals.
What’s more, it is very likely that you will want to time code sequences What's more, it is very likely that you will want to time code sequences
longer than 54 ms at some point in your programming career. Accordingly, longer than 54 ms at some point in your programming career. Accordingly,
I’ve also developed a Zen timer for periods longer than 54 ms. The I've also developed a Zen timer for periods longer than 54 ms. The
long-period Zen timer (so named by contrast with the precision Zen timer long-period Zen timer (so named by contrast with the precision Zen timer
just presented) shown in Listing 3.5 can measure periods up to one hour just presented) shown in Listing 3.5 can measure periods up to one hour
in length. in length.
@ -139,7 +139,7 @@ long periods. Practically speaking, however, there is no need for a
timer that can measure more than a few minutes, since the DOS time of timer that can measure more than a few minutes, since the DOS time of
day and date functions (or, indeed, the DATE and TIME commands in a day and date functions (or, indeed, the DATE and TIME commands in a
batch file) serve perfectly well for longer intervals. Since very long batch file) serve perfectly well for longer intervals. Since very long
timing intervals aren’t needed, the long-period Zen timer uses a timing intervals aren't needed, the long-period Zen timer uses a
simplified means of calculating elapsed time that is limited to simplified means of calculating elapsed time that is limited to
measuring intervals of an hour or less. If a period longer than an hour measuring intervals of an hour or less. If a period longer than an hour
is timed, the long-period Zen timer prints a message to the effect that is timed, the long-period Zen timer prints a message to the effect that
@ -150,7 +150,7 @@ of timing code that starts before midnight and ends after midnight; if
that eventuality occurs, the long-period Zen timer reports that it was that eventuality occurs, the long-period Zen timer reports that it was
unable to time the code because midnight was crossed. If this happens to unable to time the code because midnight was crossed. If this happens to
you, just time the code again, secure in the knowledge that at least you you, just time the code again, secure in the knowledge that at least you
won’t run into the problem again for 23-odd hours. won't run into the problem again for 23-odd hours.
You should not use the long-period Zen timer to time code that requires You should not use the long-period Zen timer to time code that requires
interrupts to be disabled for more than 54 ms at a stretch during the interrupts to be disabled for more than 54 ms at a stretch during the

View file

@ -11,7 +11,7 @@ that occur during the timing interval, most notably keyboard and mouse
interrupts, will increase the measured time. interrupts, will increase the measured time.
The long-period Zen timer has some of the same effects on the system The long-period Zen timer has some of the same effects on the system
time as does the precision Zen timer, so it’s a good idea to reboot the time as does the precision Zen timer, so it's a good idea to reboot the
system after a session with the long-period Zen timer. The long-period system after a session with the long-period Zen timer. The long-period
Zen timer does not, however, have the same potential for introducing Zen timer does not, however, have the same potential for introducing
major inaccuracy into the system clock time during a single timing run major inaccuracy into the system clock time during a single timing run
@ -20,7 +20,7 @@ to update normally.
#### Stopping the Clock {#Heading13} #### Stopping the Clock {#Heading13}
There’s a potential problem with the long-period Zen timer. The problem There's a potential problem with the long-period Zen timer. The problem
is this: In order to measure times longer than 54 ms, we must maintain is this: In order to measure times longer than 54 ms, we must maintain
not one but two timing components, the timer 0 count and the BIOS not one but two timing components, the timer 0 count and the BIOS
time-of-day count. The time-of-day count measures the passage of 54.9 ms time-of-day count. The time-of-day count measures the passage of 54.9 ms
@ -36,9 +36,9 @@ direction.)
The only way to avoid this problem is to stop timer 0, read both the The only way to avoid this problem is to stop timer 0, read both the
timer and time-of-day counts while the timer is stopped, and then timer and time-of-day counts while the timer is stopped, and then
restart the timer. Alas, the gate input to timer 0 isn’t restart the timer. Alas, the gate input to timer 0 isn't
program-controllable in the PC, so there’s no documented way to stop the program-controllable in the PC, so there's no documented way to stop the
timer. (The latched read feature we used in Listing 3.1 doesn’t stop the timer. (The latched read feature we used in Listing 3.1 doesn't stop the
timer; it latches a count, but the timer keeps running.) What should we timer; it latches a count, but the timer keeps running.) What should we
do? do?
@ -49,23 +49,23 @@ until the count is loaded. Surprisingly, the timer count remains
readable and correct while the timer is waiting for the initial load. readable and correct while the timer is waiting for the initial load.
In my experience, this approach works beautifully with fully In my experience, this approach works beautifully with fully
8253-compatible chips. However, there’s no guarantee that it will always 8253-compatible chips. However, there's no guarantee that it will always
work, since it programs the 8253 in an undocumented way. What’s more, work, since it programs the 8253 in an undocumented way. What's more,
IBM chose not to implement compatibility with this particular 8253 IBM chose not to implement compatibility with this particular 8253
feature in the custom chips used in PS/2 computers. On PS/2 computers, feature in the custom chips used in PS/2 computers. On PS/2 computers,
we have no choice but to latch the timer 0 count and then stop the BIOS we have no choice but to latch the timer 0 count and then stop the BIOS
count (by disabling interrupts) as quickly as possible. We’ll just have count (by disabling interrupts) as quickly as possible. We'll just have
to accept the fact that on PS/2 computers we may occasionally get a to accept the fact that on PS/2 computers we may occasionally get a
reading that’s off by 54 ms, and leave it at that. reading that's off by 54 ms, and leave it at that.
I’ve set up Listing 3.5 so that it can assemble to either use or not use I've set up Listing 3.5 so that it can assemble to either use or not use
the undocumented timer-stopping feature, as you please. The **PS2** the undocumented timer-stopping feature, as you please. The **PS2**
equate selects between the two modes of operation. If **PS2** is 1 (as equate selects between the two modes of operation. If **PS2** is 1 (as
it is in Listing 3.5), then the latch-and-read method is used; if it is in Listing 3.5), then the latch-and-read method is used; if
**PS2** is 0, then the undocumented timer-stop approach is used. The **PS2** is 0, then the undocumented timer-stop approach is used. The
latch-and-read method will work on all PC-compatible computers, but may latch-and-read method will work on all PC-compatible computers, but may
occasionally produce results that are incorrect by 54 ms. The timer-stop occasionally produce results that are incorrect by 54 ms. The timer-stop
approach avoids synchronization problems, but doesn’t work on all approach avoids synchronization problems, but doesn't work on all
computers. computers.
**LISTING 3.5 LZTIMER.ASM** **LISTING 3.5 LZTIMER.ASM**
@ -103,13 +103,13 @@ computers.
; more than adequate. ; more than adequate.
; ;
; Note: The PS/2 version is assembled by setting the symbol PS2 to 1. ; Note: The PS/2 version is assembled by setting the symbol PS2 to 1.
; PS2 must be set to 1 on PS/2 computers because the PS/2’s ; PS2 must be set to 1 on PS/2 computers because the PS/2's
; timers are not compatible with an undocumented timer-stopping ; timers are not compatible with an undocumented timer-stopping
; feature of the 8253; the alternative timing approach that ; feature of the 8253; the alternative timing approach that
; must be used on PS/2 computers leaves a short window ; must be used on PS/2 computers leaves a short window
; during which the timer 0 count and the BIOS timer count may ; during which the timer 0 count and the BIOS timer count may
; not be synchronized. You should also set the PS2 symbol to ; not be synchronized. You should also set the PS2 symbol to
; 1 if you’re getting erratic or obviously incorrect results. ; 1 if you're getting erratic or obviously incorrect results.
; ;
; Note: When PS2 is 0, the code relies on an undocumented 8253 ; Note: When PS2 is 0, the code relies on an undocumented 8253
; feature to get more reliable readings. It is possible that ; feature to get more reliable readings. It is possible that
@ -140,7 +140,7 @@ computers.
; ;
; Note: These routines can introduce inaccuracies of up to a few ; Note: These routines can introduce inaccuracies of up to a few
; tenths of a second into the system clock count for each ; tenths of a second into the system clock count for each
; code section timed. Consequently, it’s a good idea to ; code section timed. Consequently, it's a good idea to
; reboot at the conclusion of timing sessions. (The ; reboot at the conclusion of timing sessions. (The
; battery-backed clock, if any, is not affected by the Zen ; battery-backed clock, if any, is not affected by the Zen
; timer.) ; timer.)
@ -148,7 +148,7 @@ computers.
; All registers and all flags are preserved by all routines. ; All registers and all flags are preserved by all routines.
; ;
Code segment word public ‘CODE’ Code segment word public ‘CODE'
assume cs: Code, ds:nothing assume cs: Code, ds:nothing
public ZTimerOn, ZTimerOff, ZTimerReport public ZTimerOn, ZTimerOff, ZTimerReport
@ -157,7 +157,7 @@ computers.
; system; when PS2 is 0, the readings are more reliable if the ; system; when PS2 is 0, the readings are more reliable if the
; computer supports the undocumented timer-stopping feature, ; computer supports the undocumented timer-stopping feature,
; but may be badly off if that feature is not supported. In ; but may be badly off if that feature is not supported. In
; fact, timer-stopping may interfere with your computer’s ; fact, timer-stopping may interfere with your computer's
; overall operation by putting the 8253 into an undefined or ; overall operation by putting the 8253 into an undefined or
; incorrect state. Use with caution!!! ; incorrect state. Use with caution!!!
; ;
@ -231,10 +231,10 @@ computers.
OutputStr labelbyte OutputStr labelbyte
db 0dh, 0ah, ‘Timed count: ‘ db 0dh, 0ah, ‘Timed count: ‘
TimedCountStr db10 dup (?) TimedCountStr db10 dup (?)
db’ microseconds’, 0dh, 0ah db' microseconds', 0dh, 0ah
db ‘$’ db ‘$'
; ;
; Temporary storage for timed count as it’s divided down by powers ; Temporary storage for timed count as it's divided down by powers
; of ten when converting from doubleword binary to ASCII. ; of ten when converting from doubleword binary to ASCII.
; ;
CurrentCountLow dw ? CurrentCountLow dw ?
@ -262,25 +262,25 @@ computers.
; ;
TurnOverStrlabelbyte TurnOverStrlabelbyte
db 0dh, 0ah db 0dh, 0ah
db ‘****************************************************’ db ‘****************************************************'
db 0dh, 0ah db 0dh, 0ah
db’* Either midnight passed or an hour or more passed *’ db'* Either midnight passed or an hour or more passed *'
db 0dh, 0ah db 0dh, 0ah
db’* while timing was in progress. If the former was *’ db'* while timing was in progress. If the former was *'
db 0dh, 0ah db 0dh, 0ah
db’* the case, please rerun the test; if the latter *’ db'* the case, please rerun the test; if the latter *'
db 0dh, 0ah db 0dh, 0ah
db’* was the case, the test code takes too long to *’ db'* was the case, the test code takes too long to *'
db 0dh, 0ah db 0dh, 0ah
db’* run to be timed by the long-period Zen timer. *’ db'* run to be timed by the long-period Zen timer. *'
db 0dh, 0ah db 0dh, 0ah
db ‘* Suggestions: use the DOS TIME command, the DOS *’ db ‘* Suggestions: use the DOS TIME command, the DOS *'
db 0dh, 0ah db 0dh, 0ah
db ‘* time function, or a watch. *’ db ‘* time function, or a watch. *'
db 0dh, 0ah db 0dh, 0ah
db ‘****************************************************’ db ‘****************************************************'
db 0dh, 0ah db 0dh, 0ah
db’$’ db'$'
;******************************************************************** ;********************************************************************
;* Routine called to start timing. * ;* Routine called to start timing. *
@ -302,7 +302,7 @@ computers.
mov al,00110100b ;mode 2 mov al,00110100b ;mode 2
out MODE_8253,al out MODE_8253,al
; ;
; Set the timer count to 0, so we know we won’t get another ; Set the timer count to 0, so we know we won't get another
; timer interrupt right away. ; timer interrupt right away.
; Note: this introduces an inaccuracy of up to 54 ms in the system ; Note: this introduces an inaccuracy of up to 54 ms in the system
; clock count each time it is executed. ; clock count each time it is executed.
@ -329,7 +329,7 @@ computers.
; ;
; Store the timing start BIOS count. ; Store the timing start BIOS count.
; (Since the timer count was just set to 0, the BIOS count will ; (Since the timer count was just set to 0, the BIOS count will
; stay the same for the next 54 ms, so we don’t need to disable ; stay the same for the next 54 ms, so we don't need to disable
; interrupts in order to avoid getting a half-changed count.) ; interrupts in order to avoid getting a half-changed count.)
; ;
push ds push ds
@ -394,12 +394,12 @@ computers.
; ;
; This is where a one-instruction-long window exists on the PS/2. ; This is where a one-instruction-long window exists on the PS/2.
; The timer count and the BIOS count can lose synchronization; ; The timer count and the BIOS count can lose synchronization;
; since the timer keeps counting after it’s latched, it can turn ; since the timer keeps counting after it's latched, it can turn
; over right after it’s latched and cause the BIOS count to turn ; over right after it's latched and cause the BIOS count to turn
; over before interrupts are disabled, leaving us with the timer ; over before interrupts are disabled, leaving us with the timer
; count from before the timer turned over coupled with the BIOS ; count from before the timer turned over coupled with the BIOS
; count from after the timer turned over. The result is a count ; count from after the timer turned over. The result is a count
; that’s 54 ms too long. ; that's 54 ms too long.
; ;
else else
@ -420,7 +420,7 @@ computers.
cli ;stop the BIOS count cli ;stop the BIOS count
; ;
; Read the BIOS count. (Since interrupts are disabled, the BIOS ; Read the BIOS count. (Since interrupts are disabled, the BIOS
; count won’t change.) ; count won't change.)
; ;
push ds push ds
sub ax,ax sub ax,ax
@ -526,9 +526,9 @@ computers.
; ;
; Called by ZTimerOff to stop the timer and add the result to ; Called by ZTimerOff to stop the timer and add the result to
; ReferenceCount for overhead measurements. Doesn’t need to look ; ReferenceCount for overhead measurements. Doesn't need to look
; at the BIOS count because timing a zero-length code fragment ; at the BIOS count because timing a zero-length code fragment
; isn’t going to take anywhere near 54 ms. ; isn't going to take anywhere near 54 ms.
; ;
ReferenceZTimerOff procnear ReferenceZTimerOff procnear
@ -596,8 +596,8 @@ computers.
; ;
mov ax,[StartBIOSCountHigh] mov ax,[StartBIOSCountHigh]
cmp ax,[EndBIOSCountHigh] cmp ax,[EndBIOSCountHigh]
jz CalcBIOSTime ;hour count didn’t change, jz CalcBIOSTime ;hour count didn't change,
; so everything’s fine ; so everything's fine
inc ax inc ax
cmp ax,[EndBIOSCountHigh] cmp ax,[EndBIOSCountHigh]
jnz TestTooLong ;midnight or two hour jnz TestTooLong ;midnight or two hour
@ -606,14 +606,14 @@ computers.
mov ax,[EndBIOSCountLow] mov ax,[EndBIOSCountLow]
cmp ax,[StartBIOSCountLow] cmp ax,[StartBIOSCountLow]
jb CalcBIOSTime ;a single hour boundary jb CalcBIOSTime ;a single hour boundary
; passed--that’s OK, so long as ; passed--that's OK, so long as
; the total time wasn’t more ; the total time wasn't more
; than an hour ; than an hour
; ;
; Over an hour elapsed or midnight passed during timing, which ; Over an hour elapsed or midnight passed during timing, which
; renders the results invalid. Notify the user. This misses the ; renders the results invalid. Notify the user. This misses the
; case where a multiple of 24 hours has passed, but we’ll rely ; case where a multiple of 24 hours has passed, but we'll rely
; on the perspicacity of the user to detect that case. ; on the perspicacity of the user to detect that case.
; ;
TestTooLong: TestTooLong:
@ -665,7 +665,7 @@ computers.
mov di,offset PowersOfTenEnd - offset PowersOfTen - 4 mov di,offset PowersOfTenEnd - offset PowersOfTen - 4
mov si,offset TimedCountStr mov si,offset TimedCountStr
CTSNextDigit: CTSNextDigit:
mov bl,’0’ mov bl,'0'
CTSLoop: CTSLoop:
mov ax,[CurrentCountLow] mov ax,[CurrentCountLow]
mov dx,[CurrentCountHigh] mov dx,[CurrentCountHigh]

View file

@ -4,8 +4,8 @@
Moreover, because it uses an undocumented feature, the timer-stop Moreover, because it uses an undocumented feature, the timer-stop
approach could conceivably cause erratic 8253 operation, which could in approach could conceivably cause erratic 8253 operation, which could in
turn seriously affect your computer’s operation until the next reboot. turn seriously affect your computer's operation until the next reboot.
In non-8253-compatible systems, I’ve observed not only wildly incorrect In non-8253-compatible systems, I've observed not only wildly incorrect
timing results, but also failure of a diskette drive to operate properly timing results, but also failure of a diskette drive to operate properly
after the long-period Zen timer with **PS2** set to 0 has run, so be after the long-period Zen timer with **PS2** set to 0 has run, so be
alert for signs of trouble if you do set **PS2** to 0. alert for signs of trouble if you do set **PS2** to 0.
@ -17,7 +17,7 @@ each code-timing session.) You should *immediately* reboot and set the
with the long-period Zen timer when **PS2** is set to 0. If you want to with the long-period Zen timer when **PS2** is set to 0. If you want to
set **PS2** to 0, it would be a good idea to time a few of the listings set **PS2** to 0, it would be a good idea to time a few of the listings
in this book with **PS2** set first to 1 and then to 0, to make sure in this book with **PS2** set first to 1 and then to 0, to make sure
that the results match. If they’re consistently different, you should that the results match. If they're consistently different, you should
set **PS2** to 1. set **PS2** to 1.
While the the non-PS/2 version is more dangerous than the PS/2 version, While the the non-PS/2 version is more dangerous than the PS/2 version,
@ -29,15 +29,15 @@ If you do leave the **PS2** equate at 1 in Listing 3.5, you should
repeat each code-timing run several times before relying on the results repeat each code-timing run several times before relying on the results
to be accurate to more than 54 ms, since variations may result from the to be accurate to more than 54 ms, since variations may result from the
possible lack of synchronization between the timer 0 count and the BIOS possible lack of synchronization between the timer 0 count and the BIOS
time-of-day count. In fact, it’s a good idea to time code more than once time-of-day count. In fact, it's a good idea to time code more than once
no matter which version of the long-period Zen timer you’re using, since no matter which version of the long-period Zen timer you're using, since
interrupts, which must be enabled in order for the long-period timer to interrupts, which must be enabled in order for the long-period timer to
work properly, may occur at any time and can alter execution time work properly, may occur at any time and can alter execution time
substantially. substantially.
Finally, please note that the *precision* Zen timer works perfectly well Finally, please note that the *precision* Zen timer works perfectly well
on both PS/2 and non-PS/2 computers. The PS/2 and 8253 considerations on both PS/2 and non-PS/2 computers. The PS/2 and 8253 considerations
we’ve just discussed apply *only* to the longZen timer. we've just discussed apply *only* to the longZen timer.
### Example Use of the Long-Period Zen Timer {#Heading14} ### Example Use of the Long-Period Zen Timer {#Heading14}
@ -49,7 +49,7 @@ the code being timed takes too long for the precision timer to handle,
all you have to do is link in the long-period timer instead. all you have to do is link in the long-period timer instead.
Listing 3.6 shows a test-bed program for the long-period Zen timer. Listing 3.6 shows a test-bed program for the long-period Zen timer.
While this program is similar to Listing 3.2, it’s worth noting that While this program is similar to Listing 3.2, it's worth noting that
Listing 3.6 waits for a few seconds before calling **ZTimerOn**, thereby Listing 3.6 waits for a few seconds before calling **ZTimerOn**, thereby
allowing any pending keyboard interrupts to be processed. Since allowing any pending keyboard interrupts to be processed. Since
interrupts must be left on in order to time periods longer than 54 ms, interrupts must be left on in order to time periods longer than 54 ms,
@ -72,11 +72,11 @@ timing.
; ;
; By Michael Abrash ; By Michael Abrash
; ;
mystack segment para stack ‘STACK’ mystack segment para stack ‘STACK'
db 512 dup(?) db 512 dup(?)
mystack ends mystack ends
; ;
Code segment para public ‘CODE’ Code segment para public ‘CODE'
assume cs:Code, ds:Code assume cs:Code, ds:Code
extrn ZTimerOn:near, ZTimerOff:near, ZTimerReport:near extrn ZTimerOn:near, ZTimerOff:near, ZTimerReport:near
Startproc near Startproc near
@ -102,7 +102,7 @@ timing.
add dh,60 ;yes, a minute must have turned over, add dh,60 ;yes, a minute must have turned over,
; so add one minute ; so add one minute
CheckDelayTime: CheckDelayTime:
sub dh,bh ;get time that’s passed sub dh,bh ;get time that's passed
cmp dh,7 ;has it been more than 6 seconds yet? cmp dh,7 ;has it been more than 6 seconds yet?
jb DelayLoop ;not yet jb DelayLoop ;not yet
; ;

View file

@ -13,7 +13,7 @@
rem * long-period Zen timer program LZTEST.EXE to time the code * rem * long-period Zen timer program LZTEST.EXE to time the code *
rem * named as the command-line parameter. Listing 3.5 must be * rem * named as the command-line parameter. Listing 3.5 must be *
rem * named LZTIMER.ASM, and Listing 3.6 must be named * rem * named LZTIMER.ASM, and Listing 3.6 must be named *
rem * LZTEST.ASM. To time the code in LST3-8, you’d type the * rem * LZTEST.ASM. To time the code in LST3-8, you'd type the *
rem * DOS command: * rem * DOS command: *
rem * * rem * *
rem * lztime lst3-8 * rem * lztime lst3-8 *
@ -45,7 +45,7 @@
:ckexist :ckexist
if exist %1 goto docopy if exist %1 goto docopy
echo *************************************************************** echo ***************************************************************
echo * The specified file, “%1,” doesn’t exist. * echo * The specified file, "%1," doesn't exist. *
echo *************************************************************** echo ***************************************************************
goto end goto end
rem rem
@ -127,7 +127,7 @@ designed to be called from assembly language; some relatively minor
modifications are required before the **ZTimerOn** (start timer), modifications are required before the **ZTimerOn** (start timer),
**ZTimerOff** (stop timer), and **ZTimerReport** (display timing **ZTimerOff** (stop timer), and **ZTimerReport** (display timing
results) routines can be called from C. There are two separate cases to results) routines can be called from C. There are two separate cases to
be dealt with here: small code model and large; I’ll tackle the simpler be dealt with here: small code model and large; I'll tackle the simpler
one, the small code model, first. one, the small code model, first.
Altering the Zen timer for linking to a small code model C program Altering the Zen timer for linking to a small code model C program
@ -137,9 +137,9 @@ involves the following steps: **C** hange **ZTimerOn** to
**\_TEXT** . Figure 3.2 shows the line numbers and new states of all **\_TEXT** . Figure 3.2 shows the line numbers and new states of all
lines from Listing 3.1 that must be changed. These changes convert the lines from Listing 3.1 that must be changed. These changes convert the
code to use C-style external label names and the small model C code code to use C-style external label names and the small model C code
segment. (In C++, use the “C” specifier, as in segment. (In C++, use the "C" specifier, as in
extern “C” ZTimerOn(void); extern "C" ZTimerOn(void);
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](03-08.html) [Table of Contents](index.html) [Next](03-10.html) [Previous](03-08.html) [Table of Contents](index.html) [Next](03-10.html)

View file

@ -3,9 +3,9 @@
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
when declaring the timer routines **extern**, so that name-mangling when declaring the timer routines **extern**, so that name-mangling
doesn’t occur, and the linker can find the routines’ C-style names.) doesn't occur, and the linker can find the routines' C-style names.)
That’s all it takes; after doing this, you’ll be able to use the Zen That's all it takes; after doing this, you'll be able to use the Zen
timer from C, as, for example, in: timer from C, as, for example, in:
ZTimerOn(): ZTimerOn():
@ -14,14 +14,14 @@ timer from C, as, for example, in:
ZTimerOff(); ZTimerOff();
ZTimerReport(); ZTimerReport();
(I’m talking about the precision timer here. The long-period (I'm talking about the precision timer here. The long-period
timer—Listing 3.5—requires the same modifications, but to different timer—Listing 3.5—requires the same modifications, but to different
lines.) lines.)
![](images/03-02.jpg)\ ![](images/03-02.jpg)\
**Figure 3.2**  *Changes for use with small code model C.* **Figure 3.2**  *Changes for use with small code model C.*
Altering the Zen timer for use in C’s large code model is a tad more Altering the Zen timer for use in C's large code model is a tad more
complex, because in addition to the above changes, all functions, complex, because in addition to the above changes, all functions,
including the internal reference timing routines that are used to including the internal reference timing routines that are used to
calculate overhead so it can be subtracted out, must be converted to calculate overhead so it can be subtracted out, must be converted to
@ -49,7 +49,7 @@ with
(and likewise for **ReferenceZTimerOff** ), which works because (and likewise for **ReferenceZTimerOff** ), which works because
**ReferenceZTimerOn** is in the same segment as the calling code. This **ReferenceZTimerOn** is in the same segment as the calling code. This
is normally a great optimization, being both smaller and faster than a is normally a great optimization, being both smaller and faster than a
far call. However, it’s not so great for the Zen far call. However, it's not so great for the Zen
![](images/03-03.jpg)\ ![](images/03-03.jpg)\
**Figure 3.3**  *Changes for use with large code model C.* **Figure 3.3**  *Changes for use with large code model C.*
@ -61,24 +61,24 @@ to push/near call pairs within the Zen timer module, TASM makes it
impossible to emulate exactly the overhead of the Zen timer, and makes impossible to emulate exactly the overhead of the Zen timer, and makes
timings slightly (about 16 cycles on a 386) less accurate. timings slightly (about 16 cycles on a 386) less accurate.
What’s the solution? Put the **NOSMART** directive at the start of the What's the solution? Put the **NOSMART** directive at the start of the
Zen timer code. This directive instructs TASM to turn off all Zen timer code. This directive instructs TASM to turn off all
optimizations, including converting far calls to push/near call pairs. optimizations, including converting far calls to push/near call pairs.
By the way, there is, to the best of my knowledge, no such problem with By the way, there is, to the best of my knowledge, no such problem with
MASM up through version 5.10A. MASM up through version 5.10A.
In my mind, the whole business of optimizing assemblers is a mixed In my mind, the whole business of optimizing assemblers is a mixed
blessing. In general, it’s nice to have the assembler shortening jumps blessing. In general, it's nice to have the assembler shortening jumps
and selecting sign-extended forms of instructions for you. On the other and selecting sign-extended forms of instructions for you. On the other
hand, the benefits of tricks like substituting push/near call pairs for hand, the benefits of tricks like substituting push/near call pairs for
far calls are relatively small, and those tricks can get in the way when far calls are relatively small, and those tricks can get in the way when
complete control is needed. Sure, complete control is needed very complete control is needed. Sure, complete control is needed very
rarely, but when it is, optimizing assemblers can cause subtle problems; rarely, but when it is, optimizing assemblers can cause subtle problems;
I discovered TASM’s alteration of far calls only because I happened to I discovered TASM's alteration of far calls only because I happened to
view the code in the debugger, and you might want to do the same if view the code in the debugger, and you might want to do the same if
you’re using a recent version of MASM. you're using a recent version of MASM.
I’ve tested the changes shown in Figures 3.2 and 3.3 with TASM and I've tested the changes shown in Figures 3.2 and 3.3 with TASM and
Borland C++ 4.0, and also with the latest MASM and Microsoft C/C++ Borland C++ 4.0, and also with the latest MASM and Microsoft C/C++
compiler. compiler.
@ -86,21 +86,21 @@ compiler.
For those of you who wish to pursue the mechanics of code measurement For those of you who wish to pursue the mechanics of code measurement
further, one good article about measuring code performance with the 8253 further, one good article about measuring code performance with the 8253
timer is “Programming Insight: High-Performance Software Analysis on the timer is "Programming Insight: High-Performance Software Analysis on the
IBM PC,” by Byron Sheppard, which appeared in the January, 1987 issue of IBM PC," by Byron Sheppard, which appeared in the January, 1987 issue of
*Byte*. For complete if somewhat cryptic information on the 8253 timer *Byte*. For complete if somewhat cryptic information on the 8253 timer
itself, I refer you to Intel’s *Microsystem Components Handbook*, which itself, I refer you to Intel's *Microsystem Components Handbook*, which
is also a useful reference for a number of other PC components, is also a useful reference for a number of other PC components,
including the 8259 Programmable Interrupt Controller and the 8237 DMA including the 8259 Programmable Interrupt Controller and the 8237 DMA
Controller. For details about the way the 8253 is used in the PC, as Controller. For details about the way the 8253 is used in the PC, as
well as a great deal of additional information about the PC’s hardware well as a great deal of additional information about the PC's hardware
and BIOS resources, I suggest you consult IBM’s series of technical and BIOS resources, I suggest you consult IBM's series of technical
reference manuals for the PC, XT, AT, Model 30, and microchannel reference manuals for the PC, XT, AT, Model 30, and microchannel
computers, such as the Models 50, 60, and 80. computers, such as the Models 50, 60, and 80.
For our purposes, however, it’s not critical that you understand exactly For our purposes, however, it's not critical that you understand exactly
how the Zen timer works. All you really need to know is what the Zen how the Zen timer works. All you really need to know is what the Zen
timer can do and how to use it, and we’ve accomplished that in this timer can do and how to use it, and we've accomplished that in this
chapter. chapter.
#### Armed with the Zen Timer, Onward and Upward {#Heading18} #### Armed with the Zen Timer, Onward and Upward {#Heading18}
@ -123,7 +123,7 @@ of the inaccuracy introduced by the Zen timer becomes less over longer
periods. periods.
Imperfections notwithstanding, the Zen timer is a good tool for Imperfections notwithstanding, the Zen timer is a good tool for
exploring C code and x86 family assembly language, and it’s a tool we’ll exploring C code and x86 family assembly language, and it's a tool we'll
use frequently for the remainder of this book. use frequently for the remainder of this book.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -21,19 +21,19 @@ of PC programmers to pedal-to-the-metal optimization.
This chapter focuses almost entirely on the first popular x86-family This chapter focuses almost entirely on the first popular x86-family
processor, the 8088. Some of the specific features and results that I processor, the 8088. Some of the specific features and results that I
cite in this chapter are no longer applicable to modern x86-family cite in this chapter are no longer applicable to modern x86-family
processors such as the 486 and Pentium, as I’ll point out later on when processors such as the 486 and Pentium, as I'll point out later on when
we discuss those processors. Nonetheless, the overall theme of this we discuss those processors. Nonetheless, the overall theme of this
chapter—that understanding dimly-seen and poorly-documented code chapter—that understanding dimly-seen and poorly-documented code
gremlins called cycle-eaters that lurk in your system is essential to gremlins called cycle-eaters that lurk in your system is essential to
performance programming—is every bit as valid today. Also, later performance programming—is every bit as valid today. Also, later
chapters often refer back to the basic cycle-eaters described in this chapters often refer back to the basic cycle-eaters described in this
chapter, so this chapter is the foundation for the discussions of chapter, so this chapter is the foundation for the discussions of
x86-family optimization to come. What’s more, the Zen timer remains an x86-family optimization to come. What's more, the Zen timer remains an
excellent tool with which to flush out and examine cycle-eaters, as excellent tool with which to flush out and examine cycle-eaters, as
we’ll see in later chapters, and this chapter is as good an illustration we'll see in later chapters, and this chapter is as good an illustration
of how to use the Zen timer as you’re likely to find. of how to use the Zen timer as you're likely to find.
So, don’t take either the absolute or the relative execution times So, don't take either the absolute or the relative execution times
presented in this chapter as gospel for newer processors, and read on to presented in this chapter as gospel for newer processors, and read on to
later chapters to see how the cycle-eaters and optimization rules have later chapters to see how the cycle-eaters and optimization rules have
changed over time, but do take the time to at least skim through this changed over time, but do take the time to at least skim through this
@ -46,8 +46,8 @@ Programming has many levels, ranging from the familiar (high-level
languages, DOS calls, and the like) down to the esoteric things that lie languages, DOS calls, and the like) down to the esoteric things that lie
on the shadowy edge of hardware-land. I call these *cycle-eaters* on the shadowy edge of hardware-land. I call these *cycle-eaters*
because, like the monsters in a bad 50s horror movie, they lurk in those because, like the monsters in a bad 50s horror movie, they lurk in those
shadows, taking their share of your program’s performance without regard shadows, taking their share of your program's performance without regard
to the forces of goodness or the U.S. Army. In this chapter, we’re going to the forces of goodness or the U.S. Army. In this chapter, we're going
to jump right in at the lowest level by examining the cycle-eaters that to jump right in at the lowest level by examining the cycle-eaters that
live beneath the programming interface; that is, beneath your live beneath the programming interface; that is, beneath your
application, DOS, and BIOS—in fact, beneath the instruction set itself. application, DOS, and BIOS—in fact, beneath the instruction set itself.
@ -55,15 +55,15 @@ application, DOS, and BIOS—in fact, beneath the instruction set itself.
Why start at the lowest level? Simply because cycle-eaters affect the Why start at the lowest level? Simply because cycle-eaters affect the
performance of all assembler code, and yet are almost unknown to most performance of all assembler code, and yet are almost unknown to most
programmers. A full understanding of code optimization requires an programmers. A full understanding of code optimization requires an
understanding of cycle-eaters and their implications. That’s no simple understanding of cycle-eaters and their implications. That's no simple
task, and in fact it is in precisely that area that most books and task, and in fact it is in precisely that area that most books and
articles about assembly programming fall short. articles about assembly programming fall short.
Nearly all literature on assembly programming discusses only the Nearly all literature on assembly programming discusses only the
programming interface: the instruction set, the registers, the flags, programming interface: the instruction set, the registers, the flags,
and the BIOS and DOS calls. Those topics cover the functionality of and the BIOS and DOS calls. Those topics cover the functionality of
assembly programs most thoroughly—but it’s performance above all else assembly programs most thoroughly—but it's performance above all else
that we’re after. No one ever tells you about the raw stuff of that we're after. No one ever tells you about the raw stuff of
performance, which lies *beneath* the programming interface, in the performance, which lies *beneath* the programming interface, in the
dimly-seen realm—populated by instruction prefetching, dynamic RAM dimly-seen realm—populated by instruction prefetching, dynamic RAM
refresh, and wait states—where software meets hardware. This area is the refresh, and wait states—where software meets hardware. This area is the
@ -78,56 +78,56 @@ Which brings us to cycle-eaters.
Cycle-eaters are gremlins that live on the bus or in peripherals (and Cycle-eaters are gremlins that live on the bus or in peripherals (and
sometimes within the CPU itself), slowing the performance of PC code so sometimes within the CPU itself), slowing the performance of PC code so
that it doesn’t execute at full speed. Most cycle-eaters (and all of that it doesn't execute at full speed. Most cycle-eaters (and all of
those haunting the older Intel processors) live outside the CPU’s those haunting the older Intel processors) live outside the CPU's
Execution Unit, where they can *only* affect the CPU when the CPU Execution Unit, where they can *only* affect the CPU when the CPU
performs a bus access (a memory or I/O read or write). Once your code performs a bus access (a memory or I/O read or write). Once your code
and data are already inside the CPU, those cycle-eaters can no longer be and data are already inside the CPU, those cycle-eaters can no longer be
a problem. Only on the 486 and Pentium CPUs will you find cycle-eaters a problem. Only on the 486 and Pentium CPUs will you find cycle-eaters
inside the chip, as we’ll see in later chapters. inside the chip, as we'll see in later chapters.
The nature and severity of the cycle-eaters vary enormously from The nature and severity of the cycle-eaters vary enormously from
processor to processor, and (especially) from memory architecture to processor to processor, and (especially) from memory architecture to
memory architecture. In order to understand them all, we need first to memory architecture. In order to understand them all, we need first to
understand the simplest among them, those that haunted the original understand the simplest among them, those that haunted the original
8088-based IBM PC. Later on in this book, I’ll be better able to explain 8088-based IBM PC. Later on in this book, I'll be better able to explain
the newer generation of cycle-eaters in terms of those ancestral the newer generation of cycle-eaters in terms of those ancestral
cycle-eaters—but we have to get the groundwork down first. cycle-eaters—but we have to get the groundwork down first.
#### The 8088’s Ancestral Cycle-Eaters {#Heading5} #### The 8088's Ancestral Cycle-Eaters {#Heading5}
Internally, the 8088 is a 16-bit processor, capable of running at full Internally, the 8088 is a 16-bit processor, capable of running at full
speed at all times—unless external data is required. External data must speed at all times—unless external data is required. External data must
traverse the 8088’s external data bus and the PC’s data bus one byte at traverse the 8088's external data bus and the PC's data bus one byte at
a time to and from peripherals, with cycle-eaters lurking along every a time to and from peripherals, with cycle-eaters lurking along every
step of the way. What’s more, external data includes not only memory step of the way. What's more, external data includes not only memory
operands *but also instruction bytes,* so even instructions with no operands *but also instruction bytes,* so even instructions with no
memory operands can suffer from cycle-eaters. Since some of the 8088’s memory operands can suffer from cycle-eaters. Since some of the 8088's
fastest instructions are register-only instructions, that’s important fastest instructions are register-only instructions, that's important
indeed. indeed.
The major cycle-eaters are: The major cycle-eaters are:
- The 8088’s 8-bit external data bus. - The 8088's 8-bit external data bus.
- The prefetch queue. - The prefetch queue.
- Dynamic RAM refresh. - Dynamic RAM refresh.
- Wait states, notably display memory wait states and, in the AT and - Wait states, notably display memory wait states and, in the AT and
80386 computers, system memory wait states. 80386 computers, system memory wait states.
The locations of these cycle-eaters in the primordial 8088-based PC are The locations of these cycle-eaters in the primordial 8088-based PC are
shown in Figure 4.1. We’ll cover each of the cycle-eaters in turn in shown in Figure 4.1. We'll cover each of the cycle-eaters in turn in
this chapter. The material won’t be easy since cycle-eaters are among this chapter. The material won't be easy since cycle-eaters are among
the most subtle aspects of assembly programming. By the same token, the most subtle aspects of assembly programming. By the same token,
however, this will be one of the most important and rewarding chapters however, this will be one of the most important and rewarding chapters
in this book. Don’t worry if you don’t catch everything in this chapter, in this book. Don't worry if you don't catch everything in this chapter,
but do read it all even if the going gets a bit tough. Cycle-eaters play but do read it all even if the going gets a bit tough. Cycle-eaters play
a key role in later chapters, so some familiarity with them is highly a key role in later chapters, so some familiarity with them is highly
desirable. desirable.
### The 8-Bit Bus Cycle-Eater {#Heading6} ### The 8-Bit Bus Cycle-Eater {#Heading6}
*Look! Down on the motherboard! It’s a 16-bit processor! It’s an 8-bit *Look! Down on the motherboard! It's a 16-bit processor! It's an 8-bit
processor! It’s...* processor! It's...*
...an 8088! ...an 8088!
@ -138,7 +138,7 @@ processor.
The 8088 is internally a full 16-bit processor, equivalent to an 8086. The 8088 is internally a full 16-bit processor, equivalent to an 8086.
(In fact, the 8086 is identical to the 8088, except that it has a full (In fact, the 8086 is identical to the 8088, except that it has a full
16-bit bus. The 8088 is basically the poor man’s 8086, because it allows 16-bit bus. The 8088 is basically the poor man's 8086, because it allows
a cheaper—albeit slower—system to be built, thanks to the half-sized a cheaper—albeit slower—system to be built, thanks to the half-sized
bus.) In terms of the instruction set, the 8088 is clearly a 16-bit bus.) In terms of the instruction set, the 8088 is clearly a 16-bit
processor, capable of performing any given 16-bit operation—addition, processor, capable of performing any given 16-bit operation—addition,

View file

@ -9,12 +9,12 @@
**Figure 4.2**  *Internal data bus widths of the 8088.* **Figure 4.2**  *Internal data bus widths of the 8088.*
As shown in Figure 4.1, the 8-bit bus cycle-eater lies squarely on the As shown in Figure 4.1, the 8-bit bus cycle-eater lies squarely on the
8088’s external data bus. Technically, it might be more accurate to 8088's external data bus. Technically, it might be more accurate to
place this cycle-eater in the Bus Interface Unit, which breaks 16-bit place this cycle-eater in the Bus Interface Unit, which breaks 16-bit
memory accesses into paired 8-bit accesses, but it is really the limited memory accesses into paired 8-bit accesses, but it is really the limited
width of the external data bus that constricts data flow into and out of width of the external data bus that constricts data flow into and out of
the 8088. True, the original PC’s bus is also only 8 bits wide, but the 8088. True, the original PC's bus is also only 8 bits wide, but
that’s just to match the 8088’s 8-bit bus; even if the PC’s bus were 16 that's just to match the 8088's 8-bit bus; even if the PC's bus were 16
bits wide, data could still pass into and out of the 8088 chip itself bits wide, data could still pass into and out of the 8088 chip itself
only 1 byte at a time. only 1 byte at a time.
@ -43,7 +43,7 @@ doubleword from memory in two halves.
One obvious effect of the 8-bit bus cycle-eater is that word-sized One obvious effect of the 8-bit bus cycle-eater is that word-sized
accesses to memory operands on the 8088 take 4 cycles longer than accesses to memory operands on the 8088 take 4 cycles longer than
byte-sized accesses. That’s why the official instruction timings byte-sized accesses. That's why the official instruction timings
indicate that for code running on an 8088 an additional 4 cycles are indicate that for code running on an 8088 an additional 4 cycles are
required for every word-sized access to a memory operand. For instance, required for every word-sized access to a memory operand. For instance,
@ -54,11 +54,11 @@ takes 4 cycles longer to read the word at address **MemVar** than
mov al,byte ptr [MemVar] mov al,byte ptr [MemVar]
takes to read the byte at address **MemVar.** (Actually, the difference takes to read the byte at address **MemVar.** (Actually, the difference
between the two isn’t very likely to be exactly 4 cycles, for reasons between the two isn't very likely to be exactly 4 cycles, for reasons
that will become clear once we discuss the prefetch queue and dynamic that will become clear once we discuss the prefetch queue and dynamic
RAM refresh cycle-eaters later in this chapter.) RAM refresh cycle-eaters later in this chapter.)
What’s more, in some cases one instruction can perform multiple What's more, in some cases one instruction can perform multiple
word-sized accesses, incurring that 4-cycle penalty on each access. For word-sized accesses, incurring that 4-cycle penalty on each access. For
example, adding a value to a word-sized memory variable requires two example, adding a value to a word-sized memory variable requires two
word-sized accesses—one to read the destination operand from memory word-sized accesses—one to read the destination operand from memory
@ -79,7 +79,7 @@ accesses x 4 cycles, or *524,280 cycles* to the 8-bit bus cycle-eater!
In other words, one 8088 instruction (admittedly, an instruction that In other words, one 8088 instruction (admittedly, an instruction that
does a great deal) can take over one-tenth of a second longer on an 8088 does a great deal) can take over one-tenth of a second longer on an 8088
than on an 8086, simply because of the 8-bit bus. *One-tenth of a than on an 8086, simply because of the 8-bit bus. *One-tenth of a
second!* That’s a phenomenally long time in computer terms; in one-tenth second!* That's a phenomenally long time in computer terms; in one-tenth
of a second, the 8088 can perform more than 50,000 additions and of a second, the 8088 can perform more than 50,000 additions and
subtractions. subtractions.
@ -98,7 +98,7 @@ The obvious implication of the 8-bit bus cycle-eater is that byte-sized
memory variables should be used whenever possible. After all, the 8088 memory variables should be used whenever possible. After all, the 8088
performs *byte-sized* memory accesses just as quickly as the 8086. For performs *byte-sized* memory accesses just as quickly as the 8086. For
instance, Listing 4.1, which uses a byte-sized memory variable as a loop instance, Listing 4.1, which uses a byte-sized memory variable as a loop
counter, runs in 10.03 s per loop. That’s 20 percent faster than the counter, runs in 10.03 s per loop. That's 20 percent faster than the
12.05 µs per loop execution time of Listing 4.2, which uses a word-sized 12.05 µs per loop execution time of Listing 4.2, which uses a word-sized
counter. Why the difference in execution times? Simply because each counter. Why the difference in execution times? Simply because each
word-sized **DEC** performs 4 byte-sized memory accesses (two to read word-sized **DEC** performs 4 byte-sized memory accesses (two to read
@ -138,18 +138,18 @@ in all.
jnz LoopTop jnz LoopTop
call ZTimerOff call ZTimerOff
I’d like to make a brief aside concerning code optimization in the I'd like to make a brief aside concerning code optimization in the
listings in this book. Throughout this book I’ve modeled the sample code listings in this book. Throughout this book I've modeled the sample code
after working code so that the timing results are applicable to after working code so that the timing results are applicable to
real-world programming. In Listings 4.1 and 4.2, for example, I could real-world programming. In Listings 4.1 and 4.2, for example, I could
have shown a still greater advantage for byte-sized operands simply by have shown a still greater advantage for byte-sized operands simply by
performing 1,000 **DEC** instructions in a row, with no branching at performing 1,000 **DEC** instructions in a row, with no branching at
all. However, **DEC** instructions don’t exist in a vacuum, so in the all. However, **DEC** instructions don't exist in a vacuum, so in the
listings I used code that both decremented the counter and tested the listings I used code that both decremented the counter and tested the
result. The difference is that between decrementing a memory location result. The difference is that between decrementing a memory location
(simply an instruction) and using a loop counter (a functional (simply an instruction) and using a loop counter (a functional
instruction sequence). If you come across code in this book that seems instruction sequence). If you come across code in this book that seems
less than optimal, it’s simply due to my desire to provide code that’s less than optimal, it's simply due to my desire to provide code that's
relevant to real programming problems. On the other hand, optimal code relevant to real programming problems. On the other hand, optimal code
is an elusive thing indeed; by no means should you assume that the code is an elusive thing indeed; by no means should you assume that the code
in this book is ideal! Examine it, question it, and improve upon it, for in this book is ideal! Examine it, question it, and improve upon it, for

View file

@ -2,7 +2,7 @@
[Previous](04-02.html) [Table of Contents](index.html) [Next](04-04.html) [Previous](04-02.html) [Table of Contents](index.html) [Next](04-04.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
Back to the 8-bit bus cycle-eater. As I’ve said, in 8088 work you should Back to the 8-bit bus cycle-eater. As I've said, in 8088 work you should
strive to use byte-sized memory variables whenever possible. That does strive to use byte-sized memory variables whenever possible. That does
*not* mean that you should use 2 byte-sized memory accesses to *not* mean that you should use 2 byte-sized memory accesses to
manipulate a word-sized memory variable in preference to 1 word-sized manipulate a word-sized memory variable in preference to 1 word-sized
@ -18,16 +18,16 @@ versus:
Recall that every access to a memory byte takes at least 4 cycles; that Recall that every access to a memory byte takes at least 4 cycles; that
limitation is built right into the 8088. The 8088 is also built so that limitation is built right into the 8088. The 8088 is also built so that
the second byte-sized memory access to a 16-bit memory variable takes the second byte-sized memory access to a 16-bit memory variable takes
just those 4 cycles and no more. There’s no way you can manipulate the just those 4 cycles and no more. There's no way you can manipulate the
second byte of a word-sized memory variable faster with a second second byte of a word-sized memory variable faster with a second
separate byte-sized instruction in less than 4 cycles. As a matter of separate byte-sized instruction in less than 4 cycles. As a matter of
fact, you’re bound to access that second byte much more slowly with a fact, you're bound to access that second byte much more slowly with a
separate instruction, thanks to the overhead of instruction fetching and separate instruction, thanks to the overhead of instruction fetching and
execution, address calculation, and the like. execution, address calculation, and the like.
For example, consider Listing 4.3, which performs 1,000 word-sized reads For example, consider Listing 4.3, which performs 1,000 word-sized reads
from memory. This code runs in 3.77 µs per word read on a 4.77 MHz 8088. from memory. This code runs in 3.77 µs per word read on a 4.77 MHz 8088.
That’s 45 percent faster than the 5.49 µs per word read of Listing 4.4, That's 45 percent faster than the 5.49 µs per word read of Listing 4.4,
which reads the same 1,000 words as Listing 4.3 but does so with 2,000 which reads the same 1,000 words as Listing 4.3 but does so with 2,000
byte-sized reads. Both listings perform exactly the same number of byte-sized reads. Both listings perform exactly the same number of
memory accesses—2,000 accesses, each byte-sized, as all 8088 memory memory accesses—2,000 accesses, each byte-sized, as all 8088 memory
@ -67,14 +67,14 @@ efficient at that task than your code can possibly be.
Word-sized variables should be stored in registers to the greatest Word-sized variables should be stored in registers to the greatest
feasible extent, since registers are inside the 8088, where 16-bit feasible extent, since registers are inside the 8088, where 16-bit
operations are just as fast as 8-bit operations because the 8-bit operations are just as fast as 8-bit operations because the 8-bit
cycle-eater can’t get at them. In fact, it’s a good idea to keep as many cycle-eater can't get at them. In fact, it's a good idea to keep as many
variables of all sorts in registers as you can. Instructions with variables of all sorts in registers as you can. Instructions with
register-only operands execute very rapidly, partially because they register-only operands execute very rapidly, partially because they
avoid both the time-consuming memory accesses and the lengthy address avoid both the time-consuming memory accesses and the lengthy address
calculations associated with memory operands. calculations associated with memory operands.
There is yet another reason why register operands are preferable to There is yet another reason why register operands are preferable to
memory operands, and it’s an unexpected effect of the 8-bit bus memory operands, and it's an unexpected effect of the 8-bit bus
cycle-eater. Instructions with only register operands tend to be shorter cycle-eater. Instructions with only register operands tend to be shorter
(in terms of bytes) than instructions with memory operands, and when it (in terms of bytes) than instructions with memory operands, and when it
comes to performance, shorter is usually better. In order to explain why comes to performance, shorter is usually better. In order to explain why
@ -82,32 +82,32 @@ that is true and how it relates to the 8-bit bus cycle-eater, I must
diverge for a moment. diverge for a moment.
For the last few pages, you may well have been thinking that the 8-bit For the last few pages, you may well have been thinking that the 8-bit
bus cycle-eater, while a nuisance, doesn’t seem particularly subtle or bus cycle-eater, while a nuisance, doesn't seem particularly subtle or
difficult to quantify. After all, any instruction reference tells us difficult to quantify. After all, any instruction reference tells us
exactly how many cycles each instruction loses to the 8-bit bus exactly how many cycles each instruction loses to the 8-bit bus
cycle-eater, doesn’t it? cycle-eater, doesn't it?
Yes and no. It’s true that in general we know approximately how much Yes and no. It's true that in general we know approximately how much
longer a given instruction will take to execute with a word-sized memory longer a given instruction will take to execute with a word-sized memory
operand than with a byte-sized operand, although the dynamic RAM refresh operand than with a byte-sized operand, although the dynamic RAM refresh
and wait state cycle-eaters (which I’ll cover a little later) can raise and wait state cycle-eaters (which I'll cover a little later) can raise
the cost of the 8-bit bus cycle-eater considerably. However, *all* the cost of the 8-bit bus cycle-eater considerably. However, *all*
word-sized memory accesses lose 4 cycles to the 8-bit bus cycle-eater, word-sized memory accesses lose 4 cycles to the 8-bit bus cycle-eater,
and there’s one sort of word-sized memory access we haven’t discussed and there's one sort of word-sized memory access we haven't discussed
yet: instruction fetching. The ugliest manifestation of the 8-bit bus yet: instruction fetching. The ugliest manifestation of the 8-bit bus
cycle-eater is in fact the prefetch queue cycle-eater. cycle-eater is in fact the prefetch queue cycle-eater.
### The Prefetch Queue Cycle-Eater {#Heading9} ### The Prefetch Queue Cycle-Eater {#Heading9}
In an 8088 context, here’s the prefetch queue cycle-eater in a nutshell: In an 8088 context, here's the prefetch queue cycle-eater in a nutshell:
The 8088’s 8-bit external data bus keeps the Bus Interface Unit from The 8088's 8-bit external data bus keeps the Bus Interface Unit from
fetching instruction bytes as fast as the 16-bit Execution Unit can fetching instruction bytes as fast as the 16-bit Execution Unit can
execute them, so the Execution Unit often lies idle while waiting for execute them, so the Execution Unit often lies idle while waiting for
the next instruction byte to be fetched. the next instruction byte to be fetched.
Exactly why does this happen? Recall that the 8088 is an 8086 Exactly why does this happen? Recall that the 8088 is an 8086
internally, but accesses word-sized memory data at only one-half the internally, but accesses word-sized memory data at only one-half the
maximum rate of the 8086 due to the 8088’s 8-bit external data bus. maximum rate of the 8086 due to the 8088's 8-bit external data bus.
Unfortunately, instructions are among the word-sized data the 8086 Unfortunately, instructions are among the word-sized data the 8086
fetches, meaning that the 8088 can fetch instructions at only one-half fetches, meaning that the 8088 can fetch instructions at only one-half
the speed of the 8086. On the other hand, the 8086-equivalent Execution the speed of the 8086. On the other hand, the 8086-equivalent Execution
@ -119,24 +119,24 @@ up idling while waiting for instructions bytes to arrive.
The BIU can fetch instruction bytes at a maximum rate of one byte every The BIU can fetch instruction bytes at a maximum rate of one byte every
4 cycles—*and that 4-cycle per instruction byte rate is the ultimate 4 cycles—*and that 4-cycle per instruction byte rate is the ultimate
limit on overall instruction execution time, regardless of EU speed.* limit on overall instruction execution time, regardless of EU speed.*
While the EU may execute a given instruction that’s already in the While the EU may execute a given instruction that's already in the
prefetch queue in less than 4 cycles per byte, over time the EU can’t prefetch queue in less than 4 cycles per byte, over time the EU can't
execute instructions any faster than they can arrive—and they can’t execute instructions any faster than they can arrive—and they can't
arrive faster than 1 byte every 4 cycles. arrive faster than 1 byte every 4 cycles.
Clearly, then, the prefetch queue cycle-eater is nothing more than one Clearly, then, the prefetch queue cycle-eater is nothing more than one
aspect of the 8-bit bus cycle-eater. 8088 code often runs at less than aspect of the 8-bit bus cycle-eater. 8088 code often runs at less than
the Execution Unit’s maximum speed because the 8-bit data bus can’t keep the Execution Unit's maximum speed because the 8-bit data bus can't keep
up with the demand for instruction bytes. That’s straightforward up with the demand for instruction bytes. That's straightforward
enough—so why all the fuss about the prefetch queue cycle-eater? enough—so why all the fuss about the prefetch queue cycle-eater?
What makes the prefetch queue cycle-eater tricky is that it’s What makes the prefetch queue cycle-eater tricky is that it's
undocumented and unpredictable. That is, with a word-sized memory undocumented and unpredictable. That is, with a word-sized memory
access, such as access, such as
mov [bx],ax mov [bx],ax
it’s well-documented that an extra 4 cycles will always be required to it's well-documented that an extra 4 cycles will always be required to
write the upper byte of AX to memory. Not so with the prefetch queue write the upper byte of AX to memory. Not so with the prefetch queue
cycle-eater lurking nearby. For instance, the instructions cycle-eater lurking nearby. For instance, the instructions
@ -147,17 +147,17 @@ cycle-eater lurking nearby. For instance, the instructions
shr ax,1 shr ax,1
should execute in 10 cycles, since each **SHR** takes 2 cycles to should execute in 10 cycles, since each **SHR** takes 2 cycles to
execute, according to Intel’s specifications. Those specifications execute, according to Intel's specifications. Those specifications
contain Intel’s official instruction execution times, but in this contain Intel's official instruction execution times, but in this
case—and in many others—the specifications are drastically wrong. Why? case—and in many others—the specifications are drastically wrong. Why?
Because they describe execution time *once an instruction reaches the Because they describe execution time *once an instruction reaches the
prefetch queue.* They say nothing about whether a given instruction will prefetch queue.* They say nothing about whether a given instruction will
be in the prefetch queue when it’s time for that instruction to run, or be in the prefetch queue when it's time for that instruction to run, or
how long it will take that instruction to reach the prefetch queue if how long it will take that instruction to reach the prefetch queue if
it’s not there already. Thanks to the low performance of the 8088’s it's not there already. Thanks to the low performance of the 8088's
external data bus, that’s a glaring omission—but, alas, an unavoidable external data bus, that's a glaring omission—but, alas, an unavoidable
one. Let’s look at why the official execution times are wrong, and why one. Let's look at why the official execution times are wrong, and why
that can’t be helped. that can't be helped.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](04-02.html) [Table of Contents](index.html) [Next](04-04.html) [Previous](04-02.html) [Table of Contents](index.html) [Next](04-04.html)

View file

@ -9,16 +9,16 @@ 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 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 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 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 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 its documented speed, and could take up to four times longer than it is
supposed to. supposed to.
Why does Intel document Execution Unit execution time rather than Why does Intel document Execution Unit execution time rather than
overall instruction execution time, which includes both instruction overall instruction execution time, which includes both instruction
fetch time and Execution Unit (EU) execution time? Well, instruction fetch time and Execution Unit (EU) execution time? Well, instruction
fetching isn’t performed as part of instruction execution by the fetching isn't performed as part of instruction execution by the
Execution Unit, but instead is carried on in parallel by the Bus 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 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 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 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 the EU needs them, so in those cases instruction fetching takes no time
@ -34,14 +34,14 @@ overall execution time of the following instructions.
![](images/i.jpg) *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.* ![](images/i.jpg) *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 As we'll see later, other cycle-eaters, such as DRAM refresh and display
memory wait states, can cause prefetching variations even during memory wait states, can cause prefetching variations even during
different executions of the same code sequence. Given that, it’s different executions of the same code sequence. Given that, it's
meaningless to talk about the prefetch time of a given instruction meaningless to talk about the prefetch time of a given instruction
except in the context of a specific code sequence. except in the context of a specific code sequence.
So now you know why the official instruction execution times are often So now you know why the official instruction execution times are often
wrong, and why Intel can’t provide better specifications. You also know wrong, and why Intel can't provide better specifications. You also know
now why it is that you must time your code if you want to know how fast now why it is that you must time your code if you want to know how fast
it really is. it really is.
@ -57,16 +57,16 @@ the timer can cause problems.
Some slight prefetch queue-induced inaccuracy usually exists even when Some slight prefetch queue-induced inaccuracy usually exists even when
the Zen timer is used to time longer code sequences, since the calls to the Zen timer is used to time longer code sequences, since the calls to
the Zen timer usually alter the code’s prefetch queue from its normal the Zen timer usually alter the code's prefetch queue from its normal
state. (Branches—jumps, calls, returns and the like—empty the prefetch state. (Branches—jumps, calls, returns and the like—empty the prefetch
queue.) Ideally, the Zen timer is used to measure the performance of an queue.) Ideally, the Zen timer is used to measure the performance of an
entire subroutine, so the prefetch queue effects of the branches at the entire subroutine, so the prefetch queue effects of the branches at the
start and end of the subroutine are similar to the effects of the calls start and end of the subroutine are similar to the effects of the calls
to the Zen timer when you’re measuring the subroutine’s performance. to the Zen timer when you're measuring the subroutine's performance.
Another way in which the prefetch queue cycle-eater complicates the use Another way in which the prefetch queue cycle-eater complicates the use
of the Zen timer involves the practice of timing the performance of a of the Zen timer involves the practice of timing the performance of a
few instructions over and over. I’ll often repeat one or two few instructions over and over. I'll often repeat one or two
instructions 100 or 1,000 times in a row in listings in this book in instructions 100 or 1,000 times in a row in listings in this book in
order to get timing intervals that are long enough to provide reliable order to get timing intervals that are long enough to provide reliable
measurements. However, as we just learned, the actual performance of any measurements. However, as we just learned, the actual performance of any
@ -84,12 +84,12 @@ always empty, execution time should work out to about 4 cycles per byte,
or 8 cycles per **SHR,** as shown in Figure 4.3. (Figure 4.3 illustrates or 8 cycles per **SHR,** as shown in Figure 4.3. (Figure 4.3 illustrates
the relationship between instruction fetching and execution in a the relationship between instruction fetching and execution in a
simplified way, and is not intended to show the exact timings of 8088 simplified way, and is not intended to show the exact timings of 8088
operations.) That’s quite a contrast to the official 2-cycle execution operations.) That's quite a contrast to the official 2-cycle execution
time of **SHR**. In fact, the Zen timer reports that Listing 4.5 time of **SHR**. In fact, the Zen timer reports that Listing 4.5
executes in 1.81µs per byte, or slightly *more* than 4 cycles per byte. executes in 1.81µs per byte, or slightly *more* than 4 cycles per byte.
(The extra time is the result of the dynamic RAM refresh cycle-eater, (The extra time is the result of the dynamic RAM refresh cycle-eater,
which we’ll discuss shortly.) Going by Listing 4.5, we would conclude which we'll discuss shortly.) Going by Listing 4.5, we would conclude
that the “true” execution time of **SHR** is 8.64 cycles. that the "true" execution time of **SHR** is 8.64 cycles.
**LISTING 4.5 LST4-5.ASM** **LISTING 4.5 LST4-5.ASM**
@ -124,20 +124,20 @@ that the “true” execution time of **SHR** is 8.64 cycles.
**Figure 4.3**  *Execution and instruction prefetching sequence for **Figure 4.3**  *Execution and instruction prefetching sequence for
Listing 4.5.* Listing 4.5.*
Now let’s examine Listing 4.6. Here each **SHR** follows a **MUL** Now let's examine Listing 4.6. Here each **SHR** follows a **MUL**
instruction. Since **MUL** instructions take so long to execute that the instruction. Since **MUL** instructions take so long to execute that the
prefetch queue is always full when they finish, each **SHR** should be prefetch queue is always full when they finish, each **SHR** should be
ready and waiting in the prefetch queue when the preceding **MUL** ends. ready and waiting in the prefetch queue when the preceding **MUL** ends.
As a result, we’d expect that each **SHR** would execute in 2 cycles; As a result, we'd expect that each **SHR** would execute in 2 cycles;
together with the 118-cycle execution time of multiplying 0 times 0, the together with the 118-cycle execution time of multiplying 0 times 0, the
total execution time should come to 120 cycles per **SHR/MUL** pair, as total execution time should come to 120 cycles per **SHR/MUL** pair, as
shown in Figure 4.4. And, by God, when we run Listing 4.6 we get an shown in Figure 4.4. And, by God, when we run Listing 4.6 we get an
execution time of 25.14 µs per **SHR/MUL** pair, or *exactly* 120 execution time of 25.14 µs per **SHR/MUL** pair, or *exactly* 120
cycles! According to these results, the “true” execution time of **SHR** cycles! According to these results, the "true" execution time of **SHR**
would seem to be 2 cycles, quite a change from the conclusion we drew would seem to be 2 cycles, quite a change from the conclusion we drew
from Listing 4.5. from Listing 4.5.
The key point is this: We’ve seen one code sequence in which **SHR** The key point is this: We've seen one code sequence in which **SHR**
took 8-plus cycles to execute, and another in which it took only 2 took 8-plus cycles to execute, and another in which it took only 2
cycles. Are we talking about two different forms of **SHR** here? Of cycles. Are we talking about two different forms of **SHR** here? Of
course not—the difference is purely a reflection of the differing states course not—the difference is purely a reflection of the differing states

View file

@ -7,19 +7,19 @@ time—or even a mix of the two, if an instruction is partially
prefetched—can determine code performance. Some people operate under a prefetched—can determine code performance. Some people operate under a
rule of thumb by which they assume that the execution time of each rule of thumb by which they assume that the execution time of each
instruction is 4 cycles times the number of bytes in the instruction. instruction is 4 cycles times the number of bytes in the instruction.
While that’s often true for register-only code, it frequently doesn’t While that's often true for register-only code, it frequently doesn't
hold for code that accesses memory. For one thing, the rule should be 4 hold for code that accesses memory. For one thing, the rule should be 4
cycles times the number of *memory accesses,* not instruction bytes, cycles times the number of *memory accesses,* not instruction bytes,
since all accesses take 4 cycles on the 8088-based PC. For another, since all accesses take 4 cycles on the 8088-based PC. For another,
memory-accessing instructions often have slower Execution Unit execution memory-accessing instructions often have slower Execution Unit execution
times than the 4 cycles per memory access rule would dictate, because times than the 4 cycles per memory access rule would dictate, because
the 8088 isn’t very fast at calculating memory addresses. Also, the 4 the 8088 isn't very fast at calculating memory addresses. Also, the 4
cycles per instruction byte rule isn’t true for register-only cycles per instruction byte rule isn't true for register-only
instructions that are already in the prefetch queue when the preceding instructions that are already in the prefetch queue when the preceding
instruction ends. instruction ends.
The truth is that it never hurts performance to reduce either the cycle The truth is that it never hurts performance to reduce either the cycle
count or the byte count of a given bit of code, but there’s no guarantee count or the byte count of a given bit of code, but there's no guarantee
that one or the other will improve performance either. For example, that one or the other will improve performance either. For example,
consider Listing 4.7, which consists of a series of 4-cycle, 2-byte consider Listing 4.7, which consists of a series of 4-cycle, 2-byte
**MOV AL,0** instructions, and which executes at the rate of 1.81 µs per **MOV AL,0** instructions, and which executes at the rate of 1.81 µs per
@ -60,17 +60,17 @@ Listing 4.6.*
endm endm
call ZTimerOff call ZTimerOff
As you can see, it’s easy to be drawn into thinking you’re saving cycles 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 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 of code by reducing the factor—either instruction fetch time or
execution time, or sometimes a mix of the two—that’s limiting the execution time, or sometimes a mix of the two—that's limiting the
performance of that code. performance of that code.
In case you missed it in all the excitement, the variability of In case you missed it in all the excitement, the variability of
prefetching means that our method of testing performance by executing prefetching means that our method of testing performance by executing
1,000 instructions in a row by no means produces “true” instruction 1,000 instructions in a row by no means produces "true" instruction
execution times, any more than the official execution times in the Intel 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 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 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, 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 depending on the state of the prefetch queue when the preceding
@ -84,24 +84,24 @@ 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 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 the tool we need to gather that information. Granted, it would be easier
if we could just add up neatly documented instruction execution if we could just add up neatly documented instruction execution
times—but that’s not going to happen. Without actually measuring the 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 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 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 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 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 execution times are only meaningful in context, learn the rules of thumb
in this book, and use the Zen timer to measure your code. in this book, and use the Zen timer to measure your code.
#### Approximating Overall Execution Times {#Heading12} #### Approximating Overall Execution Times {#Heading12}
Don’t think that because overall instruction execution time is Don't think that because overall instruction execution time is
determined by both instruction fetch time and Execution Unit execution determined by both instruction fetch time and Execution Unit execution
time, the two times should be added together when estimating time, the two times should be added together when estimating
performance. For example, practically speaking, each **SHR** in Listing performance. For example, practically speaking, each **SHR** in Listing
4.5 does not take 8 cycles of instruction fetch time plus 2 cycles of 4.5 does not take 8 cycles of instruction fetch time plus 2 cycles of
Execution Unit execution time to execute. Figure 4.3 shows that while a Execution Unit execution time to execute. Figure 4.3 shows that while a
given **SHR** is executing, the fetch of the next **SHR** is starting, given **SHR** is executing, the fetch of the next **SHR** is starting,
and since the two operations are overlapped for 2 cycles, there’s no and since the two operations are overlapped for 2 cycles, there's no
sense in charging the time to both instructions. You could think of the sense in charging the time to both instructions. You could think of the
extra instruction fetch time for **SHR** in Listing 4.5 as being 6 extra instruction fetch time for **SHR** in Listing 4.5 as being 6
cycles, which yields an overall execution time of 8 cycles when added to cycles, which yields an overall execution time of 8 cycles when added to
@ -116,7 +116,7 @@ be counted toward the overall execution time of one of the instructions.
For all intents and purposes, one of the two instructions runs at no For all intents and purposes, one of the two instructions runs at no
performance cost whatsoever while the overlap exists. performance cost whatsoever while the overlap exists.
As a working definition, we’ll consider the execution time of a given As a working definition, we'll consider the execution time of a given
instruction in a particular context to start when the first byte of the instruction in a particular context to start when the first byte of the
instruction is sent to the Execution Unit and end when the first byte of instruction is sent to the Execution Unit and end when the first byte of
the next instruction is sent to the EU. the next instruction is sent to the EU.
@ -140,12 +140,12 @@ actually often suffer more from the prefetch queue bottleneck than do
long instructions. Short instructions generally have such fast execution long instructions. Short instructions generally have such fast execution
times that they drain the prefetch queue despite their small size. For times that they drain the prefetch queue despite their small size. For
example, consider the **SHR** of Listing 4.5, which runs at only 25 example, consider the **SHR** of Listing 4.5, which runs at only 25
percent of its Execution Unit execution time even though it’s only 2 percent of its Execution Unit execution time even though it's only 2
bytes long, thanks to the prefetch queue bottleneck. Short instructions bytes long, thanks to the prefetch queue bottleneck. Short instructions
are nonetheless generally faster than long instructions, thanks to the are nonetheless generally faster than long instructions, thanks to the
combination of fewer instruction bytes and faster Execution Unit combination of fewer instruction bytes and faster Execution Unit
execution times, and should be used as much as possible—just don’t execution times, and should be used as much as possible—just don't
expect them to run at their “official” documented speeds. expect them to run at their "official" documented speeds.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](04-04.html) [Table of Contents](index.html) [Next](04-06.html) [Previous](04-04.html) [Table of Contents](index.html) [Next](04-06.html)

View file

@ -4,10 +4,10 @@
More than anything, the above rules mean using the registers as heavily More than anything, the above rules mean using the registers as heavily
as possible, both because register-only instructions are short and as possible, both because register-only instructions are short and
because they don’t perform memory accesses to read or write operands. because they don't perform memory accesses to read or write operands.
However, using the registers is a rule of thumb, not a commandment. In However, using the registers is a rule of thumb, not a commandment. In
some circumstances, it may actually be *faster* to access memory. (The some circumstances, it may actually be *faster* to access memory. (The
look-up table technique is one such case.) What’s more, the performance look-up table technique is one such case.) What's more, the performance
of the prefetch queue (and hence the performance of each instruction) of the prefetch queue (and hence the performance of each instruction)
differs from one code sequence to the next, and can even differ during differs from one code sequence to the next, and can even differ during
different executions of the *same* code sequence. different executions of the *same* code sequence.
@ -20,42 +20,42 @@ bottom line.
#### Holding Up the 8088 {#Heading14} #### Holding Up the 8088 {#Heading14}
In this chapter I’ve taken you further and further into the depths of In this chapter I've taken you further and further into the depths of
the PC, telling you again and again that you must understand the the PC, telling you again and again that you must understand the
computer at the lowest possible level in order to write good code. At computer at the lowest possible level in order to write good code. At
this point, you may well wonder, “Have we gotten low enough?” this point, you may well wonder, "Have we gotten low enough?"
Not quite yet. The 8-bit bus and prefetch queue cycle-eaters are Not quite yet. The 8-bit bus and prefetch queue cycle-eaters are
low-level indeed, but we’ve one level yet to go. Dynamic RAM refresh and low-level indeed, but we've one level yet to go. Dynamic RAM refresh and
wait states—our next topics—together form the lowest level at which the wait states—our next topics—together form the lowest level at which the
hardware of the PC affects code performance. Below this level, the PC is hardware of the PC affects code performance. Below this level, the PC is
of interest only to hardware engineers. of interest only to hardware engineers.
Before we begin our discussion of dynamic RAM refresh, let’s step back Before we begin our discussion of dynamic RAM refresh, let's step back
for a moment to take an overall look at this lowest level of for a moment to take an overall look at this lowest level of
cycle-eaters. In truth, the distinctions between wait states and dynamic cycle-eaters. In truth, the distinctions between wait states and dynamic
RAM refresh don’t much matter to a programmer. What is important is that RAM refresh don't much matter to a programmer. What is important is that
you understand this: *Under certain circumstances, devices on the PC bus you understand this: *Under certain circumstances, devices on the PC bus
can stop the CPU for 1 or more cycles, making your code run more slowly can stop the CPU for 1 or more cycles, making your code run more slowly
than it seemingly should.* than it seemingly should.*
Unlike all the cycle-eaters we’ve encountered so far, wait states and Unlike all the cycle-eaters we've encountered so far, wait states and
dynamic RAM refresh are strictly external to the CPU, as was shown in dynamic RAM refresh are strictly external to the CPU, as was shown in
Figure 4.1. Adapters on the PC’s bus, such as video and memory cards, Figure 4.1. Adapters on the PC's bus, such as video and memory cards,
can insert wait states on any bus access, the idea being that they won’t can insert wait states on any bus access, the idea being that they won't
be able to complete the access properly unless the access is stretched be able to complete the access properly unless the access is stretched
out. Likewise, the channel of the DMA controller dedicated to dynamic out. Likewise, the channel of the DMA controller dedicated to dynamic
RAM refresh can request control of the bus at any time, although the CPU RAM refresh can request control of the bus at any time, although the CPU
must relinquish the bus before the DMA controller can take over. This must relinquish the bus before the DMA controller can take over. This
means that your code can’t directly control wait states or dynamic RAM means that your code can't directly control wait states or dynamic RAM
refresh. However, code *can* sometimes be designed to minimize the refresh. However, code *can* sometimes be designed to minimize the
effects of these cycle-eaters, and even when the cycle-eaters slow your effects of these cycle-eaters, and even when the cycle-eaters slow your
code without there being a thing in the world you can do about it, code without there being a thing in the world you can do about it,
you’re still better off understanding that you’re losing performance and you're still better off understanding that you're losing performance and
knowing why your code doesn’t run as fast as it’s supposed to than you knowing why your code doesn't run as fast as it's supposed to than you
were programming in ignorance. were programming in ignorance.
Let’s start with DRAM refresh, which affects the performance of every Let's start with DRAM refresh, which affects the performance of every
program that runs on the PC. program that runs on the PC.
### Dynamic RAM Refresh: The Invisible Hand {#Heading15} ### Dynamic RAM Refresh: The Invisible Hand {#Heading15}
@ -64,29 +64,29 @@ Dynamic RAM (DRAM) refresh is sort of an act of God. By that I mean that
DRAM refresh invisibly and inexorably steals a certain fraction of all DRAM refresh invisibly and inexorably steals a certain fraction of all
available memory access time from your programs, when they are accessing available memory access time from your programs, when they are accessing
memory for code and data. (When they are accessing cache on more recent memory for code and data. (When they are accessing cache on more recent
processors, theoretically the DRAM refresh cycle-eater doesn’t come into processors, theoretically the DRAM refresh cycle-eater doesn't come into
play, but there are other cycle-eaters waiting to prey on cache-bound play, but there are other cycle-eaters waiting to prey on cache-bound
programs.) While you *could* stop DRAM refresh, you wouldn’t want to programs.) While you *could* stop DRAM refresh, you wouldn't want to
since that would be a sure prescription for crashing your computer. In since that would be a sure prescription for crashing your computer. In
the end, thanks to DRAM refresh, almost all code runs a bit slower on the end, thanks to DRAM refresh, almost all code runs a bit slower on
the PC than it otherwise would, and that’s that. the PC than it otherwise would, and that's that.
A bit of background: A static RAM (SRAM) chip is a memory chip that A bit of background: A static RAM (SRAM) chip is a memory chip that
retains its contents indefinitely so long as power is maintained. By retains its contents indefinitely so long as power is maintained. By
contrast, each of several blocks of bits in a dynamic RAM (DRAM) chip contrast, each of several blocks of bits in a dynamic RAM (DRAM) chip
retains its contents for only a short time after it’s accessed for a retains its contents for only a short time after it's accessed for a
read or write. In order to get a DRAM chip to store data for an extended read or write. In order to get a DRAM chip to store data for an extended
period, each of the blocks of bits in that chip must be accessed period, each of the blocks of bits in that chip must be accessed
regularly, so that the chip’s stored data is kept refreshed and valid. regularly, so that the chip's stored data is kept refreshed and valid.
So long as this is done often enough, a DRAM chip will retain its So long as this is done often enough, a DRAM chip will retain its
contents indefinitely. contents indefinitely.
All of the PC’s system memory consists of DRAM chips. Each DRAM chip in All of the PC's system memory consists of DRAM chips. Each DRAM chip in
the PC must be completely refreshed about once every four milliseconds the PC must be completely refreshed about once every four milliseconds
in order to ensure the integrity of the data it stores. Obviously, it’s in order to ensure the integrity of the data it stores. Obviously, it's
highly desirable that the memory in the PC retain the correct data highly desirable that the memory in the PC retain the correct data
indefinitely, so each DRAM chip in the PC *must* always be refreshed indefinitely, so each DRAM chip in the PC *must* always be refreshed
within 4 µs of the last refresh. Since there’s no guarantee that a given within 4 µs of the last refresh. Since there's no guarantee that a given
program will access each and every DRAM block once every 4 µs, the PC program will access each and every DRAM block once every 4 µs, the PC
contains special circuitry and programming for providing DRAM refresh. contains special circuitry and programming for providing DRAM refresh.
@ -100,25 +100,25 @@ signal. (DMA stands for *direct memory access,* the ability of a device
other than the 8088 to control the bus and access memory directly, other than the 8088 to control the bus and access memory directly,
without any help from the 8088.) As soon as the 8088 is between memory without any help from the 8088.) As soon as the 8088 is between memory
accesses, it gives control of the bus to the 8237, which in conjunction accesses, it gives control of the bus to the 8237, which in conjunction
with special circuitry on the PC’s motherboard then performs a single with special circuitry on the PC's motherboard then performs a single
4-cycle read access to 1 of 256 possible addresses, advancing to the 4-cycle read access to 1 of 256 possible addresses, advancing to the
next address on each successive access. (The read access is only for the next address on each successive access. (The read access is only for the
purpose of refreshing the DRAM; the data that is read isn’t used.) purpose of refreshing the DRAM; the data that is read isn't used.)
The 256 addresses accessed by the refresh DMA accesses are arranged so The 256 addresses accessed by the refresh DMA accesses are arranged so
that taken together they properly refresh all the memory in the PC. By that taken together they properly refresh all the memory in the PC. By
accessing one of the 256 addresses every 15.08 µs, all of the PC’s DRAM accessing one of the 256 addresses every 15.08 µs, all of the PC's DRAM
is refreshed in 256 x 15.08 µs, or 3.86 µs, which is just about the is refreshed in 256 x 15.08 µs, or 3.86 µs, which is just about the
desired 4 µs time I mentioned earlier. (Only the first 640K of memory is desired 4 µs time I mentioned earlier. (Only the first 640K of memory is
refreshed in the PC; video adapters and other adapters above 640K refreshed in the PC; video adapters and other adapters above 640K
containing memory that requires refreshing must provide their own DRAM containing memory that requires refreshing must provide their own DRAM
refresh in pre-AT systems.) refresh in pre-AT systems.)
Don’t sweat the details here. The important point is this: For at least Don't sweat the details here. The important point is this: For at least
4 out of every 72 cycles, the original PC’s bus is given over to DRAM 4 out of every 72 cycles, the original PC's bus is given over to DRAM
refresh and is not available to the 8088, as shown in Figure 4.5. That refresh and is not available to the 8088, as shown in Figure 4.5. That
means that as much as 5.56 percent of the PC’s already inadequate bus means that as much as 5.56 percent of the PC's already inadequate bus
capacity is lost. However, DRAM refresh doesn’t necessarily stop the capacity is lost. However, DRAM refresh doesn't necessarily stop the
8088 in its tracks for 4 cycles. The Execution Unit of the 8088 can keep 8088 in its tracks for 4 cycles. The Execution Unit of the 8088 can keep
processing while DRAM refresh is occurring, unless the EU needs to processing while DRAM refresh is occurring, unless the EU needs to
access memory. Consequently, DRAM refresh can slow code performance access memory. Consequently, DRAM refresh can slow code performance

View file

@ -4,7 +4,7 @@
#### The Impact of DRAM Refresh {#Heading17} #### The Impact of DRAM Refresh {#Heading17}
Let’s look at examples from opposite ends of the spectrum in terms of Let's look at examples from opposite ends of the spectrum in terms of
the impact of DRAM refresh on code performance. First, consider the the impact of DRAM refresh on code performance. First, consider the
series of **MUL** instructions in Listing 4.9. Since a 16-bit **MUL** on series of **MUL** instructions in Listing 4.9. Since a 16-bit **MUL** on
the 8088 executes in between 118 and 133 cycles and is only 2 bytes the 8088 executes in between 118 and 133 cycles and is only 2 bytes
@ -15,7 +15,7 @@ keep the Execution Unit well-supplied with instruction bytes at all
times. Since Listing 4.9 uses no memory operands, the Execution Unit times. Since Listing 4.9 uses no memory operands, the Execution Unit
should never have to wait for data from memory, and DRAM refresh should should never have to wait for data from memory, and DRAM refresh should
have no impact on performance. (Remember that the Execution Unit can have no impact on performance. (Remember that the Execution Unit can
operate normally during DRAM refreshes so long as it doesn’t need to operate normally during DRAM refreshes so long as it doesn't need to
request a memory access from the Bus Interface Unit.) request a memory access from the Bus Interface Unit.)
**LISTING 4.9 LST4-9.ASM** **LISTING 4.9 LST4-9.ASM**
@ -33,13 +33,13 @@ request a memory access from the Bus Interface Unit.)
call ZTimerOff call ZTimerOff
Running Listing 4.9, we find that each **MUL** executes in 24.72 µs, or Running Listing 4.9, we find that each **MUL** executes in 24.72 µs, or
exactly 118 cycles. Since that’s the shortest time in which **MUL** can exactly 118 cycles. Since that's the shortest time in which **MUL** can
execute, we can see that no performance is lost to DRAM refresh. Listing execute, we can see that no performance is lost to DRAM refresh. Listing
4.9 clearly illustrates that DRAM refresh only affects code performance 4.9 clearly illustrates that DRAM refresh only affects code performance
when a DRAM refresh forces the Execution Unit of the 8088 to wait for a when a DRAM refresh forces the Execution Unit of the 8088 to wait for a
memory access. memory access.
Now let’s look at the series of **SHR** instructions shown in Listing Now let's look at the series of **SHR** instructions shown in Listing
4.10. Since **SHR** executes in 2 cycles but is 2 bytes long, the 4.10. Since **SHR** executes in 2 cycles but is 2 bytes long, the
prefetch queue should be empty while Listing 4.10 executes, with the prefetch queue should be empty while Listing 4.10 executes, with the
8088 prefetching instruction bytes non-stop. As a result, the time per 8088 prefetching instruction bytes non-stop. As a result, the time per
@ -58,18 +58,18 @@ to fetch the instruction bytes.
endm endm
call ZTimerOff call ZTimerOff
Since 4 cycles are required to read each instruction byte, we’d expect Since 4 cycles are required to read each instruction byte, we'd expect
each **SHR** to execute in 8 cycles, or 1.676 µs, if there were no DRAM each **SHR** to execute in 8 cycles, or 1.676 µs, if there were no DRAM
refresh. In fact, each **SHR** in Listing 4.10 executes in 1.81 µs, refresh. In fact, each **SHR** in Listing 4.10 executes in 1.81 µs,
indicating that DRAM refresh is taking 7.4 percent of the program’s indicating that DRAM refresh is taking 7.4 percent of the program's
execution time. That’s nearly 2 percent more than our worst-case execution time. That's nearly 2 percent more than our worst-case
estimate of the loss to DRAM refresh overhead! In fact, the result estimate of the loss to DRAM refresh overhead! In fact, the result
indicates that DRAM refresh is stealing not 4, but 5.33 cycles out of indicates that DRAM refresh is stealing not 4, but 5.33 cycles out of
every 72 cycles. How can this be? every 72 cycles. How can this be?
The answer is that a given DRAM refresh can actually hold up CPU memory The answer is that a given DRAM refresh can actually hold up CPU memory
accesses for as many as 6 cycles, depending on the timing of the DRAM accesses for as many as 6 cycles, depending on the timing of the DRAM
refresh’s DMA request relative to the 8088’s internal instruction refresh's DMA request relative to the 8088's internal instruction
execution state. When the code in Listing 4.10 runs, each DRAM refresh execution state. When the code in Listing 4.10 runs, each DRAM refresh
holds up the CPU for either 5 or 6 cycles, depending on where the 8088 holds up the CPU for either 5 or 6 cycles, depending on where the 8088
is in executing the current **SHR** instruction when the refresh request is in executing the current **SHR** instruction when the refresh request
@ -77,7 +77,7 @@ occurs. Now we see that things can get even worse than we thought: *DRAM
refresh can steal as much as 8.33 percent of available memory access refresh can steal as much as 8.33 percent of available memory access
time—6 out of every 72 cycles—from the 8088.* time—6 out of every 72 cycles—from the 8088.*
Which of the two cases we’ve examined reflects reality? While either Which of the two cases we've examined reflects reality? While either
case *can* happen, the latter case—significant performance reduction, case *can* happen, the latter case—significant performance reduction,
ranging as high as 8.33 percent—is far more likely to occur. This is ranging as high as 8.33 percent—is far more likely to occur. This is
especially true for high-performance assembly code, which uses fast especially true for high-performance assembly code, which uses fast
@ -93,8 +93,8 @@ DRAM refresh cycle-eater?
Nothing. Nothing.
As I’ve said before, DRAM refresh is an act of God. DRAM refresh is a As I've said before, DRAM refresh is an act of God. DRAM refresh is a
fundamental, unchanging part of the PC’s operation, and there’s nothing fundamental, unchanging part of the PC's operation, and there's nothing
you or I can do about it. If refresh were any less frequent, the you or I can do about it. If refresh were any less frequent, the
reliability of the PC would be compromised, so tinkering with either reliability of the PC would be compromised, so tinkering with either
timer 1 or DMA channel 0 to reduce DRAM refresh overhead is out. Nor is timer 1 or DMA channel 0 to reduce DRAM refresh overhead is out. Nor is
@ -102,7 +102,7 @@ there any way to structure code to minimize the impact of DRAM refresh.
Sure, some instructions are affected less by DRAM refresh than others, Sure, some instructions are affected less by DRAM refresh than others,
but how many multiplies and divides in a row can you really use? I but how many multiplies and divides in a row can you really use? I
suppose that code *could* conceivably be structured to leave a free suppose that code *could* conceivably be structured to leave a free
memory access every 72 cycles, so DRAM refresh wouldn’t have any effect. memory access every 72 cycles, so DRAM refresh wouldn't have any effect.
In the old days when code size was measured in bytes, not K bytes, and In the old days when code size was measured in bytes, not K bytes, and
processors were less powerful—and complex—programmers did in fact use processors were less powerful—and complex—programmers did in fact use
similar tricks to eke every last bit of performance from their code. similar tricks to eke every last bit of performance from their code.
@ -112,35 +112,35 @@ modest performance improvement that did result could never justify the
increase in programming complexity and the limits on creative increase in programming complexity and the limits on creative
programming that such an approach would entail. Besides, all that effort programming that such an approach would entail. Besides, all that effort
goes to waste on faster 8088s, 286s, and other computers with different goes to waste on faster 8088s, 286s, and other computers with different
execution speeds and refresh characteristics. There’s no way around it: execution speeds and refresh characteristics. There's no way around it:
Useful code accesses memory frequently and at irregular intervals, and Useful code accesses memory frequently and at irregular intervals, and
over the long haul DRAM refresh always exacts its price. over the long haul DRAM refresh always exacts its price.
If you’re still harboring thoughts of reducing the overhead of DRAM If you're still harboring thoughts of reducing the overhead of DRAM
refresh, consider this. Instructions that tend not to suffer very much refresh, consider this. Instructions that tend not to suffer very much
from DRAM refresh are those that have a high ratio of execution time to from DRAM refresh are those that have a high ratio of execution time to
instruction fetch time, and those aren’t the fastest instructions of the instruction fetch time, and those aren't the fastest instructions of the
PC. It certainly wouldn’t make sense to use slower instructions just to PC. It certainly wouldn't make sense to use slower instructions just to
reduce DRAM refresh overhead, for it’s *total* execution time—DRAM reduce DRAM refresh overhead, for it's *total* execution time—DRAM
refresh, instruction fetching, and all—that matters. refresh, instruction fetching, and all—that matters.
The important thing to understand about DRAM refresh is that it The important thing to understand about DRAM refresh is that it
generally slows your code down, and that the extent of that performance generally slows your code down, and that the extent of that performance
reduction can vary considerably and unpredictably, depending on how the reduction can vary considerably and unpredictably, depending on how the
DRAM refreshes interact with your code’s pattern of memory accesses. DRAM refreshes interact with your code's pattern of memory accesses.
When you use the Zen timer and get a fractional cycle count for the When you use the Zen timer and get a fractional cycle count for the
execution time of an instruction, that’s often the DRAM refresh execution time of an instruction, that's often the DRAM refresh
cycle-eater at work. (The display adapter cycleis another possible cycle-eater at work. (The display adapter cycleis another possible
culprit, and, on 386s and later processors, cache misses and pipeline culprit, and, on 386s and later processors, cache misses and pipeline
execution hazards produce this sort of effect as well.) Whenever you get execution hazards produce this sort of effect as well.) Whenever you get
two timing results that differ less or more than they seemingly should, two timing results that differ less or more than they seemingly should,
that’s usually DRAM refresh too. Thanks to DRAM refresh, variations of that's usually DRAM refresh too. Thanks to DRAM refresh, variations of
up to 8.33 percent in PC code performance are par for the course. up to 8.33 percent in PC code performance are par for the course.
### Wait States {#Heading19} ### Wait States {#Heading19}
Wait states are cycles during which a bus access by the CPU to a device Wait states are cycles during which a bus access by the CPU to a device
on the PC’s bus is temporarily halted by that device while the device on the PC's bus is temporarily halted by that device while the device
gets ready to complete the read or write. Wait states are well and truly gets ready to complete the read or write. Wait states are well and truly
the lowest level of code performance. Everything we have discussed (and the lowest level of code performance. Everything we have discussed (and
will discuss)—even DMA accesses—can be affected by wait states. will discuss)—even DMA accesses—can be affected by wait states.

View file

@ -5,17 +5,17 @@
Wait states exist because the CPU must to be able to coexist with any Wait states exist because the CPU must to be able to coexist with any
adapter, no matter how slow (within reason). The 8088 expects to be able adapter, no matter how slow (within reason). The 8088 expects to be able
to complete each bus access—a memory or I/O read or write—in 4 cycles, to complete each bus access—a memory or I/O read or write—in 4 cycles,
but adapters can’t always respond that quickly for a number of reasons. but adapters can't always respond that quickly for a number of reasons.
For example, display adapters must split access to display memory For example, display adapters must split access to display memory
between the CPU and the circuitry that generates the video signal based between the CPU and the circuitry that generates the video signal based
on the contents of display memory, so they often can’t immediately on the contents of display memory, so they often can't immediately
fulfill a request by the CPU for a display memory read or write. To fulfill a request by the CPU for a display memory read or write. To
resolve this conflict, display adapters can tell the CPU to wait during resolve this conflict, display adapters can tell the CPU to wait during
bus accesses by inserting one or more wait states, as shown in Figure bus accesses by inserting one or more wait states, as shown in Figure
4.6. The CPU simply sits and idles as long as wait states are inserted, 4.6. The CPU simply sits and idles as long as wait states are inserted,
then completes the access as soon as the display adapter indicates its then completes the access as soon as the display adapter indicates its
readiness by no longer inserting wait states. The same would be true of readiness by no longer inserting wait states. The same would be true of
any adapter that couldn’t keep up with the CPU. any adapter that couldn't keep up with the CPU.
Mind you, this is all transparent to executing code. An instruction that Mind you, this is all transparent to executing code. An instruction that
encounters wait states runs exactly as if there were no wait states, encounters wait states runs exactly as if there were no wait states,
@ -23,11 +23,11 @@ only slower. Wait states are nothing more or less than wasted time as
far as the CPU and your program are concerned. far as the CPU and your program are concerned.
By understanding the circumstances in which wait states can occur, you By understanding the circumstances in which wait states can occur, you
can avoid them when possible. Even when it’s not possible to work around can avoid them when possible. Even when it's not possible to work around
wait states, it’s still to your advantage to understand how they can wait states, it's still to your advantage to understand how they can
cause your code to run more slowly. cause your code to run more slowly.
First, let’s learn a bit more about wait states by contrast with DRAM First, let's learn a bit more about wait states by contrast with DRAM
refresh. Unlike DRAM refresh, wait states do not occur on any regularly refresh. Unlike DRAM refresh, wait states do not occur on any regularly
scheduled basis, and are of no particular duration. Wait states can only scheduled basis, and are of no particular duration. Wait states can only
occur when an instruction performs a memory or I/O read or write. Both occur when an instruction performs a memory or I/O read or write. Both
@ -43,9 +43,9 @@ perform the access.
![](images/04-06.jpg)\ ![](images/04-06.jpg)\
**Figure 4.6**  *Video wait states inserted by the display adapter.* **Figure 4.6**  *Video wait states inserted by the display adapter.*
As with DRAM refresh, wait states don’t stop the 8088 completely. The As with DRAM refresh, wait states don't stop the 8088 completely. The
Execution Unit can continue processing while wait states are inserted, Execution Unit can continue processing while wait states are inserted,
so long as the EU doesn’t need to perform a bus access. However, in the so long as the EU doesn't need to perform a bus access. However, in the
PC, wait states most often occur when an instruction accesses a memory PC, wait states most often occur when an instruction accesses a memory
operand, so in fact the Execution Unit usually is stopped by wait operand, so in fact the Execution Unit usually is stopped by wait
states. (Instruction fetches rarely wait in an 8088-based PC because states. (Instruction fetches rarely wait in an 8088-based PC because
@ -96,11 +96,11 @@ VGA clones.)
![](images/04-07.jpg)\ ![](images/04-07.jpg)\
**Figure 4.7**  *Allocation of display memory access.* **Figure 4.7**  *Allocation of display memory access.*
Second, because the displayed dots (or *pixels,* short for “picture Second, because the displayed dots (or *pixels,* short for "picture
elements”) must be drawn on the screen at a constant speed, many display elements") must be drawn on the screen at a constant speed, many display
adapters provide memory accesses only at fixed intervals. As a result, adapters provide memory accesses only at fixed intervals. As a result,
time can be lost while the 8088 synchronizes with the start of the next time can be lost while the 8088 synchronizes with the start of the next
display adapter memory access, even if the video circuitry isn’t display adapter memory access, even if the video circuitry isn't
accessing display memory at that time, as shown in Figure 4.8. accessing display memory at that time, as shown in Figure 4.8.
Finally, the time it takes a display adapter to complete a memory access Finally, the time it takes a display adapter to complete a memory access
@ -110,27 +110,27 @@ the time taken for display memory to complete an 8088 read or write
access is often longer than the time taken for system memory to complete access is often longer than the time taken for system memory to complete
an access, even if the 8088 lucks into hitting a free display memory an access, even if the 8088 lucks into hitting a free display memory
access just as it becomes available, again as shown in Figure 4.8. Any access just as it becomes available, again as shown in Figure 4.8. Any
or all of the three factors I’ve described can result in wait states, or all of the three factors I've described can result in wait states,
slowing the 8088 and creating the display adapter cycle. slowing the 8088 and creating the display adapter cycle.
![](images/04-08.jpg)\ ![](images/04-08.jpg)\
**Figure 4.8**  *Display memory access slots.* **Figure 4.8**  *Display memory access slots.*
If some of this is Greek to you, don’t worry. The important point is If some of this is Greek to you, don't worry. The important point is
that display memory is not very fast compared to normal system memory. that display memory is not very fast compared to normal system memory.
How slow is it? *Incredibly* slow. Remember how slow IBM’s ill-fated How slow is it? *Incredibly* slow. Remember how slow IBM's ill-fated
PC*jr*was? In case you’ve forgotten, I’ll refresh your memory: The PC*jr*was? In case you've forgotten, I'll refresh your memory: The
PC*jr*was at best only half as fast as the PC. The PC*jr* had an 8088 PC*jr*was at best only half as fast as the PC. The PC*jr* had an 8088
running at 4.77 MHz, just like the PC—why do you suppose it was so much running at 4.77 MHz, just like the PC—why do you suppose it was so much
slower? I’ll tell you why: *All the memory in the PCjr was display slower? I'll tell you why: *All the memory in the PCjr was display
memory.* memory.*
Enough said. All the memory in the PC is *not* display memory, however, Enough said. All the memory in the PC is *not* display memory, however,
and unless you’re thickheaded enough to put code in display memory, the and unless you're thickheaded enough to put code in display memory, the
PC isn’t going to run as slowly as a PC*jr.* (Putting code or other PC isn't going to run as slowly as a PC*jr.* (Putting code or other
non-video data in unused areas of display memory sounds like a neat non-video data in unused areas of display memory sounds like a neat
idea—until you consider the effect on instruction prefetching of cutting idea—until you consider the effect on instruction prefetching of cutting
the 8088’s already-poor memory access performance in half. Running your the 8088's already-poor memory access performance in half. Running your
code from display memory is sort of like running on a hypothetical code from display memory is sort of like running on a hypothetical
8084—an 8086 with a *4-bit* bus. Not recommended!) Given that your code 8084—an 8086 with a *4-bit* bus. Not recommended!) Given that your code
and data reside in normal system memory below the 640K mark, how great and data reside in normal system memory below the 640K mark, how great

View file

@ -3,43 +3,43 @@
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
The answer varies considerably depending on what display adapter and The answer varies considerably depending on what display adapter and
what display mode we’re talking about. The display adapter cycle-eater what display mode we're talking about. The display adapter cycle-eater
is worst with the Enhanced Graphics Adapter (EGA) and the original Video is worst with the Enhanced Graphics Adapter (EGA) and the original Video
Graphics Array (VGA). (Many VGAs, especially newer ones, insert many Graphics Array (VGA). (Many VGAs, especially newer ones, insert many
fewer wait states than IBM’s original VGA. On the other hand, Super VGAs fewer wait states than IBM's original VGA. On the other hand, Super VGAs
have more bytes of display memory to be accessed in high-resolution have more bytes of display memory to be accessed in high-resolution
mode.) While the Color/Graphics Adapter (CGA), Monochrome Display mode.) While the Color/Graphics Adapter (CGA), Monochrome Display
Adapter (MDA), and Hercules Graphics Card (HGC) all suffer from the Adapter (MDA), and Hercules Graphics Card (HGC) all suffer from the
display adapter cycle-eater as well, they suffer to a lesser degree. display adapter cycle-eater as well, they suffer to a lesser degree.
Since the VGA represents the base standard for PC graphics now and for Since the VGA represents the base standard for PC graphics now and for
the foreseeable future, and since it is the hardest graphics adapter to the foreseeable future, and since it is the hardest graphics adapter to
wring performance from, we’ll restrict our discussion to the VGA (and wring performance from, we'll restrict our discussion to the VGA (and
its close relative, the EGA) for the remainder of this chapter. its close relative, the EGA) for the remainder of this chapter.
#### The Impact of the Display Adapter Cycle-Eater {#Heading21} #### The Impact of the Display Adapter Cycle-Eater {#Heading21}
Even on the EGA and VGA, the effect of the display adapter cycle-eater Even on the EGA and VGA, the effect of the display adapter cycle-eater
depends on the display mode selected. In text mode, the display adapter depends on the display mode selected. In text mode, the display adapter
cycle-eater is rarely a major factor. It’s not that the cycle-eater cycle-eater is rarely a major factor. It's not that the cycle-eater
isn’t present; however, a mere 4,000 bytes control the entire text mode isn't present; however, a mere 4,000 bytes control the entire text mode
display, and even with the display adapter cycle-eater it just doesn’t display, and even with the display adapter cycle-eater it just doesn't
take that long to manipulate 4,000 bytes. Even if the display adapter take that long to manipulate 4,000 bytes. Even if the display adapter
cycle-eater were to cause the 8088 to take as much as 5µs per display cycle-eater were to cause the 8088 to take as much as 5µs per display
memory access—more than five times normal—it would still take only memory access—more than five times normal—it would still take only
4,000x 2x 5µs, or 40 µs, to read and write every byte of display memory. 4,000x 2x 5µs, or 40 µs, to read and write every byte of display memory.
That’s a lot of time as measured in 8088 cycles, but it’s less than the That's a lot of time as measured in 8088 cycles, but it's less than the
blink of an eye in human time, and video performance only matters in blink of an eye in human time, and video performance only matters in
human time. After all, the whole point of drawing graphics is to convey human time. After all, the whole point of drawing graphics is to convey
visual information, and if that information can be presented faster than visual information, and if that information can be presented faster than
the eye can see, that is by definition fast enough. the eye can see, that is by definition fast enough.
That’s not to say that the display adapter cycle-eater *can’t* matter in That's not to say that the display adapter cycle-eater *can't* matter in
text mode. In Chapter 3, I recounted the story of a debate among text mode. In Chapter 3, I recounted the story of a debate among
letter-writers to a magazine about exactly how quickly characters could letter-writers to a magazine about exactly how quickly characters could
be written to display memory without causing snow. The writers carefully be written to display memory without causing snow. The writers carefully
added up Intel’s instruction cycle times to see how many writes to added up Intel's instruction cycle times to see how many writes to
display memory they could squeeze into a single horizontal retrace display memory they could squeeze into a single horizontal retrace
interval. (On a CGA, it’s only during the short horizontal retrace interval. (On a CGA, it's only during the short horizontal retrace
interval and the longer vertical retrace interval that display memory interval and the longer vertical retrace interval that display memory
can be accessed in 80-column text mode without causing snow.) Of course, can be accessed in 80-column text mode without causing snow.) Of course,
now we know that their cardinal sin was to ignore the prefetch queue; now we know that their cardinal sin was to ignore the prefetch queue;
@ -63,15 +63,15 @@ graphics modes, the cumulative effects of display memory wait states can
seriously impact code performance, even as measured in human time. seriously impact code performance, even as measured in human time.
For example, if we assume the same 5 µs per display memory access for For example, if we assume the same 5 µs per display memory access for
the EGA’s high-resolution graphics mode that we assumed for text mode, the EGA's high-resolution graphics mode that we assumed for text mode,
it would take 26,000 x 2 x 5 µs, or 260 µs, to scroll the screen once in it would take 26,000 x 2 x 5 µs, or 260 µs, to scroll the screen once in
the EGA’s high-resolution graphics mode, mode 10H. That’s more than the EGA's high-resolution graphics mode, mode 10H. That's more than
one-quarter of a second—noticeable by human standards, an eternity by one-quarter of a second—noticeable by human standards, an eternity by
computer standards. computer standards.
That sounds pretty serious, but we did make an unfounded assumption That sounds pretty serious, but we did make an unfounded assumption
about memory access speed. Let’s get some hard numbers. Listing 4.11 about memory access speed. Let's get some hard numbers. Listing 4.11
accesses display memory at the 8088’s maximum speed, by way of a **REP accesses display memory at the 8088's maximum speed, by way of a **REP
MOVSW** with display memory as both source and destination. The code in MOVSW** with display memory as both source and destination. The code in
Listing 4.11 executes in 3.18 µs per access to display memory—not as Listing 4.11 executes in 3.18 µs per access to display memory—not as
long as we had assumed, but a long time nonetheless. long as we had assumed, but a long time nonetheless.
@ -107,7 +107,7 @@ long as we had assumed, but a long time nonetheless.
mov ax,0003h mov ax,0003h
int 10h ;return to text mode int 10h ;return to text mode
For comparison, let’s see how long the same code takes when accessing For comparison, let's see how long the same code takes when accessing
normal system RAM instead of display memory. The code in Listing 4.12, normal system RAM instead of display memory. The code in Listing 4.12,
which performs a **REP MOVSW** from the code segment to the code which performs a **REP MOVSW** from the code segment to the code
segment, executes in 1.39 µs per display memory access. That means that segment, executes in 1.39 µs per display memory access. That means that
@ -136,7 +136,7 @@ cycle-eater can *more than double* the execution time of 8088 code!
; times ; times
call ZTimerOff call ZTimerOff
Bear in mind that we’re talking about a worst case here; the impact of Bear in mind that we're talking about a worst case here; the impact of
the display adapter cycle-eater is proportional to the percent of time a the display adapter cycle-eater is proportional to the percent of time a
given code sequence spends accessing display memory. given code sequence spends accessing display memory.

View file

@ -21,7 +21,7 @@ instructions per microsecond than can the 8088-based PC, it takes just
as long to access display memory on those computers as on the 8088-based as long to access display memory on those computers as on the 8088-based
PC. Remember, the limited speed of access to a graphics adapter is an PC. Remember, the limited speed of access to a graphics adapter is an
inherent characteristic of the adapter, so the fastest computer around inherent characteristic of the adapter, so the fastest computer around
can’t access display memory one iota faster than the adapter will allow. can't access display memory one iota faster than the adapter will allow.
#### What to Do about the Display Adapter Cycle-Eater? {#Heading22} #### What to Do about the Display Adapter Cycle-Eater? {#Heading22}
@ -32,7 +32,7 @@ used to mask individual pixels and clip images. Why? Because
read/modify/write operations require two display memory accesses (one read/modify/write operations require two display memory accesses (one
read and one write) each time display memory is manipulated. Instead, we 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 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 byte of display memory at once, since such writes don't require
accompanying read accesses. The key here is that only half as many accompanying read accesses. The key here is that only half as many
display memory accesses are required to write a byte to display memory 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 as are required to read a byte from display memory, mask part of it off
@ -67,7 +67,7 @@ states.)
It would be handy to explore the display adapter cycle-eater issue in It would be handy to explore the display adapter cycle-eater issue in
depth, with lots of example code and execution timings, but alas, I depth, with lots of example code and execution timings, but alas, I
don’t have the space for that right now. For the time being, all you don't have the space for that right now. For the time being, all you
really need to know about the display adapter cycle-eater is that on the really need to know about the display adapter cycle-eater is that on the
8088 you can lose more than 8 cycles of execution time on each access to 8088 you can lose more than 8 cycles of execution time on each access to
display memory. For intensive access to display memory, the loss really display memory. For intensive access to display memory, the loss really
@ -80,10 +80,10 @@ code, and that is of course to measure the performance of that code.
#### Cycle-Eaters: A Summary {#Heading23} #### Cycle-Eaters: A Summary {#Heading23}
We’ve covered a great deal of sophisticated material in this chapter, so We've covered a great deal of sophisticated material in this chapter, so
don’t feel bad if you haven’t understood everything you’ve read; it will don't feel bad if you haven't understood everything you've read; it will
all become clear from further reading, especially once you study, time, all become clear from further reading, especially once you study, time,
and tune code that you have written yourself. What’s really important is and tune code that you have written yourself. What's really important is
that you come away from this chapter understanding that on the 8088: that you come away from this chapter understanding that on the 8088:
- The 8-bit bus cycle-eater causes each access to a word-sized operand - The 8-bit bus cycle-eater causes each access to a word-sized operand
@ -100,24 +100,24 @@ that you come away from this chapter understanding that on the 8088:
This basic knowledge about cycle-eaters puts you in a good position to This basic knowledge about cycle-eaters puts you in a good position to
understand the results reported by the Zen timer, and that means that understand the results reported by the Zen timer, and that means that
you’re well on your way to writing high-performance assembler code. you're well on your way to writing high-performance assembler code.
#### What Does It All Mean? {#Heading24} #### What Does It All Mean? {#Heading24}
There you have it: life under the programming interface. It’s not a There you have it: life under the programming interface. It's not a
particularly pretty picture for the inhabitants of that strange realm particularly pretty picture for the inhabitants of that strange realm
where hardware and software meet are little-known cycle-eaters that sap where hardware and software meet are little-known cycle-eaters that sap
the speed from your unsuspecting code. Still, some of those cycle-eaters the speed from your unsuspecting code. Still, some of those cycle-eaters
can be minimized by keeping instructions short, using the registers, can be minimized by keeping instructions short, using the registers,
using byte-sized memory operands, and accessing display memory as little using byte-sized memory operands, and accessing display memory as little
as possible. None of the cycle-eaters can be eliminated, and dynamic RAM as possible. None of the cycle-eaters can be eliminated, and dynamic RAM
refresh can scarcely be addressed at all; still, aren’t you better off refresh can scarcely be addressed at all; still, aren't you better off
knowing how fast your code *really* runs—and why—than you were reading knowing how fast your code *really* runs—and why—than you were reading
the official execution times and guessing? And while specific the official execution times and guessing? And while specific
cycle-eaters vary in importance on later x86-family processors, with cycle-eaters vary in importance on later x86-family processors, with
some cycle-eaters vanishing altogether and new ones appearing, the some cycle-eaters vanishing altogether and new ones appearing, the
concept that understanding these obscure gremlins is a key to concept that understanding these obscure gremlins is a key to
performance remains unchanged, as we’ll see again and again in later performance remains unchanged, as we'll see again and again in later
chapters. chapters.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -20,17 +20,17 @@ The move took days to complete.
*Never again*. *Never again*.
You’re probably wondering two things: What does this have to do with You're probably wondering two things: What does this have to do with
high-performance programming, and why on earth didn’t I rent a truck and high-performance programming, and why on earth didn't I rent a truck and
get the move over in one or two trips, saving hours of driving? As it get the move over in one or two trips, saving hours of driving? As it
happens, the second question answers the first. I didn’t rent a truck happens, the second question answers the first. I didn't rent a truck
because it *seemed* easier and cheaper to use cars—no big truck to because it *seemed* easier and cheaper to use cars—no big truck to
drive, no rentals, spread the work out more manageably, and so on. drive, no rentals, spread the work out more manageably, and so on.
It wasn’t easier, and wasn’t even much cheaper. (It costs quite a bit to It wasn't easier, and wasn't even much cheaper. (It costs quite a bit to
drive a car 330 miles, to say nothing of the value of 15 hours of my drive a car 330 miles, to say nothing of the value of 15 hours of my
time.) But, at the time, it seemed as though my approach would be easier time.) But, at the time, it seemed as though my approach would be easier
and cheaper. In fact, I didn’t realize just how much time I had wasted and cheaper. In fact, I didn't realize just how much time I had wasted
driving back and forth until I sat down to write this chapter. driving back and forth until I sat down to write this chapter.
In Chapter 1, I briefly discussed using *restartable blocks*. This, you In Chapter 1, I briefly discussed using *restartable blocks*. This, you
@ -55,24 +55,24 @@ more effort and forethought, but would have paid off handsomely.
![](images/i.jpg) *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.* ![](images/i.jpg) *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 And with that, let's look at a fairly complex application of restartable
blocks. blocks.
#### Searching for Text {#Heading3} #### Searching for Text {#Heading3}
The application we’re going to examine searches a file for a specified The application we're going to examine searches a file for a specified
string. We’ll develop a program that will search the file specified on string. We'll develop a program that will search the file specified on
the command line for a string (also specified on the comline), then the command line for a string (also specified on the comline), then
report whether the string was found or not. (Because the searched-for report whether the string was found or not. (Because the searched-for
string is obtained via **argv**, it can’t contain any whitespace string is obtained via **argv**, it can't contain any whitespace
characters.) characters.)
This is a *very* limited subset of what search utilities such as grep This is a *very* limited subset of what search utilities such as grep
can do, and isn’t really intended to be a generally useful application; can do, and isn't really intended to be a generally useful application;
the purpose is to provide insight into restartable blocks in particular the purpose is to provide insight into restartable blocks in particular
and optimization in general in the course of developing a search engine. and optimization in general in the course of developing a search engine.
That search engine will, however, be easy to plug into any program, and That search engine will, however, be easy to plug into any program, and
there’s nothing preventing you from using it in a more fruitful context, there's nothing preventing you from using it in a more fruitful context,
like searching through a user-selectable file set. like searching through a user-selectable file set.
The first point to address in designing our program involves the The first point to address in designing our program involves the
@ -82,9 +82,9 @@ that involve reading any byte of the file more than once, because disk
access time is orders of magnitude slower than any data handling access time is orders of magnitude slower than any data handling
performed by our own code. Based on our experience in Chapter 1, we can performed by our own code. Based on our experience in Chapter 1, we can
also discard all approaches that get bytes either one at a time or in also discard all approaches that get bytes either one at a time or in
small sets from DOS. We want to read big “buffers-full” of bytes at a small sets from DOS. We want to read big "buffers-full" of bytes at a
pop from the searched file, and the bigger the buffer the better—in pop from the searched file, and the bigger the buffer the better—in
order to minimize DOS’s overhead. A good rough cut is a buffer that will order to minimize DOS's overhead. A good rough cut is a buffer that will
be between 16K and 64K, depending on the exact search approach, 64K be between 16K and 64K, depending on the exact search approach, 64K
being the maximum size because near pointers make for superior being the maximum size because near pointers make for superior
performance. performance.
@ -93,13 +93,13 @@ So we know we want to work with a large buffer, filling it as
infrequently as possible. Now we have to figure out how to search infrequently as possible. Now we have to figure out how to search
through a file by loading it into that large buffer in chunks. To through a file by loading it into that large buffer in chunks. To
accomplish this, we have to know how we want to do our searching, and accomplish this, we have to know how we want to do our searching, and
that’s not immediately obvious. Where do we begin? that's not immediately obvious. Where do we begin?
Well, it might be instructive to consider how we would search if our Well, it might be instructive to consider how we would search if our
search involved only one buffer, already resident in memory. In other search involved only one buffer, already resident in memory. In other
words, suppose we don’t have to bother with file handling at all, and words, suppose we don't have to bother with file handling at all, and
further suppose that we don’t have to deal with searching through further suppose that we don't have to deal with searching through
multiple blocks. After all, that’s a good description of the multiple blocks. After all, that's a good description of the
all-important inner loop of our searching program, where the program all-important inner loop of our searching program, where the program
will spend virtually all of its time (aside from the unavoidable disk will spend virtually all of its time (aside from the unavoidable disk
access overhead). access overhead).
@ -109,7 +109,7 @@ access overhead).
The easiest approach would be to use a C/C++ library function. The The easiest approach would be to use a C/C++ library function. The
closest match to what we need is **strstr()**, which searches one string closest match to what we need is **strstr()**, which searches one string
for the first occurrence of a second string. However, while **strstr()** for the first occurrence of a second string. However, while **strstr()**
would work, it isn’t ideal for our purposes. The problem is this: Where would work, it isn't ideal for our purposes. The problem is this: Where
we want to search a fixed-length buffer for the first occurrence of a we want to search a fixed-length buffer for the first occurrence of a
string, **strstr()** searches a *string* for the first occurrence of string, **strstr()** searches a *string* for the first occurrence of
another string. another string.

View file

@ -11,7 +11,7 @@ implementation is well-written, its performance will suffer, at least
for our application, from unnecessary overhead. for our application, from unnecessary overhead.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 {#Heading5} ### Brute-Force Techniques {#Heading5}
@ -19,12 +19,12 @@ for our application, from unnecessary overhead.
Given that no C/C++ library function meets our needs precisely, an Given that no C/C++ library function meets our needs precisely, an
obvious alternative approach is the brute-force technique that uses obvious alternative approach is the brute-force technique that uses
**memcmp()** to compare *every* potential matching location in the **memcmp()** to compare *every* potential matching location in the
buffer to the string we’re searching for, as illustrated in Figure 5.1. buffer to the string we're searching for, as illustrated in Figure 5.1.
By the way, we could, of course, use our own code, working with pointers By the way, we could, of course, use our own code, working with pointers
in a loop, to perform the comparison in place of **memcmp()**. But in a loop, to perform the comparison in place of **memcmp()**. But
**memcmp()** will almost certainly use the very fast **REPZ CMPS** **memcmp()** will almost certainly use the very fast **REPZ CMPS**
instruction. However, *never assume!* It wouldn’t hurt to use a debugger instruction. However, *never assume!* It wouldn't hurt to use a debugger
to check out the actual machine-code implementation of **memcmp()** from to check out the actual machine-code implementation of **memcmp()** from
your compiler. If necessary, you could always write your own assembly your compiler. If necessary, you could always write your own assembly
language implementation of **memcmp()**. language implementation of **memcmp()**.
@ -35,14 +35,14 @@ language implementation of **memcmp()**.
Invoking **memcmp()** for each potential match location works, but Invoking **memcmp()** for each potential match location works, but
entails considerable overhead. Each comparison requires that parameters entails considerable overhead. Each comparison requires that parameters
be pushed and that a call to and return from **memcmp()** be performed, be pushed and that a call to and return from **memcmp()** be performed,
along with a pass through the comparison loop. Surely there’s a better along with a pass through the comparison loop. Surely there's a better
way! way!
Indeed there is. We can eliminate most calls to **memcmp()** by Indeed there is. We can eliminate most calls to **memcmp()** by
performing a simple test on each potential match location that will performing a simple test on each potential match location that will
reject most such locations right off the bat. We’ll just check whether reject most such locations right off the bat. We'll just check whether
the first character of the potentially matching buffer location matches the first character of the potentially matching buffer location matches
the first character of the string we’re searching for. We could make the first character of the string we're searching for. We could make
this check by using a pointer in a loop to scan the buffer for the next this check by using a pointer in a loop to scan the buffer for the next
match for the first character, stopping to check for a match with the match for the first character, stopping to check for a match with the
rest of the string *only* when the first character matches, as shown in rest of the string *only* when the first character matches, as shown in
@ -50,65 +50,65 @@ Figure 5.2.
### Using memchr() {#Heading6} ### Using memchr() {#Heading6}
There’s yet a better way to implement this approach, however. Use the There's yet a better way to implement this approach, however. Use the
**memchr()** function, which does nothing more or less than find the **memchr()** function, which does nothing more or less than find the
next occurrence of a specified character in a fixed-length buffer next occurrence of a specified character in a fixed-length buffer
(presumably by using the extremely efficient **REPNZ SCASB** (presumably by using the extremely efficient **REPNZ SCASB**
instruction, although again it wouldn’t hurt to check). By using instruction, although again it wouldn't hurt to check). By using
**memchr()** to scan for potential matches that can then be fully tested **memchr()** to scan for potential matches that can then be fully tested
with **memcmp()**, we can build a highly efficient search engine that with **memcmp()**, we can build a highly efficient search engine that
takes good advantage of the information we have about the buffer being takes good advantage of the information we have about the buffer being
searched and the string we’re searching for. Our engine also relies searched and the string we're searching for. Our engine also relies
heavily on repeated string instructions, assuming that the **memchr()** heavily on repeated string instructions, assuming that the **memchr()**
and **memcmp()** library functions are properly coded. and **memcmp()** library functions are properly coded.
![](images/05-02.jpg)\ ![](images/05-02.jpg)\
**Figure 5.2**  *The faster string-searching technique.* **Figure 5.2**  *The faster string-searching technique.*
We’re going to go with the this approach in our file-searching program; We're going to go with the this approach in our file-searching program;
the only trick lies in deciding how to integrate this approach with the only trick lies in deciding how to integrate this approach with
restartable blocks in order to search through files larger than our restartable blocks in order to search through files larger than our
buffer. This certainly isn’t the fastest-possible searching algorithm; buffer. This certainly isn't the fastest-possible searching algorithm;
as one example, the Boyer-Moore algorithm, which cleverly eliminates as one example, the Boyer-Moore algorithm, which cleverly eliminates
many buffer locations as potential matches in the process of checking many buffer locations as potential matches in the process of checking
preceding locations, can be considerably faster. However, the preceding locations, can be considerably faster. However, the
Boyer-Moore algorithm is quite complex to understand and implement, and Boyer-Moore algorithm is quite complex to understand and implement, and
would distract us from our main focus, restartable blocks, so we’ll save would distract us from our main focus, restartable blocks, so we'll save
it for a later chapter (Chapter 14, to be precise). Besides, I suspect it for a later chapter (Chapter 14, to be precise). Besides, I suspect
you’ll find the approach we’ll use to be fast enough for most purposes. you'll find the approach we'll use to be fast enough for most purposes.
Now that we’ve selected a searching approach, let’s integrate it with Now that we've selected a searching approach, let's integrate it with
file handling and searching through multiple blocks. In other words, file handling and searching through multiple blocks. In other words,
let’s make it restartable. let's make it restartable.
#### Making a Search Restartable {#Heading7} #### Making a Search Restartable {#Heading7}
As it happens, there’s no great trick to putting the pieces of this As it happens, there's no great trick to putting the pieces of this
search program together. Basically, we’ll read in a buffer of data search program together. Basically, we'll read in a buffer of data
(we’ll work with 16K at a time to avoid signed overflow problems with (we'll work with 16K at a time to avoid signed overflow problems with
integers), search it for a match with the **memchr()/memcmp()** engine integers), search it for a match with the **memchr()/memcmp()** engine
described, and exit with a “string found” response if the desired string described, and exit with a "string found" response if the desired string
is found. is found.
Otherwise, we’ll load in another buffer full of data from the file, Otherwise, we'll load in another buffer full of data from the file,
search it, and so on. The only trick lies in handling potentially search it, and so on. The only trick lies in handling potentially
matching sequences in the file that start in one buffer and end in the matching sequences in the file that start in one buffer and end in the
next—that is, sequences that span buffers. We’ll handle this by copying next—that is, sequences that span buffers. We'll handle this by copying
the unchecked bytes at the end of one buffer to the start of the next the unchecked bytes at the end of one buffer to the start of the next
and reading that many fewer bytes the next time we fill the buffer. and reading that many fewer bytes the next time we fill the buffer.
The exact number of bytes to be copied from the end of one buffer to the The exact number of bytes to be copied from the end of one buffer to the
start of the next is the length of the searched-for string minus 1, start of the next is the length of the searched-for string minus 1,
since that’s how many bytes at the end of the buffer can’t be checked as since that's how many bytes at the end of the buffer can't be checked as
possible matches (because the check would run off the end of the possible matches (because the check would run off the end of the
buffer). buffer).
That’s really all there is to it. Listing 5.1 shows the file-searching That's really all there is to it. Listing 5.1 shows the file-searching
program. As you can see, it’s not particularly complex, although a few program. As you can see, it's not particularly complex, although a few
fairly opaque lines of code are required to handle merging the end of fairly opaque lines of code are required to handle merging the end of
one block with the start of the next. The code that searches a single one block with the start of the next. The code that searches a single
block—the function **SearchForString()—**is simple and compact (as it block—the function **SearchForString()—**is simple and compact (as it
should be, given that it’s by far the most heavily-executed code in the should be, given that it's by far the most heavily-executed code in the
listing). listing).
Listing 5.1 nicely illustrates the core concept of restartable blocks: Listing 5.1 nicely illustrates the core concept of restartable blocks:

View file

@ -15,7 +15,7 @@
#include <alloc.h> /* alloc.h for Borland compilers, #include <alloc.h> /* alloc.h for Borland compilers,
malloc.h for Microsoft compilers */ malloc.h for Microsoft compilers */
#define BLOCK_SIZE 0x4000 /* we’ll process the file in 16K blocks */ #define BLOCK_SIZE 0x4000 /* we'll process the file in 16K blocks */
/* Searches the specified number of sequences in the specified /* Searches the specified number of sequences in the specified
buffer for matches to SearchString of SearchStringLength. Note buffer for matches to SearchString of SearchStringLength. Note
@ -42,16 +42,16 @@
also matches */ also matches */
if ( SearchStringLength == 1 ) { if ( SearchStringLength == 1 ) {
return(1); /* That one matching character was the whole return(1); /* That one matching character was the whole
search string, so we’ve got a match */ search string, so we've got a match */
} }
else { else {
/* Check whether the remaining characters match */ /* Check whether the remaining characters match */
if ( !memcmp(PotentialMatch + 1, SearchString + 1, if ( !memcmp(PotentialMatch + 1, SearchString + 1,
SearchStringLength - 1) ) { SearchStringLength - 1) ) {
return(1); /* We’ve got a match */ return(1); /* We've got a match */
} }
} }
/* The string doesn’t match; keep going by pointing past the /* The string doesn't match; keep going by pointing past the
potential match location we just rejected */ potential match location we just rejected */
SearchLength -= PotentialMatch - Buffer + 1; SearchLength -= PotentialMatch - Buffer + 1;
Buffer = PotentialMatch + 1; Buffer = PotentialMatch + 1;
@ -79,13 +79,13 @@
/* Check for the proper number of arguments */ /* Check for the proper number of arguments */
if ( argc != 3 ) { if ( argc != 3 ) {
printf(“usage: search filename search-string\n”); printf("usage: search filename search-string\n");
exit(1); exit(1);
} }
/* Try to open the file to be searched */ /* Try to open the file to be searched */
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) { if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(“Can’t open file: %s\n”, argv[1]); printf("Can't open file: %s\n", argv[1]);
exit(1); exit(1);
} }
/* Calculate the length of text to search for */ /* Calculate the length of text to search for */
@ -93,7 +93,7 @@
SearchStringLength = strlen(SearchString); SearchStringLength = strlen(SearchString);
/* Try to get memory in which to buffer the data */ /* Try to get memory in which to buffer the data */
if ( (WorkingBlock = malloc(BLOCK_SIZE)) == NULL ) { if ( (WorkingBlock = malloc(BLOCK_SIZE)) == NULL ) {
printf(“Can’t get enough memory\n”); printf("Can't get enough memory\n");
exit(1); exit(1);
} }
@ -102,7 +102,7 @@
NextLoadPtr = WorkingBlock; NextLoadPtr = WorkingBlock;
NextLoadCount = BLOCK_SIZE; NextLoadCount = BLOCK_SIZE;
Done = 0; /* Not done with search yet */ Done = 0; /* Not done with search yet */
Found = 0; /* Assume we won’t find a match */ Found = 0; /* Assume we won't find a match */
/* Search the file in BLOCK_SIZE chunks */ /* Search the file in BLOCK_SIZE chunks */
do { do {
/* Read in however many bytes are needed to fill out the block /* Read in however many bytes are needed to fill out the block
@ -110,10 +110,10 @@
the rest of the bytes in the file, whichever is less */ the rest of the bytes in the file, whichever is less */
if ( (WorkingLength = read(Handle, NextLoadPtr, if ( (WorkingLength = read(Handle, NextLoadPtr,
NextLoadCount)) == -1 ) { NextLoadCount)) == -1 ) {
printf(“Error reading file %s\n”, argv[1]); printf("Error reading file %s\n", argv[1]);
exit(1); exit(1);
} }
/* If we didn’t read all the bytes we requested, we’re done /* If we didn't read all the bytes we requested, we're done
after this block, whether we find a match or not */ after this block, whether we find a match or not */
if ( WorkingLength != NextLoadCount ) { if ( WorkingLength != NextLoadCount ) {
Done = 1; Done = 1;
@ -132,7 +132,7 @@
WorkingLength - SearchStringLength + 1) <= 0 ) { WorkingLength - SearchStringLength + 1) <= 0 ) {
Done = 1; /* Too few characters in this block for Done = 1; /* Too few characters in this block for
there to be any possible matches, so this there to be any possible matches, so this
is the final block and we’re done without is the final block and we're done without
finding a match finding a match
*/ */
} }
@ -140,7 +140,7 @@
/* Search this block */ /* Search this block */
if ( SearchForString(WorkingBlock, BlockSearchLength, if ( SearchForString(WorkingBlock, BlockSearchLength,
SearchString, SearchStringLength) ) { SearchString, SearchStringLength) ) {
Found = 1; /* We’ve found a match */ Found = 1; /* We've found a match */
Done = 1; Done = 1;
} }
else { else {
@ -162,9 +162,9 @@
/* Report the results */ /* Report the results */
if ( Found ) { if ( Found ) {
printf(“String found\n”); printf("String found\n");
} else { } else {
printf(“String not found\n”); printf("String not found\n");
} }
exit(Found); /* Return the found/not found status as the exit(Found); /* Return the found/not found status as the
DOS errorlevel */ DOS errorlevel */

View file

@ -6,26 +6,26 @@
To boost the overall performance of Listing 5.1, I would normally To boost the overall performance of Listing 5.1, I would normally
convert **SearchForString()** to assembly language at this point. convert **SearchForString()** to assembly language at this point.
However, I’m not going to do that, and the reason is as important a However, I'm not going to do that, and the reason is as important a
lesson as any discussion of optimized assembly code is likely to be. lesson as any discussion of optimized assembly code is likely to be.
Take a moment to examine some interesting performance aspects of the C Take a moment to examine some interesting performance aspects of the C
implementation, and all should become much clearer. implementation, and all should become much clearer.
As you’ll recall from Chapter 1, one of the important rules for As you'll recall from Chapter 1, one of the important rules for
optimization involves knowing when optimization is worth bothering with optimization involves knowing when optimization is worth bothering with
at all. Another rule involves understanding where most of a program’s at all. Another rule involves understanding where most of a program's
execution time is going. That’s more true for Listing 5.1 than you might execution time is going. That's more true for Listing 5.1 than you might
think. think.
When Listing 5.1 is run on a 1 MB assembly source file, it takes about When Listing 5.1 is run on a 1 MB assembly source file, it takes about
three seconds to find the string “xxxend” (which is at the end of the three seconds to find the string "xxxend" (which is at the end of the
file) on a 20 MHz 386 machine, with the entire file in a disk cache. If file) on a 20 MHz 386 machine, with the entire file in a disk cache. If
**BLOCK\_SIZE** is trimmed from 16K to 4K, *execution time does not **BLOCK\_SIZE** is trimmed from 16K to 4K, *execution time does not
increase perceptibly!* At 2K, the program slows slightly; it’s not until increase perceptibly!* At 2K, the program slows slightly; it's not until
the block size shrinks to 64 bytes that execution time becomes the block size shrinks to 64 bytes that execution time becomes
approximately double that of the 16K buffer. approximately double that of the 16K buffer.
So the first thing we’ve discovered is that, while bigger blocks do make So the first thing we've discovered is that, while bigger blocks do make
for the best performance, the increment in performance may not be very for the best performance, the increment in performance may not be very
large, and might not justify the extra memory required for those larger large, and might not justify the extra memory required for those larger
blocks. Our next discovery is that, even though we read the file in blocks. Our next discovery is that, even though we read the file in
@ -35,9 +35,9 @@ spent in executing the **read()** function.
When I replaced the **read()** function call in Listing 5.1 with code When I replaced the **read()** function call in Listing 5.1 with code
that simply fools the program into thinking that a 1 MB file is being that simply fools the program into thinking that a 1 MB file is being
read, the program ran almost instantaneously—in less than 1/2 second, read, the program ran almost instantaneously—in less than 1/2 second,
even when the searched-for string wasn’t anywhere to be found. By even when the searched-for string wasn't anywhere to be found. By
contrast, Listing 5.1 requires three seconds to run even when searching contrast, Listing 5.1 requires three seconds to run even when searching
for a single character that isn’t found anywhere in the file, the case for a single character that isn't found anywhere in the file, the case
in which a single call to **memchr()** (and thus a single **REPNZ in which a single call to **memchr()** (and thus a single **REPNZ
SCASB**) can eliminate an entire block at a time. SCASB**) can eliminate an entire block at a time.
@ -48,7 +48,7 @@ good deal less than 20 percent of the total, given that the overhead of
loading the program, running through the C startup code, opening the loading the program, running through the C startup code, opening the
file, executing **printf()**, and exiting the program and returning to file, executing **printf()**, and exiting the program and returning to
the DOS shell are also included in my timings. Given which, it should be the DOS shell are also included in my timings. Given which, it should be
apparent why converting to assembly language isn’t worth the trouble—the apparent why converting to assembly language isn't worth the trouble—the
best we could do by speeding up the search is a 10 percent or so best we could do by speeding up the search is a 10 percent or so
improvement, and that would require more than doubling the performance improvement, and that would require more than doubling the performance
of code that already uses repeated string instructions to do most of the of code that already uses repeated string instructions to do most of the
@ -58,7 +58,7 @@ Not likely.
#### Knowing When Assembly Is Pointless {#Heading9} #### Knowing When Assembly Is Pointless {#Heading9}
So that’s why we’re not going to go to assembly language in this So that's why we're not going to go to assembly language in this
example—which is not to say it would never be worth converting the example—which is not to say it would never be worth converting the
search engine in Listing 5.1 to assembly. search engine in Listing 5.1 to assembly.
@ -75,7 +75,7 @@ parameters, and call **memcmp()** in order to do the same thing.
Likewise, assembly can switch back to **REPNZ SCASB** after a non-match Likewise, assembly can switch back to **REPNZ SCASB** after a non-match
much more quickly than Listing 5.1. The switching overhead is high; when much more quickly than Listing 5.1. The switching overhead is high; when
searching a file completely filled with the character z for the string searching a file completely filled with the character z for the string
“zy,” Listing 5.1 takes almost 1/2 minute, or nearly an order of "zy," Listing 5.1 takes almost 1/2 minute, or nearly an order of
magnitude longer than when searching a file filled with normal text. magnitude longer than when searching a file filled with normal text.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -12,30 +12,30 @@ searching through huge (segment-spanning) buffers.
And so we find, as we so often will, that optimization is definitely not 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 a cut-and-dried matter, and that there is no such thing as a single
“best” approach. "best" approach.
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 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 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 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 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. of repeated calls to DOS and/or the C stream I/O library.
Restartable blocks do minimize the overhead of DOS file-access calls in Restartable blocks do minimize the overhead of DOS file-access calls in
Listing 5.1; it’s just that there’s no way to reduce that overhead to Listing 5.1; it's just that there's no way to reduce that overhead to
the point where it becomes worth attempting to further improve the the point where it becomes worth attempting to further improve the
performance of our relatively efficient search engine. Although the performance of our relatively efficient search engine. Although the
search engine is by no means fully optimized, it’s nonetheless as fast search engine is by no means fully optimized, it's nonetheless as fast
as there’s any reason for it to be, given the balance of performance as there's any reason for it to be, given the balance of performance
among the components of this program. among the components of this program.
### Always Look Where Execution Is Going {#Heading10} ### Always Look Where Execution Is Going {#Heading10}
I’ve explained two important lessons: Know when it’s worth optimizing I've explained two important lessons: Know when it's worth optimizing
further, and use restartable blocks to process large data sets as a further, and use restartable blocks to process large data sets as a
series of blocks, with each block handled at high speed. The first series of blocks, with each block handled at high speed. The first
lesson is less obvious than it seems. lesson is less obvious than it seems.
@ -46,7 +46,7 @@ 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 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 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 same thing much more easily), I found that the best code in the world
wouldn’t make much difference. wouldn't make much difference.
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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.*
@ -54,17 +54,17 @@ wouldn’t make much difference.
As for restartable blocks: Here we tackled a considerably more complex As for restartable blocks: Here we tackled a considerably more complex
application of restartable blocks than we did in Chapter 1—which turned 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 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 programming tasks you tackle, such as strings that span blocks, fluster
you into settling for easy, general—and slow—solutions. Focus on making you into settling for easy, general—and slow—solutions. Focus on making
the inner loop—the code that handles each block—as efficient as the inner loop—the code that handles each block—as efficient as
possible, then structure the rest of your code to support the inner possible, then structure the rest of your code to support the inner
loop. loop.
Programming with restartable blocks isn’t easy, but when speed is an Programming with restartable blocks isn't easy, but when speed is an
issue, using restartable blocks in the right places more than pays for issue, using restartable blocks in the right places more than pays for
itself with greatly improved performance. And when speed is *not* an itself with greatly improved performance. And when speed is *not* an
issue, of course, or in code that’s not time-critical, you wouldn’t issue, of course, or in code that's not time-critical, you wouldn't
dream of wasting your time on optimization. dream of wasting your time on optimization.
Would you? Would you?

View file

@ -8,7 +8,7 @@ Chapter 6\
### How Machine Instructions May Do More Than You Think {#Heading2} ### How Machine Instructions May Do More Than You Think {#Heading2}
I first met Jeff Duntemann at an authors’ dinner hosted by *PC Tech I first met Jeff Duntemann at an authors' dinner hosted by *PC Tech
Journal* at Fall Comdex, back in 1985. Jeff was already reasonably Journal* at Fall Comdex, back in 1985. Jeff was already reasonably
well-known as a computer editor and writer, although not as famous as well-known as a computer editor and writer, although not as famous as
*Complete Turbo Pascal*, editions 1 through 672 (or thereabouts), *TURBO *Complete Turbo Pascal*, editions 1 through 672 (or thereabouts), *TURBO
@ -22,7 +22,7 @@ time, I nonetheless harbored vague ambitions of being a science-fiction
writer when I grew up. (I have since realized that this hardly puts me writer when I grew up. (I have since realized that this hardly puts me
in elite company, especially in the computer world, where it seems that in elite company, especially in the computer world, where it seems that
every other person has told me they plan to write science fiction every other person has told me they plan to write science fiction
“someday.” Given that probably fewer than 500—I’m guessing here—original "someday." Given that probably fewer than 500—I'm guessing here—original
science fiction and fantasy short stories, and perhaps a few more novels science fiction and fantasy short stories, and perhaps a few more novels
than that, are published each year in this country, I see a few mid-life than that, are published each year in this country, I see a few mid-life
crises coming.) crises coming.)
@ -31,10 +31,10 @@ At any rate, I had accumulated a small collection of rejection slips,
and fancied myself something of an old hand in the field. At the end of and fancied myself something of an old hand in the field. At the end of
the dinner, as the other writers complained half-seriously about how the dinner, as the other writers complained half-seriously about how
little they were paid for writing for *Tech Journal*, I leaned over to little they were paid for writing for *Tech Journal*, I leaned over to
Jeff and whispered, “You know, the pay isn’t so bad here. You should see Jeff and whispered, "You know, the pay isn't so bad here. You should see
what they pay for science fiction—even to the guys who win awards!” what they pay for science fiction—even to the guys who win awards!"
To which Jeff replied, “I know. I’ve been nominated for two Hugos.” To which Jeff replied, "I know. I've been nominated for two Hugos."
Oh. Oh.
@ -45,22 +45,22 @@ put on by a computer magazine, seated next to an editor who had just
finished a book about Turbo Pascal, and, gosh, it was *obvious* that the finished a book about Turbo Pascal, and, gosh, it was *obvious* that the
appropriate topic was computers. appropriate topic was computers.
For once, the moral is *not* “don’t judge a book by its cover.” Jeff is For once, the moral is *not* "don't judge a book by its cover." Jeff is
in fact what he appeared to be at face value: a computer writer and in fact what he appeared to be at face value: a computer writer and
editor. However, he is more, too; face value wasn’t full value. You’ll editor. However, he is more, too; face value wasn't full value. You'll
similarly find that face value isn’t always full value in computer similarly find that face value isn't always full value in computer
programming, and especially so when working in assembly language, where programming, and especially so when working in assembly language, where
many instructions have talents above and beyond their obvious abilities. many instructions have talents above and beyond their obvious abilities.
On the other hand, there are also a number of instructions, such as On the other hand, there are also a number of instructions, such as
**LOOP**, that are designed to perform specific functions but aren’t **LOOP**, that are designed to perform specific functions but aren't
always the best instructions for those functions. So don’t judge a book always the best instructions for those functions. So don't judge a book
by its cover, either. by its cover, either.
Assembly language for the x86 family isn’t like any other language (for Assembly language for the x86 family isn't like any other language (for
which we should, without hesitation, offer our profuse thanks). Assembly which we should, without hesitation, offer our profuse thanks). Assembly
language reflects the design of the processor rather than the way we language reflects the design of the processor rather than the way we
think, so it’s full of multiple instructions that perform similar think, so it's full of multiple instructions that perform similar
functions, instructions with odd and often confusing side effects, and functions, instructions with odd and often confusing side effects, and
endless ways to string together different instructions to do much the endless ways to string together different instructions to do much the
same things, often with seemingly minuscule differences that can turn same things, often with seemingly minuscule differences that can turn
@ -69,44 +69,44 @@ out to be surprisingly important.
To produce the best code, you must decide precisely what you need to To produce the best code, you must decide precisely what you need to
accomplish, then put together the sequence of instructions that accomplish, then put together the sequence of instructions that
accomplishes that end most efficiently, regardless of what the accomplishes that end most efficiently, regardless of what the
instructions are usually used for. That’s why optimization for the PC is instructions are usually used for. That's why optimization for the PC is
an art, and it’s why the best assembly language for the x86 family will an art, and it's why the best assembly language for the x86 family will
almost always handily outperform compiled code. With that in mind, let’s almost always handily outperform compiled code. With that in mind, let's
look past face value—and while we’re at it, I’ll toss in a few examples look past face value—and while we're at it, I'll toss in a few examples
of not judging a book by its cover. of not judging a book by its cover.
The point to all this: You must come to regard the x86 family The point to all this: You must come to regard the x86 family
instructions for what they do, not what you’re used to thinking they do. instructions for what they do, not what you're used to thinking they do.
Yes, **SHL** shifts a pattern left—but a look-up table can do the same Yes, **SHL** shifts a pattern left—but a look-up table can do the same
thing, and can often do it faster. **ADD** can indeed add two operands, thing, and can often do it faster. **ADD** can indeed add two operands,
but it can’t put the result in a third register; **LEA** can. The but it can't put the result in a third register; **LEA** can. The
instruction set is your raw material for writing high-performance code. instruction set is your raw material for writing high-performance code.
By limiting yourself to thinking only in certain well-established ways By limiting yourself to thinking only in certain well-established ways
about the various instructions, you’re putting yourself at a substantial about the various instructions, you're putting yourself at a substantial
disadvantage every time you sit down to program. disadvantage every time you sit down to program.
In short, the x86 family can do much more than you think—if you’ll use In short, the x86 family can do much more than you think—if you'll use
everything it has to offer. Give it a shot! everything it has to offer. Give it a shot!
#### Memory Addressing and Arithmetic {#Heading3} #### Memory Addressing and Arithmetic {#Heading3}
Years ago, I saw a clip on the David Letterman show in which Letterman Years ago, I saw a clip on the David Letterman show in which Letterman
walked into a store by the name of “Just Lamps” and asked, “So what do walked into a store by the name of "Just Lamps" and asked, "So what do
you sell here?” you sell here?"
“Lamps,” he was told. “Just lamps. Can’t you read?” "Lamps," he was told. "Just lamps. Can't you read?"
“Lamps,” he said. “I see. And what else?” "Lamps," he said. "I see. And what else?"
From that bit of sublime idiocy we can learn much about divining the From that bit of sublime idiocy we can learn much about divining the
full value of an instruction. To wit: full value of an instruction. To wit:
Quick, what do the x86’s memory addressing modes do? Quick, what do the x86's memory addressing modes do?
“Calculate memory addresses,” you no doubt replied. And you’re right, of "Calculate memory addresses," you no doubt replied. And you're right, of
course. But what *else* do they do? course. But what *else* do they do?
They perform arithmetic, that’s what they do, and that’s a distinctly They perform arithmetic, that's what they do, and that's a distinctly
different and often useful perspective on memory address calculations. different and often useful perspective on memory address calculations.
For example, suppose you have an array base address in BX and an index For example, suppose you have an array base address in BX and an index

View file

@ -4,10 +4,10 @@
The two approaches are functionally interchangeable but *not* equivalent The two approaches are functionally interchangeable but *not* equivalent
from a performance standpoint, and which is better depends on the from a performance standpoint, and which is better depends on the
particular context. If it’s a one-shot memory access, it’s best to let particular context. If it's a one-shot memory access, it's best to let
the processor perform the addition; it’s generally faster at doing this the processor perform the addition; it's generally faster at doing this
than a separate **ADD** instruction would be. If it’s a memory access than a separate **ADD** instruction would be. If it's a memory access
within a loop, however, it’s advantageous on the 8088 CPU to perform the within a loop, however, it's advantageous on the 8088 CPU to perform the
addition outside the loop, if possible, reducing effective address addition outside the loop, if possible, reducing effective address
calculation time inside the loop, as in the following: calculation time inside the loop, as in the following:
@ -24,22 +24,22 @@ no longer than **MOV AL,[BX]** on these processors because effective
address calculations generally take no extra time at all. (According to address calculations generally take no extra time at all. (According to
the MASM manual, one extra clock is required if three memory addressing the MASM manual, one extra clock is required if three memory addressing
components, as in **MOV AL,[BX+SI+1]**, are used. I have not been able components, as in **MOV AL,[BX+SI+1]**, are used. I have not been able
to confirm this from Intel publications, but then I haven’t looked all to confirm this from Intel publications, but then I haven't looked all
that hard.) If you’re optimizing for the 286 or 386, then, you can take that hard.) If you're optimizing for the 286 or 386, then, you can take
advantage of the processor’s ability to perform arithmetic as part of advantage of the processor's ability to perform arithmetic as part of
memory address calculations without taking a performance hit. memory address calculations without taking a performance hit.
The 486 is an odd case, in which the use of an index register or the use The 486 is an odd case, in which the use of an index register or the use
of a base register that’s the destination of the previous instruction of a base register that's the destination of the previous instruction
may slow things down, so it is generally but not always better to may slow things down, so it is generally but not always better to
perform the addition outside the loop on the 486. All memory addressing perform the addition outside the loop on the 486. All memory addressing
calculations are free on the Pentium, however. I’ll discuss 486 calculations are free on the Pentium, however. I'll discuss 486
performance issues in Chapters 12 and 13, and the Pentium in Chapters 19 performance issues in Chapters 12 and 13, and the Pentium in Chapters 19
through 21. through 21.
### Math via Memory Addressing {#Heading4} ### Math via Memory Addressing {#Heading4}
You’re probably not particularly wowed to hear that you can use You're probably not particularly wowed to hear that you can use
addressing modes to perform memory addressing arithmetic that would addressing modes to perform memory addressing arithmetic that would
otherwise have to be performed with separate arithmetic instructions. otherwise have to be performed with separate arithmetic instructions.
You may, however, be a tad more interested to hear that you can also use You may, however, be a tad more interested to hear that you can also use
@ -50,14 +50,14 @@ instructions, at that.
How? How?
With **LEA**, the only instruction that performs memory addressing With **LEA**, the only instruction that performs memory addressing
calculations but doesn’t actually address memory. **LEA** accepts a calculations but doesn't actually address memory. **LEA** accepts a
standard memory addressing operand, but does nothing more than store the standard memory addressing operand, but does nothing more than store the
calculated memory offset in the specified register, which may be any calculated memory offset in the specified register, which may be any
general-purpose register. The operation of **LEA** is illustrated in general-purpose register. The operation of **LEA** is illustrated in
Figure 6.1, which also shows the operation of register-to-register Figure 6.1, which also shows the operation of register-to-register
**ADD**, for comparis on. **ADD**, for comparis on.
What does that give us? Two things that **ADD** doesn’t provide: the What does that give us? Two things that **ADD** doesn't provide: the
ability to perform addition with either two or three operands, and the ability to perform addition with either two or three operands, and the
ability to store the result in *any* register, not just in one of the ability to store the result in *any* register, not just in one of the
source operands. source operands.
@ -70,7 +70,7 @@ the result in AX. The obvious solution is this:
add ax,2 add ax,2
(It would be more compact to increment AX twice than to add two to it, (It would be more compact to increment AX twice than to add two to it,
and would probably be faster on an 8088, but that’s not what we’re after and would probably be faster on an 8088, but that's not what we're after
at the moment.) An elegant alternative solution is simply: at the moment.) An elegant alternative solution is simply:
lea ax,[bx+di+2] lea ax,[bx+di+2]
@ -85,7 +85,7 @@ or:
lea di,[si+2] lea di,[si+2]
Mind you, the only components **LEA** can add are BX or BP, SI or DI, Mind you, the only components **LEA** can add are BX or BP, SI or DI,
and a constant displacement, so it’s not going to replace **ADD** most and a constant displacement, so it's not going to replace **ADD** most
of the time. Also, **LEA** is considerably slower than **ADD** on an of the time. Also, **LEA** is considerably slower than **ADD** on an
8088, although it is just as fast as **ADD** on a 286 or 386 when fewer 8088, although it is just as fast as **ADD** on a 286 or 386 when fewer
than three memory addressing components are used. **LEA** is 1 cycle than three memory addressing components are used. **LEA** is 1 cycle
@ -98,16 +98,16 @@ and Pentium, **LEA** can also be slowed down by addressing interlocks.
#### The Wonders of LEA on the 386 {#Heading5} #### The Wonders of LEA on the 386 {#Heading5}
**LEA** really comes into its own as a “super-ADD” instruction on the **LEA** really comes into its own as a "super-ADD" instruction on the
386, 486, and Pentium, where it can take advantage of the enhanced 386, 486, and Pentium, where it can take advantage of the enhanced
memory addressing modes of those processors. (The 486 and Pentium offer memory addressing modes of those processors. (The 486 and Pentium offer
the same modes as the 386, so I’ll refer only to the 386 from now on.) the same modes as the 386, so I'll refer only to the 386 from now on.)
The 386 can do two very interesting things: It can use *any* 32-bit The 386 can do two very interesting things: It can use *any* 32-bit
register (EAX, EBX, and so on) as the memory addressing base register register (EAX, EBX, and so on) as the memory addressing base register
and/or the memory addressing index register, and it can multiply any and/or the memory addressing index register, and it can multiply any
32-bit register used as an index by two, four, or eight in the process 32-bit register used as an index by two, four, or eight in the process
of calculating a memory address, as shown in Figure 6.2. Let’s see what of calculating a memory address, as shown in Figure 6.2. Let's see what
that’s good for. that's good for.
Well, the obvious advantage is that any two 32-bit registers, or any Well, the obvious advantage is that any two 32-bit registers, or any
32-bit register and any constant, or any two 32-bit registers and any 32-bit register and any constant, or any two 32-bit registers and any
@ -122,7 +122,7 @@ destination.
But what else can **LEA** do on a 386, besides add? But what else can **LEA** do on a 386, besides add?
It can multiply any register used as an index. **LEA** can multiply only It can multiply any register used as an index. **LEA** can multiply only
by the power-of-two values 2, 4, or 8, but that’s useful more often than by the power-of-two values 2, 4, or 8, but that's useful more often than
you might imagine, especially when dealing with pointers into tables. you might imagine, especially when dealing with pointers into tables.
Besides, multiplying by 2, 4, or 8 amounts to a left shift of 1, 2, or 3 Besides, multiplying by 2, 4, or 8 amounts to a left shift of 1, 2, or 3
bits, so we can now add up to two 32-bit registers and a constant, *and* bits, so we can now add up to two 32-bit registers and a constant, *and*
@ -168,10 +168,10 @@ cycles is a pretty neat trick, even though it works only on a 386 or
486. 486.
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 help in brainstorming uses for the 386 version of **LEA** and for
pointing out the complications of 486 instruction timings. pointing out the complications of 486 instruction timings.

View file

@ -8,16 +8,16 @@ Chapter 7\
### Optimizing Halfway between Algorithms and Cycle Counting {#Heading2} ### Optimizing Halfway between Algorithms and Cycle Counting {#Heading2}
You might not think it, but there’s much to learn about performance You might not think it, but there's much to learn about performance
programming from the Great Buffalo Sauna Fiasco. To wit: programming from the Great Buffalo Sauna Fiasco. To wit:
The scene is Buffalo, New York, in the dead of winter, with the snow The scene is Buffalo, New York, in the dead of winter, with the snow
piled several feet deep. Four college students, living in typical piled several feet deep. Four college students, living in typical
student housing, are frozen to the bone. The third floor of their house, student housing, are frozen to the bone. The third floor of their house,
uninsulated and so cold that it’s uninhabitable, has an ancient uninsulated and so cold that it's uninhabitable, has an ancient
bathroom. One fabulously cold day, inspiration strikes: bathroom. One fabulously cold day, inspiration strikes:
“Hey—we could make that bathroom into a *sauna!*” "Hey—we could make that bathroom into a *sauna!*"
Pandemonium ensues. Someone rushes out and buys a gas heater, and at Pandemonium ensues. Someone rushes out and buys a gas heater, and at
considerable risk to life and limb hooks it up to an abandoned but still considerable risk to life and limb hooks it up to an abandoned but still
@ -29,7 +29,7 @@ benches along the sides of the bathroom. *Voila*—instant sauna!
They crank up the gas heater, put the bucket of rocks in front of it, They crank up the gas heater, put the bucket of rocks in front of it,
close the door, take off their clothes, and sit down to steam close the door, take off their clothes, and sit down to steam
themselves. Mind you, it’s not yet 50 degrees Fahrenheit in this room, themselves. Mind you, it's not yet 50 degrees Fahrenheit in this room,
but the gas heater is roaring. Surely warmer times await. but the gas heater is roaring. Surely warmer times await.
Indeed they do. The temperature climbs to 55 degrees, then 60, then 63, Indeed they do. The temperature climbs to 55 degrees, then 60, then 63,
@ -66,51 +66,51 @@ program is worth bothering with only in the context of a good design.
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
So, drawing fortitude from the knowledge that our quest is a pure and So, drawing fortitude from the knowledge that our quest is a pure and
worthy one, let’s resume our exploration of assembly language worthy one, let's resume our exploration of assembly language
instructions with hidden talents and instructions with well-known instructions with hidden talents and instructions with well-known
talents that are less than they appear to be. In the process, we’ll come 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 to see that there is another, very important optimization level between
the algorithm/design level and the cycle-counting/individual instruction the algorithm/design level and the cycle-counting/individual instruction
level. I’ll call this middle level *local optimization;* it involves level. I'll call this middle level *local optimization;* it involves
focusing on optimizing sequences of instructions rather than individual focusing on optimizing sequences of instructions rather than individual
instructions, all with an eye to implementing designs as efficiently as instructions, all with an eye to implementing designs as efficiently as
possible given the capabilities of the x86 family instruction set. 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 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! there? Let me put it this way: If I were, I'd never admit it!
#### When LOOP Is a Bad Idea {#Heading3} #### When LOOP Is a Bad Idea {#Heading3}
Let’s examine first an instruction that is less than it appears to be: 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 **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 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 to the task of counting down loops that any experienced x86 programmer
instinctively stuffs the loop count in CX and reaches for **LOOP** when 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 setting up a loop. That's fine—**LOOP** does, of course, work as
advertised—but there is one problem: advertised—but there is one problem:
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.)* ![](images/i.jpg) *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, 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** **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 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 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 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 byte on even an uncached 386 generally isn't significant. (Remember that
the 386 fetches four instruction bytes at a pop.) **LOOP** is three the 386 fetches four instruction bytes at a pop.) **LOOP** is three
cycles slower than **DEC/JNZ** on the 486, and the 486 executes cycles slower than **DEC/JNZ** on the 486, and the 486 executes
instructions in so few cycles that those three cycles mean that instructions in so few cycles that those three cycles mean that
**DEC/JNZ** is nearly *twice* as fast as **LOOP**. Then, too, unlike **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** **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 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 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 the relative performances of **LOOP** and **DEC CX/JNZ** on a cached
386, and LOOP really is slower.) 386, and LOOP really is slower.)
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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!* ![](images/i.jpg) *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!*
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -2,19 +2,19 @@
[Previous](07-01.html) [Table of Contents](index.html) [Next](07-03.html) [Previous](07-01.html) [Table of Contents](index.html) [Next](07-03.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
By the way, don’t fall victim to the lures of **JCXZ** and do something By the way, don't fall victim to the lures of **JCXZ** and do something
like this: like this:
and cx,ofh ;Isolate the desired field and cx,ofh ;Isolate the desired field
jcxz SkipLoop ;If field is 0, don’t bother jcxz SkipLoop ;If field is 0, don't bother
The **AND** instruction has already set the Zero flag, so this The **AND** instruction has already set the Zero flag, so this
and cx,0fh ;Isolate the desired field and cx,0fh ;Isolate the desired field
jz SkipLoop ;If field is 0, don’t bother jz SkipLoop ;If field is 0, don't bother
will do just fine and is faster on all processors. Use **JCXZ** only will do just fine and is faster on all processors. Use **JCXZ** only
when the Zero flag isn’t already set to reflect the status of CX. when the Zero flag isn't already set to reflect the status of CX.
### The Lessons of LOOP and JCXZ {#Heading4} ### The Lessons of LOOP and JCXZ {#Heading4}
@ -24,17 +24,17 @@ faster than several instructions that together do the same thing.
Second, that the relative merits of instructions and optimization rules Second, that the relative merits of instructions and optimization rules
vary to a surprisingly large degree across the x86 family. vary to a surprisingly large degree across the x86 family.
In particular, if you’re going to write 386 protected mode code, which In particular, if you're going to write 386 protected mode code, which
will run only on the 386, 486, and Pentium, you’d be well advised to will run only on the 386, 486, and Pentium, you'd be well advised to
rethink your use of the more esoteric members of the x86 instruction rethink your use of the more esoteric members of the x86 instruction
set. **LOOP, JCXZ,** the various accumulator-specific instructions, and set. **LOOP, JCXZ,** the various accumulator-specific instructions, and
even the string instructions in many circumstances no longer offer the even the string instructions in many circumstances no longer offer the
advantages they did on the 8088. Sometimes they’re just not any faster advantages they did on the 8088. Sometimes they're just not any faster
than more general instructions, so they’re not worth going out of your than more general instructions, so they're not worth going out of your
way to use; sometimes, as with **LOOP,** they’re actually slower, and way to use; sometimes, as with **LOOP,** they're actually slower, and
you’d do well to avoid them altogether in the 386/486 world. Reviewing you'd do well to avoid them altogether in the 386/486 world. Reviewing
the instruction cycle times in the MASM or TASM manuals, or looking over the instruction cycle times in the MASM or TASM manuals, or looking over
the cycle times in Intel’s literature, is a good place to start; the cycle times in Intel's literature, is a good place to start;
published cycle times are closer to actual execution times on the 386 published cycle times are closer to actual execution times on the 386
and 486 than on the 8088, and are reasonably reliable indicators of the and 486 than on the 8088, and are reasonably reliable indicators of the
relative performance levels of x86 instructions. relative performance levels of x86 instructions.
@ -43,12 +43,12 @@ relative performance levels of x86 instructions.
Cycle counting and directly substituting instructions (**DEC CX/JNZ** Cycle counting and directly substituting instructions (**DEC CX/JNZ**
for **LOOP,** for example) are techniques that belong at the lowest for **LOOP,** for example) are techniques that belong at the lowest
level of optimization. It’s an important level, but it’s fairly level of optimization. It's an important level, but it's fairly
mechanical; once you’ve learned the capabilities and relative mechanical; once you've learned the capabilities and relative
performance levels of the various instructions, you should be able to performance levels of the various instructions, you should be able to
select the best instructions fairly easily. What’s more, this is a task select the best instructions fairly easily. What's more, this is a task
at which compilers excel. What I’m saying is that you shouldn’t get too at which compilers excel. What I'm saying is that you shouldn't get too
caught up in counting cycles because that’s a small (albeit important) caught up in counting cycles because that's a small (albeit important)
part of the optimization picture, and not the area in which your part of the optimization picture, and not the area in which your
greatest advantage lies. greatest advantage lies.
@ -59,24 +59,24 @@ that of *local optimization;* that is, selecting the best *sequence* of
instructions for a task. The key to local optimization is viewing the instructions for a task. The key to local optimization is viewing the
80x86 instruction set as a set of building blocks, each with unique 80x86 instruction set as a set of building blocks, each with unique
characteristics. Your job is to sequence those blocks so that they characteristics. Your job is to sequence those blocks so that they
perform well. It doesn’t matter what the instructions are intended to do 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.* or what their names are; all that matters is what they *do.*
Our discussion of **LOOP** versus **DEC/JNZ** is an excellent example of Our discussion of **LOOP** versus **DEC/JNZ** is an excellent example of
optimization by cycle counting. It’s worth knowing, but once you’ve 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 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 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 few cycles each time, and while that helps a little, it's not going to
make all *that* much difference. make all *that* much difference.
Now let’s step back for a moment, and with no preconceptions consider 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 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 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 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 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 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 little faster by selecting different instructions, but they can get only
so fast, and branching can’t even get all that fast. so fast, and branching can't even get all that fast.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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.*
@ -87,18 +87,18 @@ byte is found, a zero byte is found, or the specified number of
characters have been checked. Such a function would be useful for characters have been checked. Such a function would be useful for
scanning up to a maximum number of characters in a zero-terminated 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 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 search of the sample string for a period (‘.') in 170 µs on a 20 MHz
cached 386. cached 386.
When the **LOOP** in Listing 7.1 is replaced with **DEC CX/JNZ,** 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 performance improves to 168 µs, less than 2 percent faster than Listing
7.1. Actually, instruction fetching, instruction alignment, cache 7.1. Actually, instruction fetching, instruction alignment, cache
characteristics, or something similar is affecting these results; I’d characteristics, or something similar is affecting these results; I'd
expect a slightly larger improvement—around 7 percent—but that’s the expect a slightly larger improvement—around 7 percent—but that's the
most that counting cycles could buy us in this case. (All right, 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 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 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 near the benefits we're about to see from local optimization, and that's
the whole point.) the whole point.)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -14,21 +14,21 @@
.data .data
; Sample string to search through. ; Sample string to search through.
SampleString labelbyte SampleString labelbyte
db ‘This is a sample string of a long enough length ’ db ‘This is a sample string of a long enough length '
db ‘so that raw searching speed can outweigh any ’ db ‘so that raw searching speed can outweigh any '
db ‘extra set-up time that may be required.’,0 db ‘extra set-up time that may be required.',0
SAMPLE_STRING_LENGTH equ $-SampleString SAMPLE_STRING_LENGTH equ $-SampleString
; User prompt. ; User prompt.
Prompt db ‘Enter character to search for:$’ Prompt db ‘Enter character to search for:$'
; Result status messages. ; Result status messages.
ByteFoundMsg db 0dh,0ah ByteFoundMsg db 0dh,0ah
db ‘Specified byte found.’,0dh,0ah,‘$’ db ‘Specified byte found.',0dh,0ah,‘$'
ZeroByteFoundMsg db 0dh, 0ah ZeroByteFoundMsg db 0dh, 0ah
db ‘Zero byte encountered.’,0dh,0ah,‘$’ db ‘Zero byte encountered.',0dh,0ah,‘$'
NoByteFoundMsg db 0dh,0ah NoByteFoundMsg db 0dh,0ah
db ‘Buffer exhausted with no match.’, 0dh, 0ah, ‘$’ db ‘Buffer exhausted with no match.', 0dh, 0ah, ‘$'
.code .code
Startprocnear Startprocnear
@ -45,12 +45,12 @@
call SearchMaxLength ;search the buffer call SearchMaxLength ;search the buffer
mov dx,offset ByteFoundMsg ;assume we found the byte mov dx,offset ByteFoundMsg ;assume we found the byte
jc PrintStatus ;we did find the byte jc PrintStatus ;we did find the byte
;we didn’t find the byte, figure out ;we didn't find the byte, figure out
;whether we found a zero byte or ;whether we found a zero byte or
;ran out of buffer ;ran out of buffer
mov dx,offset NoByteFoundMsg mov dx,offset NoByteFoundMsg
;assume we didn’t find a zero byte ;assume we didn't find a zero byte
jcxz PrintStatus ;we didn’t find a zero byte jcxz PrintStatus ;we didn't find a zero byte
mov dx,offset ZeroByteFoundMsg ;we found a zero byte mov dx,offset ZeroByteFoundMsg ;we found a zero byte
PrintStatus: PrintStatus:
mov ah,9 ;DOS print string function mov ah,9 ;DOS print string function
@ -79,18 +79,18 @@
SearchMaxLengthLoop: SearchMaxLengthLoop:
lodsb ;get the next byte lodsb ;get the next byte
cmp al,ah ;is this the byte we want? cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we’re done with success jz ByteFound ;yes, we're done with success
and al,al ;is this the terminating 0 byte? and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we’re done with failure jz ByteNotFound ;yes, we're done with failure
loop SearchMaxLengthLoop ;it’s neither, so check the next loop SearchMaxLengthLoop ;it's neither, so check the next
;byte, if any ;byte, if any
ByteNotFound: ByteNotFound:
clc ;return “not found” status clc ;return "not found" status
ret ret
ByteFound: ByteFound:
dec si ;point back to the location at which dec si ;point back to the location at which
;we found the searched-for byte ;we found the searched-for byte
stc ;return “found” status stc ;return "found" status
ret ret
SearchMaxLengthendp SearchMaxLengthendp
end Start end Start
@ -102,7 +102,7 @@ bytes are checked for each **LOOP** performed. The same instructions are
used inside the loop in each listing, but Listing 7.2 is arranged so used inside the loop in each listing, but Listing 7.2 is arranged so
that three-quarters of the **LOOP**s are eliminated. Listings 7.1 and that three-quarters of the **LOOP**s are eliminated. Listings 7.1 and
7.2 perform exactly the same task, and they use the same instructions in 7.2 perform exactly the same task, and they use the same instructions in
the loop—the searching algorithm hasn’t changed in any way—but we have the loop—the searching algorithm hasn't changed in any way—but we have
sequenced the instructions differently in Listing 7.2, and that makes sequenced the instructions differently in Listing 7.2, and that makes
all the difference. all the difference.

View file

@ -13,21 +13,21 @@
.data .data
; Sample string to search through. ; Sample string to search through.
SampleStringlabelbyte SampleStringlabelbyte
db ‘This is a sample string of a long enough length ’ db ‘This is a sample string of a long enough length '
db ‘so that raw searching speed can outweigh any ’ db ‘so that raw searching speed can outweigh any '
db ‘extra set-up time that may be required.’,0 db ‘extra set-up time that may be required.',0
SAMPLE_STRING_LENGTH equ $-SampleString SAMPLE_STRING_LENGTH equ $-SampleString
; User prompt. ; User prompt.
Prompt db ‘Enter character to search for:$’ Prompt db ‘Enter character to search for:$'
; Result status messages. ; Result status messages.
ByteFoundMsg db 0dh,0ah ByteFoundMsg db 0dh,0ah
db ‘Specified byte found.’,0dh,0ah,‘$’ db ‘Specified byte found.',0dh,0ah,‘$'
ZeroByteFoundMsg db 0dh,0ah ZeroByteFoundMsg db 0dh,0ah
db ‘Zero byte encountered.’, 0dh, 0ah, ‘$’ db ‘Zero byte encountered.', 0dh, 0ah, ‘$'
NoByteFoundMsg db 0dh,0ah NoByteFoundMsg db 0dh,0ah
db ‘Buffer exhausted with no match.’, 0dh, 0ah, ‘$’ db ‘Buffer exhausted with no match.', 0dh, 0ah, ‘$'
; Table of initial, possibly partial loop entry points for ; Table of initial, possibly partial loop entry points for
; SearchMaxLength. ; SearchMaxLength.
@ -52,12 +52,12 @@
call SearchMaxLength ;search the buffer call SearchMaxLength ;search the buffer
mov dx,offset ByteFoundMsg ;assume we found the byte mov dx,offset ByteFoundMsg ;assume we found the byte
jc PrintStatus ;we did find the byte jc PrintStatus ;we did find the byte
;we didn’t find the byte, figure out ;we didn't find the byte, figure out
;whether we found a zero byte or ;whether we found a zero byte or
;ran out of buffer ;ran out of buffer
mov dx,offset NoByteFoundMsg mov dx,offset NoByteFoundMsg
;assume we didn’t find a zero byte ;assume we didn't find a zero byte
jcxz PrintStatus ;we didn’t find a zero byte jcxz PrintStatus ;we didn't find a zero byte
mov dx,offset ZeroByteFoundMsg ;we found a zero byte mov dx,offset ZeroByteFoundMsg ;we found a zero byte
PrintStatus: PrintStatus:
mov ah,9 ;DOS print string function mov ah,9 ;DOS print string function
@ -99,36 +99,36 @@
SearchMaxLengthEntry4: SearchMaxLengthEntry4:
lodsb ;get the next byte lodsb ;get the next byte
cmp al,ah ;is this the byte we want? cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we’re done with success jz ByteFound ;yes, we're done with success
and al,al ;is this the terminating 0 byte? and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we’re done with failure jz ByteNotFound ;yes, we're done with failure
SearchMaxLengthEntry3: SearchMaxLengthEntry3:
lodsb ;get the next byte lodsb ;get the next byte
cmp al,ah ;is this the byte we want? cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we’re done with success jz ByteFound ;yes, we're done with success
and al,al ;is this the terminating 0 byte? and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we’re done with failure jz ByteNotFound ;yes, we're done with failure
SearchMaxLengthEntry2: SearchMaxLengthEntry2:
lodsb ;get the next byte lodsb ;get the next byte
cmp al,ah ;is this the byte we want? cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we’re done with success jz ByteFound ;yes, we're done with success
and al,al ;is this the terminating 0 byte? and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we’re done with failure jz ByteNotFound ;yes, we're done with failure
SearchMaxLengthEntry1: SearchMaxLengthEntry1:
lodsb ;get the next byte lodsb ;get the next byte
cmp al,ah ;is this the byte we want? cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we’re done with success jz ByteFound ;yes, we're done with success
and al,al ;is this the terminating 0 byte? and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we’re done with failure jz ByteNotFound ;yes, we're done with failure
loop SearchMaxLengthLoop ;it’s neither, so check the next loop SearchMaxLengthLoop ;it's neither, so check the next
; four bytes, if any ; four bytes, if any
ByteNotFound: ByteNotFound:
clc ;return “not found” status clc ;return "not found" status
ret ret
ByteFound: ByteFound:
dec si ;point back to the location at which dec si ;point back to the location at which
; we found the searched-for byte ; we found the searched-for byte
stc ;return “found” status stc ;return "found" status
ret ret
SearchMaxLengthendp SearchMaxLengthendp
end Start end Start
@ -136,11 +136,11 @@
How much difference? Listing 7.2 runs in 121 µs—40 percent faster than 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 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; **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 it's just a question of how much more memory you want to trade for
ever-decreasing performance benefits.) That’s typical of local ever-decreasing performance benefits.) That's typical of local
optimization; it won’t often yield the order-of-magnitude improvements optimization; it won't often yield the order-of-magnitude improvements
that algorithmic improvements can produce, but it can get you a critical that algorithmic improvements can produce, but it can get you a critical
50 percent or 100 percent improvement when you’ve exhausted all other 50 percent or 100 percent improvement when you've exhausted all other
avenues. avenues.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

View file

@ -5,7 +5,7 @@
#### Rotating and Shifting with Tables {#Heading8} #### Rotating and Shifting with Tables {#Heading8}
As another example of local optimization, consider the matter of As another example of local optimization, consider the matter of
rotating or shifting a mask into position. First, let’s look at the rotating or shifting a mask into position. First, let's look at the
simple task of setting bit N of AX to 1. simple task of setting bit N of AX to 1.
The obvious way to do this is to place N in CL, rotate the bit into The obvious way to do this is to place N in CL, rotate the bit into
@ -18,7 +18,7 @@ position, and OR it with AX, as follows:
This solution is obvious because it takes good advantage of the special This solution is obvious because it takes good advantage of the special
ability of the x86 family to shift or rotate by the variable number of ability of the x86 family to shift or rotate by the variable number of
bits specified by CL. However, it takes an average of about 45 cycles on bits specified by CL. However, it takes an average of about 45 cycles on
an 8088. It’s actually far faster to precalculate the results, pass the an 8088. It's actually far faster to precalculate the results, pass the
bit number in BX, and look the shifted bit up, as shown in Listing 7.3. bit number in BX, and look the shifted bit up, as shown in Listing 7.3.
**LISTING 7.3 L7-3.ASM** **LISTING 7.3 L7-3.ASM**
@ -40,7 +40,7 @@ instructions, but by selecting the fastest *sequence* of instructions.
In the particular example above, we once again run into the difficulty In the particular example above, we once again run into the difficulty
of optimizing across the x86 family. The table lookup is faster on the of optimizing across the x86 family. The table lookup is faster on the
8088 and 286, but it’s slightly slower on the 386 and no faster on the 8088 and 286, but it's slightly slower on the 386 and no faster on the
486. However, 386/486-specific code could use enhanced addressing to 486. However, 386/486-specific code could use enhanced addressing to
accomplish the whole job in just one instruction, along the lines of the accomplish the whole job in just one instruction, along the lines of the
code snippet in Listing 7.4. code snippet in Listing 7.4.
@ -63,34 +63,34 @@ code snippet in Listing 7.4.
#### NOT Flips Bits—Not Flags {#Heading9} #### NOT Flips Bits—Not Flags {#Heading9}
The **NOT** instruction flips all the bits in the operand, from 0 to 1 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 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 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 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, by my unconscious assumption that **NOT** does set the flags. After all,
every other arithmetic and logical instruction sets the flags; why not every other arithmetic and logical instruction sets the flags; why not
**NOT**? Probably because **NOT** isn’t considered to be an arithmetic **NOT**? Probably because **NOT** isn't considered to be an arithmetic
or logical instruction at all; rather, it’s a data manipulation or logical instruction at all; rather, it's a data manipulation
instruction, like **MOV** and the various rotates. (These are **RCR, instruction, like **MOV** and the various rotates. (These are **RCR,
RCL, ROR,** and **ROL,** which affect only the Carry and Overflow RCL, ROR,** and **ROL,** which affect only the Carry and Overflow
flags.) NOT is often used for tasks, such as flipping masks, where 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 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. it can be handy to keep the flags unmodified for later testing.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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. The x86 instruction set offers many ways to accomplish almost any task.
Understanding the subtle distinctions between the instructions—whether Understanding the subtle distinctions between the instructions—whether
and which flags are set, for example—can be critical when you’re trying 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 to optimize a code sequence and you're running out of registers, or when
you’re trying to minimize branching. you're trying to minimize branching.
#### Incrementing with and without Carry {#Heading10} #### Incrementing with and without Carry {#Heading10}
Another case in which there are two slightly different ways to perform a Another case in which there are two slightly different ways to perform a
task involves adding 1 to an operand. You can do this with **INC,** as task involves adding 1 to an operand. You can do this with **INC,** as
in **INC AX,** or you can do it with **ADD,** as in **ADD AX,1.** What’s in **INC AX,** or you can do it with **ADD,** as in **ADD AX,1.** What's
the difference? The obvious difference is that **INC** is usually a byte the difference? The obvious difference is that **INC** is usually a byte
or two shorter (the exception being **ADD AL,1,** which at two bytes is or two shorter (the exception being **ADD AL,1,** which at two bytes is
the same length as **INC AL**), and is faster on some processors. Less the same length as **INC AL**), and is faster on some processors. Less
@ -130,15 +130,15 @@ additions, with code along the lines shown in Listing 7.6.
SAHF ;restore the carry flag SAHF ;restore the carry flag
LOOP LOOP_TOP LOOP LOOP_TOP
It’s not that the Listing 7.6 approach is necessarily better or worse; 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 that depends on the processor and the situation. The Listing 7.6
approach is *different,* and if you understand the differences, you’ll approach is *different,* and if you understand the differences, you'll
be able to choose the best approach for whatever code you happen to 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 write. (**DEC** has the same property of preserving the Carry flag, by
the way.) the way.)
There are a couple of interesting aspects to the last example. First, 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 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 Carry flag to remain unchanged from one addition to the next. Not
altering the arithmetic flags is a common characteristic of program altering the arithmetic flags is a common characteristic of program
control instructions (as opposed to arithmetic and logical instructions control instructions (as opposed to arithmetic and logical instructions
@ -149,12 +149,12 @@ like **SUB** and **AND,** which do alter the flags).
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Not only do **LOOP** and **JCXZ** not alter the flags, but **REP MOVS**, 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. which counts down CX to 0, doesn't affect the flags either.
The other interesting point about the last example is the use of The other interesting point about the last example is the use of
**LAHF** and **SAHF,** which transfer the low byte of the FLAGS register **LAHF** and **SAHF,** which transfer the low byte of the FLAGS register
to and from AH, respectively. These instructions were created to help to and from AH, respectively. These instructions were created to help
provide compatibility with the 8080’s (that’s *8080*, not *8088*) provide compatibility with the 8080's (that's *8080*, not *8088*)
**PUSH** **PSW** and **POP PSW** instructions, but turn out to be **PUSH** **PSW** and **POP PSW** instructions, but turn out to be
compact (one byte) instructions for saving and restoring the arithmetic compact (one byte) instructions for saving and restoring the arithmetic
flags. A word of caution, however: **SAHF** restores the Carry, Zero, flags. A word of caution, however: **SAHF** restores the Carry, Zero,
@ -163,13 +163,13 @@ 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 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. an 8088 but are relatively slow instructions on the 486 and Pentium.
There are times when it’s a clear liability that **INC** doesn’t set the There are times when it's a clear liability that **INC** doesn't set the
Carry flag. For instance Carry flag. For instance
INC AX INC AX
ADC DX,0 ADC DX,0
does *not* increment the 32-bit value in DX:AX. To do that, you’d need does *not* increment the 32-bit value in DX:AX. To do that, you'd need
the following: the following:
ADD AX,1 ADD AX,1

View file

@ -6,41 +6,41 @@ Chapter 8\
Speeding Up C with Assembly Language {#Heading1} Speeding Up C with Assembly Language {#Heading1}
------------------------------------- -------------------------------------
### Jumping Languages When You Know It’ll Help {#Heading2} ### Jumping Languages When You Know It'll Help {#Heading2}
When I was a senior in high school, a pop song called “Seasons in the When I was a senior in high school, a pop song called "Seasons in the
Sun,” sung by one Terry Jacks, soared up the pop charts and spent, as Sun," sung by one Terry Jacks, soared up the pop charts and spent, as
best I can recall, two straight weeks atop *Kasey Kasem’s American Top best I can recall, two straight weeks atop *Kasey Kasem's American Top
40.* “Seasons in the Sun” wasn’t a particularly good song, primarily 40.* "Seasons in the Sun" wasn't a particularly good song, primarily
because the lyrics were silly. I’ve never understood why the song was a because the lyrics were silly. I've never understood why the song was a
hit, but, as so often happens with undistinguished but popular music by hit, but, as so often happens with undistinguished but popular music by
forgotten one- or two-shot groups (“Don’t Pull Your Love Out on Me forgotten one- or two-shot groups ("Don't Pull Your Love Out on Me
Baby,” “Billy Don’t Be a Hero,” *et al.*), I heard it everywhere for a Baby," "Billy Don't Be a Hero," *et al.*), I heard it everywhere for a
month or so, then gave it not another thought for 15 years. month or so, then gave it not another thought for 15 years.
Recently, though, I came across a review of a Rhino Records collection Recently, though, I came across a review of a Rhino Records collection
of obscure 1970s pop hits. Knowing that Jeff Duntemann is an aficionado of obscure 1970s pop hits. Knowing that Jeff Duntemann is an aficionado
of such esoterica (who do *you* know who owns an album by The Peppermint of such esoterica (who do *you* know who owns an album by The Peppermint
Trolley Company?), I sent the review to him. He was amused by it and, as Trolley Company?), I sent the review to him. He was amused by it and, as
we kicked the names of old songs around, “Seasons in the Sun” came up. I we kicked the names of old songs around, "Seasons in the Sun" came up. I
expressed my wonderment that a song that really wasn’t very good was expressed my wonderment that a song that really wasn't very good was
such a big hit. such a big hit.
“Well,” said Jeff, “I think it suffered in the translation from the "Well," said Jeff, "I think it suffered in the translation from the
French.” French."
Ah-ha! Mystery solved. Apparently everyone but me knew that it was Ah-ha! Mystery solved. Apparently everyone but me knew that it was
translated from French, and that novelty undoubtedly made the song a big translated from French, and that novelty undoubtedly made the song a big
hit. The translation was also surely responsible for the sappy lyrics; hit. The translation was also surely responsible for the sappy lyrics;
dollars to donuts that the original French lyrics were stronger. dollars to donuts that the original French lyrics were stronger.
Which brings us without missing a beat to this chapter’s theme, speeding 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 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) converting selected parts of it (generally no more than a few functions)
to assembly language, make sure you end up with high-performance to assembly language, make sure you end up with high-performance
assembly language code, not fine-tuned C code. Compilers like Microsoft 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 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 you're not likely to do much better by taking the compiler's assembly
language output and tweaking it. language output and tweaking it.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -49,29 +49,29 @@ language output and tweaking it.
Apropos of which, when was the last time you heard of Terry Jacks? Apropos of which, when was the last time you heard of Terry Jacks?
#### Billy, Don’t Be a Compiler {#Heading3} #### Billy, Don't Be a Compiler {#Heading3}
The key to optimizing C programs with assembly language is, as always, The key to optimizing C programs with assembly language is, as always,
writing good assembly language code, but with an added twist. Rule 1 writing good assembly language code, but with an added twist. Rule 1
when converting C code to assembly is this: *Don’t think like a when converting C code to assembly is this: *Don't think like a
compiler.* That’s more easily said than done, especially when the C code compiler.* That's more easily said than done, especially when the C code
you’re converting is readily available as a model and the assembly code you're converting is readily available as a model and the assembly code
that the compiler generates is available as well. Nevertheless, the that the compiler generates is available as well. Nevertheless, the
principle of not thinking like a compiler is essential, and is, in one principle of not thinking like a compiler is essential, and is, in one
form or another, the basis for all that I’ll discuss below. form or another, the basis for all that I'll discuss below.
Before I discuss Rule 1 further, let me mention rule number 0: *Only 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 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 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 small portion doesn't have any perceptible impact on performance. Unless
you’re supremely concerned with code size (an area in which you're supremely concerned with code size (an area in which
assembly-only programs can excel), I’d suggest that you write most of 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 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 your code; that's the formula that I find gives the most bang for the
buck. buck.
This is not to say that complete programs shouldn’t be *designed* with This is not to say that complete programs shouldn't be *designed* with
optimized assembly language in mind. As you’ll see shortly, orienting optimized assembly language in mind. As you'll see shortly, orienting
your data structures towards assembly language can be a salubrious 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 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 actually optimizing code and/or converting it to assembly, though, do it
@ -85,7 +85,7 @@ assembly language optimization.
![](images/i.jpg) *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.* ![](images/i.jpg) *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 {#Heading4} ### Don't Call Your Functions on Me, Baby {#Heading4}
In order to think differently from a compiler, you must understand both 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 compilers and C programmers tend to do and how that differs from
@ -96,24 +96,24 @@ output file. (The latter is done with /Fa or /Fc in Microsoft C/C++ and
-S in Borland C++.) -S in Borland C++.)
C programmers tend to modularize their code with lots of function calls. C programmers tend to modularize their code with lots of function calls.
That’s good for readable, reliable, reusable code, and it allows the That's good for readable, reliable, reusable code, and it allows the
compiler to optimize better because it can deal with fewer variables and compiler to optimize better because it can deal with fewer variables and
statements in each optimization arena—but it’s not so good when viewed statements in each optimization arena—but it's not so good when viewed
from the assembly language level. Calls and returns are slow, especially from the assembly language level. Calls and returns are slow, especially
in the large code model, and the pushes required to put parameters on in the large code model, and the pushes required to put parameters on
the stack are expensive as well. the stack are expensive as well.
What this means is that when you want to speed up a portion of a C What this means is that when you want to speed up a portion of a C
program, you should identify the entire critical portion and move *all* program, you should identify the entire critical portion and move *all*
of that critical portion into an assembly language function. You don’t of that critical portion into an assembly language function. You don't
want to move a part of the inner loop into assembly language and then want to move a part of the inner loop into assembly language and then
call it from C every time through the loop; the function call and return call it from C every time through the loop; the function call and return
overhead would be unacceptable. Carve out the critical code *en masse* overhead would be unacceptable. Carve out the critical code *en masse*
and move it into assembly, and try to avoid calls and returns even in and move it into assembly, and try to avoid calls and returns even in
your assembly code. True, in assembly you can pass parameters in your assembly code. True, in assembly you can pass parameters in
registers, but the calls and returns themselves are still slow; if the registers, but the calls and returns themselves are still slow; if the
extra cycles they take don’t affect performance, then the code they’re extra cycles they take don't affect performance, then the code they're
in probably isn’t critical, and perhaps you’ve chosen to convert too in probably isn't critical, and perhaps you've chosen to convert too
much code to assembly, eh? much code to assembly, eh?
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -7,9 +7,9 @@
C compilers work within the stack frame model, whereby variables reside C compilers work within the stack frame model, whereby variables reside
in a block of stack memory and are accessed via offsets from BP. in a block of stack memory and are accessed via offsets from BP.
Compilers may store a couple of variables in registers and may briefly Compilers may store a couple of variables in registers and may briefly
keep other variables in registers when they’re used repeatedly, but the keep other variables in registers when they're used repeatedly, but the
stack frame is the underlying architecture. It’s a nice architecture; stack frame is the underlying architecture. It's a nice architecture;
it’s flexible, convenient, easy to program, and makes for fairly compact it's flexible, convenient, easy to program, and makes for fairly compact
code. However, stack frames have a few drawbacks. They must be code. However, stack frames have a few drawbacks. They must be
constructed and destroyed, which takes both time and code. They are so constructed and destroyed, which takes both time and code. They are so
easy to use that they tend to bias the assembly language programmer in easy to use that they tend to bias the assembly language programmer in
@ -18,15 +18,15 @@ Finally, you cannot use BP as a general-purpose register if you intend
to access a stack frame, and having that seventh register available is to access a stack frame, and having that seventh register available is
sometimes useful indeed. sometimes useful indeed.
That doesn’t mean you shouldn’t use stack frames, which are useful and That doesn't mean you shouldn't use stack frames, which are useful and
often necessary. Just don’t fall victim to their undeniable charms. often necessary. Just don't fall victim to their undeniable charms.
### Torn Between Two Segments {#Heading6} ### Torn Between Two Segments {#Heading6}
C compilers are not terrific at handling segments. Some compilers can 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 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 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 confuse every compiler I've seen, causing the full segment:offset
address to be reloaded each time either pointer is used. address to be reloaded each time either pointer is used.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -41,15 +41,15 @@ if necessary, reorganize your code to minimize segment loading.
You might think that the most obvious advantage assembly language has 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 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 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 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 true that C compilers typically don't generate instructions such as
**XLAT,** rotates, or the string instructions. On the other hand, **XLAT,** rotates, or the string instructions. On the other hand,
**XLAT** and rotates are useful in a limited set of circumstances, and **XLAT** and rotates are useful in a limited set of circumstances, and
string instructions *are* used in the C library functions. In fact, C 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 library code is likely to be carefully optimized by experts, and may be
much better than equivalent code you’d produce yourself. much better than equivalent code you'd produce yourself.
Am I saying that C compilers produce better code than you do? No, I’m 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. saying that they *can,* unless you use assembly language properly.
Writing code in assembly language rather than C guarantees nothing. Writing code in assembly language rather than C guarantees nothing.
@ -59,7 +59,7 @@ Writing code in assembly language rather than C guarantees nothing.
Sure, you can probably use the registers more efficiently and take Sure, you can probably use the registers more efficiently and take
advantage of an instruction or two that the compiler missed, but the advantage of an instruction or two that the compiler missed, but the
code isn’t going to get a whole lot faster that way. code isn't going to get a whole lot faster that way.
True optimization requires rethinking your code to take advantage of True optimization requires rethinking your code to take advantage of
assembly language. A C loop that searches through an integer array for assembly language. A C loop that searches through an integer array for
@ -71,19 +71,19 @@ matches might compile
to something like Figure 8.1A. You might look at that and tweak it to to something like Figure 8.1A. You might look at that and tweak it to
the code shown in Figure 8.1B. the code shown in Figure 8.1B.
Congratulations! You’ve successfully eliminated all stack frame access, Congratulations! You've successfully eliminated all stack frame access,
you’ve used **LOOP** (although **DEC SI/JNZ** is actually faster on 386 you've used **LOOP** (although **DEC SI/JNZ** is actually faster on 386
and later machines, as I explained in the last chapter), and you’ve used and later machines, as I explained in the last chapter), and you've used
a string instruction. Unfortunately, the new code isn’t going to run a string instruction. Unfortunately, the new code isn't going to run
very much faster. Maybe 25 percent faster, maybe a little more. Big very much faster. Maybe 25 percent faster, maybe a little more. Big
deal. You’ve eliminated the trappings of the compiler—the stack frame deal. You've eliminated the trappings of the compiler—the stack frame
and the restricted register usage—but you’re still *thinking* like the and the restricted register usage—but you're still *thinking* like the
compiler. Try this: compiler. Try this:
repnz scasw repnz scasw
jz Match jz Match
It’s a simple example—but, I hope, a convincing one. Stretch your brain It's a simple example—but, I hope, a convincing one. Stretch your brain
when you optimize. when you optimize.
### Taking It to the Limit {#Heading8} ### Taking It to the Limit {#Heading8}
@ -94,16 +94,16 @@ of better assembly language code in the small section of code that most
affects overall performance. For example, consider that the data affects overall performance. For example, consider that the data
searched in the last example is stored in an array of structures, with searched in the last example is stored in an array of structures, with
each structure in the array containing other information as well. In each structure in the array containing other information as well. In
this situation, **REP SCASW** couldn’t be used because the data searched this situation, **REP SCASW** couldn't be used because the data searched
through wouldn’t be contiguous. through wouldn't be contiguous.
However, if the need for performance in searching the array is urgent 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 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 mean removing the array elements from the structures and storing them in
their own array so that **REP SCASW** *could* be used. their own array so that **REP SCASW** *could* be used.
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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. More on this shortly.
@ -113,7 +113,7 @@ code into optimized assembly language:
- Move the entire performance-critical section into a single assembly - Move the entire performance-critical section into a single assembly
language function. language function.
- Don’t use calls or stack frame accesses inside the critical code, if - Don't use calls or stack frame accesses inside the critical code, if
possible, and avoid unnecessary memory accesses of any kind. possible, and avoid unnecessary memory accesses of any kind.
- Change segments as infrequently as possible. - Change segments as infrequently as possible.
- Optimize in terms of what assembly does well, *not* in terms of - Optimize in terms of what assembly does well, *not* in terms of
@ -126,9 +126,9 @@ That said, let me show some of these precepts in action.
#### A C-to-Assembly Case Study {#Heading9} #### A C-to-Assembly Case Study {#Heading9}
Listing 8.1 is the sample C application I’m going to use to examine Listing 8.1 is the sample C application I'm going to use to examine
optimization in action. Listing 8.1 isn’t really complete—it doesn’t optimization in action. Listing 8.1 isn't really complete—it doesn't
handle the “no-matches” case well, and it assumes that the sum of all handle the "no-matches" case well, and it assumes that the sum of all
matches will fit into an **int—**but it will do just fine as an matches will fit into an **int—**but it will do just fine as an
optimization example. optimization example.

View file

@ -29,7 +29,7 @@
in this variable-sized block */ in this variable-sized block */
}; };
/* Structure that contains one element of the array we’ll search */ /* Structure that contains one element of the array we'll search */
struct DataElement { struct DataElement {
unsigned int ID; /* ID # for array entry */ unsigned int ID; /* ID # for array entry */
unsigned int Value; /* Value of array entry */ unsigned int Value; /* Value of array entry */
@ -42,8 +42,8 @@
struct DataElement *WorkingDataPointer; struct DataElement *WorkingDataPointer;
struct BlockHeader **LastBlockPointer; struct BlockHeader **LastBlockPointer;
printf(”ID # for which to find average: “); printf("ID # for which to find average: ");
scanf(”%d”,&IDToFind); scanf("%d",&IDToFind);
/* Build an array across 5 blocks, for testing */ /* Build an array across 5 blocks, for testing */
/* Anchor the linked list to BaseArrayBlockPointer */ /* Anchor the linked list to BaseArrayBlockPointer */
LastBlockPointer = &BaseArrayBlockPointer; LastBlockPointer = &BaseArrayBlockPointer;
@ -71,10 +71,10 @@
/* Remember where to set link from this block to the next */ /* Remember where to set link from this block to the next */
LastBlockPointer = &WorkingBlockPointer->NextBlock; LastBlockPointer = &WorkingBlockPointer->NextBlock;
} }
/* Set the last block’s “next block” pointer to NULL to indicate /* Set the last block's "next block" pointer to NULL to indicate
that there are no more blocks */ that there are no more blocks */
WorkingBlockPointer->NextBlock = NULL; WorkingBlockPointer->NextBlock = NULL;
printf(”Average of all elements with ID %d: %u\n”, printf("Average of all elements with ID %d: %u\n",
IDToFind, FindIDAverage(IDToFind, BaseArrayBlockPointer)); IDToFind, FindIDAverage(IDToFind, BaseArrayBlockPointer));
exit(0); exit(0);
} }
@ -115,7 +115,7 @@
} }
} }
/* Point to the next block, and continue as long as that pointer /* Point to the next block, and continue as long as that pointer
isn’t NULL */ isn't NULL */
} while ((BlockPointer = BlockPointer->NextBlock) != NULL); } while ((BlockPointer = BlockPointer->NextBlock) != NULL);
/* Calculate the average of all matches */ /* Calculate the average of all matches */
if (IDMatchCount == 0) if (IDMatchCount == 0)

View file

@ -2,17 +2,17 @@
[Previous](08-03.html) [Table of Contents](index.html) [Next](08-05.html) [Previous](08-03.html) [Table of Contents](index.html) [Next](08-05.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
It’s hard to squeeze much more performance from this code by tweaking It's hard to squeeze much more performance from this code by tweaking
it, as exemplified by Listing 8.3, a fine-tuned assembly version of it, as exemplified by Listing 8.3, a fine-tuned assembly version of
**FindIDAverage** that was produced by looking at the assembly output of **FindIDAverage** that was produced by looking at the assembly output of
MS C/C++ and tightening it. Listing 8.3 eliminates all stack frame MS C/C++ and tightening it. Listing 8.3 eliminates all stack frame
access in the inner loop, but that’s about all the tightening there is access in the inner loop, but that's about all the tightening there is
to do. The result, as shown in Table 8.1, is that Listing 8.3 runs a to do. The result, as shown in Table 8.1, is that Listing 8.3 runs a
modest 11 percent faster than Listing 8.1 on a 386. The results could modest 11 percent faster than Listing 8.1 on a 386. The results could
vary considerably, depending on the nature of the data set searched vary considerably, depending on the nature of the data set searched
through (average block size and frequency of matches). But, then, through (average block size and frequency of matches). But, then,
understanding the typical and worst case conditions is part of understanding the typical and worst case conditions is part of
optimization, isn’t it? optimization, isn't it?
**LISTING 8.3 L8-3.ASM** **LISTING 8.3 L8-3.ASM**
@ -77,14 +77,14 @@ Table 8.1 Execution Times of FindIDAverage.
* * * * * * * * * *
_FindIDAverage proc near _FindIDAverage proc near
push bp ;Save caller’s stack frame push bp ;Save caller's stack frame
mov bp,sp ;Point to our stack frame mov bp,sp ;Point to our stack frame
push di ;Preserve C register variables push di ;Preserve C register variables
push si push si
sub dx,dx ;IDMatchSum = 0 sub dx,dx ;IDMatchSum = 0
mov bx,dx ;IDMatchCount = 0 mov bx,dx ;IDMatchCount = 0
mov si,[bp+BlockPointer] ;Pointer to first block mov si,[bp+BlockPointer] ;Pointer to first block
mov ax,[bp+SearchedForID] ;ID we’re looking for mov ax,[bp+SearchedForID] ;ID we're looking for
; Search through all the linked blocks until the last block ; Search through all the linked blocks until the last block
; (marked with a NULL pointer to the next block) has been searched. ; (marked with a NULL pointer to the next block) has been searched.
BlockLoop: BlockLoop:
@ -102,7 +102,7 @@ Table 8.1 Execution Times of FindIDAverage.
NoMatch: NoMatch:
add di,DATA_ELEMENT_SIZE ;point to the next element add di,DATA_ELEMENT_SIZE ;point to the next element
loop IntraBlockLoop loop IntraBlockLoop
; Point to the next block and continue if that pointer isn’t NULL. ; Point to the next block and continue if that pointer isn't NULL.
DoNextBlock: DoNextBlock:
mov si,[si+NextBlock] ;Get pointer to the next block mov si,[si+NextBlock] ;Get pointer to the next block
and si,si ;Is it a NULL pointer? and si,si ;Is it a NULL pointer?
@ -110,12 +110,12 @@ Table 8.1 Execution Times of FindIDAverage.
; Calculate the average of all matches. ; Calculate the average of all matches.
sub ax,ax ;Assume we found no matches sub ax,ax ;Assume we found no matches
and bx,bx and bx,bx
jz Done ;We didn’t find any matches, return 0 jz Done ;We didn't find any matches, return 0
xchg ax,dx ;Prepare for division xchg ax,dx ;Prepare for division
div bx ;Return IDMatchSum / IDMatchCount div bx ;Return IDMatchSum / IDMatchCount
Done: pop si ;Restore C register variables Done: pop si ;Restore C register variables
pop di pop di
pop bp ;Restore caller’s stack frame pop bp ;Restore caller's stack frame
ret ret
_FindIDAverage ENDP _FindIDAverage ENDP
end end
@ -125,7 +125,7 @@ mix. The loop is unrolled eight times, eliminating a good deal of
branching, and **SCASW** is used instead of **CMP [DI],AX.** (Note, branching, and **SCASW** is used instead of **CMP [DI],AX.** (Note,
however, that **SCASW** is in fact slower than **CMP [DI],AX** on the however, that **SCASW** is in fact slower than **CMP [DI],AX** on the
386 and 486, and is sometimes faster on the 286 and 8088 only because 386 and 486, and is sometimes faster on the 286 and 8088 only because
it’s shorter and therefore may prefetch faster.) This advanced tweaking it's shorter and therefore may prefetch faster.) This advanced tweaking
produces a 39 percent improvement over the original C code—substantial, produces a 39 percent improvement over the original C code—substantial,
but not a tremendous return for the optimization effort invested. but not a tremendous return for the optimization effort invested.
@ -146,7 +146,7 @@ but not a tremendous return for the optimization effort invested.
.code .code
public _FindIDAverage public _FindIDAverage
_FindIDAverage proc near _FindIDAverage proc near
push bp ;Save caller’s stack frame push bp ;Save caller's stack frame
mov bp,sp ;Point to our stack frame mov bp,sp ;Point to our stack frame
push di ;Preserve C register variables push di ;Preserve C register variables
push si push si
@ -156,7 +156,7 @@ but not a tremendous return for the optimization effort invested.
sub dx,dx ;IDMatchSum = 0 sub dx,dx ;IDMatchSum = 0
mov bx,dx ;IDMatchCount = 0 mov bx,dx ;IDMatchCount = 0
mov si,[bp+BlockPointer] ;Pointer to first block mov si,[bp+BlockPointer] ;Pointer to first block
mov ax,[bp+SearchedForID] ;ID we’re looking for mov ax,[bp+SearchedForID] ;ID we're looking for
; Search through all of the linked blocks until the last block ; Search through all of the linked blocks until the last block
; (marked with a NULL pointer to the next block) has been searched. ; (marked with a NULL pointer to the next block) has been searched.
BlockLoop: BlockLoop:
@ -165,7 +165,7 @@ but not a tremendous return for the optimization effort invested.
; Search through all the DataElement entries within this block ; Search through all the DataElement entries within this block
; and accumulate data from all that match the desired ID. ; and accumulate data from all that match the desired ID.
mov cx,[si+BlockCount] ;Number of elements in this block mov cx,[si+BlockCount] ;Number of elements in this block
jcxz DoNextBlock ;Skip this block if it’s empty jcxz DoNextBlock ;Skip this block if it's empty
mov bp,cx ;***stack frame no longer available*** mov bp,cx ;***stack frame no longer available***
add cx,7 add cx,7
shr cx,1 ;Number of repetitions of the unrolled shr cx,1 ;Number of repetitions of the unrolled
@ -202,7 +202,7 @@ but not a tremendous return for the optimization effort invested.
M_IBL 2 M_IBL 2
M_IBL 1 M_IBL 1
loop IntraBlockLoop loop IntraBlockLoop
; Point to the next block and continue if that pointer isn’t NULL. ; Point to the next block and continue if that pointer isn't NULL.
DoNextBlock: DoNextBlock:
mov si,[si+NextBlock] ;Get pointer to the next block mov si,[si+NextBlock] ;Get pointer to the next block
and si,si ;Is it a NULL pointer? and si,si ;Is it a NULL pointer?
@ -210,12 +210,12 @@ but not a tremendous return for the optimization effort invested.
; Calculate the average of all matches. ; Calculate the average of all matches.
sub ax,ax ;Assume we found no matches sub ax,ax ;Assume we found no matches
and bx,bx and bx,bx
jz Done ;We didn’t find any matches, return 0 jz Done ;We didn't find any matches, return 0
xchg ax,dx ;Prepare for division xchg ax,dx ;Prepare for division
div bx ;Return IDMatchSum / IDMatchCount div bx ;Return IDMatchSum / IDMatchCount
Done: pop si ;Restore C register variables Done: pop si ;Restore C register variables
pop di pop di
pop bp ;Restore caller’s stack frame pop bp ;Restore caller's stack frame
ret ret
_FindIDAverage ENDP _FindIDAverage ENDP
end end

View file

@ -50,8 +50,8 @@ merely rearranged.
int *WorkingDataPointer; int *WorkingDataPointer;
struct BlockHeader **LastBlockPointer; struct BlockHeader **LastBlockPointer;
printf(”ID # for which to find average: “); printf("ID # for which to find average: ");
scanf(”%d”,&IDToFind); scanf("%d",&IDToFind);
/* Build an array across 5 blocks, for testing */ /* Build an array across 5 blocks, for testing */
/* Anchor the linked list to BaseArrayBlockPointer */ /* Anchor the linked list to BaseArrayBlockPointer */
@ -79,10 +79,10 @@ merely rearranged.
/* Remember where to set link from this block to the next */ /* Remember where to set link from this block to the next */
LastBlockPointer = &WorkingBlockPointer->NextBlock; LastBlockPointer = &WorkingBlockPointer->NextBlock;
} }
/* Set the last block’s “next block” pointer to NULL to indicate /* Set the last block's "next block" pointer to NULL to indicate
that there are no more blocks */ that there are no more blocks */
WorkingBlockPointer->NextBlock = NULL; WorkingBlockPointer->NextBlock = NULL;
printf(”Average of all elements with ID %d: %u\n”, printf("Average of all elements with ID %d: %u\n",
IDToFind, FindIDAverage2(IDToFind, BaseArrayBlockPointer)); IDToFind, FindIDAverage2(IDToFind, BaseArrayBlockPointer));
exit(0); exit(0);
} }
@ -105,7 +105,7 @@ merely rearranged.
.code .code
public _FindIDAverage2 public _FindIDAverage2
_FindIDAverage2 proc near _FindIDAverage2 proc near
push bp ;Save caller’s stack frame push bp ;Save caller's stack frame
mov bp,sp ;Point to our stack frame mov bp,sp ;Point to our stack frame
push di ;Preserve C register variables push di ;Preserve C register variables
push si push si
@ -113,7 +113,7 @@ merely rearranged.
mov es,di mov es,di
cld cld
mov si,[bp+BlockPointer] ;Pointer to first block mov si,[bp+BlockPointer] ;Pointer to first block
mov ax,[bp+SearchedForID] ;ID we’re looking for mov ax,[bp+SearchedForID] ;ID we're looking for
sub dx,dx ;IDMatchSum = 0 sub dx,dx ;IDMatchSum = 0
mov bp,dx ;IDMatchCount = 0 mov bp,dx ;IDMatchCount = 0
;***stack frame no longer available*** ;***stack frame no longer available***
@ -123,9 +123,9 @@ merely rearranged.
; Search through all the DataElement entries within this block ; Search through all the DataElement entries within this block
; and accumulate data from all that match the desired ID. ; and accumulate data from all that match the desired ID.
mov cx,[si+BlockCount] mov cx,[si+BlockCount]
jcxz DoNextBlock;Skip this block if there’s no data jcxz DoNextBlock;Skip this block if there's no data
; to search through ; to search through
mov bx,cx ;We’ll use BX to point to the mov bx,cx ;We'll use BX to point to the
shl bx,1 ; corresponding value entry in the shl bx,1 ; corresponding value entry in the
; case of an ID match (BX is the ; case of an ID match (BX is the
; length in bytes of the ID array) ; length in bytes of the ID array)
@ -139,7 +139,7 @@ merely rearranged.
; (SCASW has advanced DI 2 bytes) ; (SCASW has advanced DI 2 bytes)
and cx,cx ;Is there more data to search through? and cx,cx ;Is there more data to search through?
jnz IntraBlockLoop ;yes jnz IntraBlockLoop ;yes
; Point to the next block and continue if that pointer isn’t NULL. ; Point to the next block and continue if that pointer isn't NULL.
DoNextBlock: DoNextBlock:
mov si,[si+NextBlock] ;Get pointer to the next block mov si,[si+NextBlock] ;Get pointer to the next block
and si,si ;Is it a NULL pointer? and si,si ;Is it a NULL pointer?
@ -147,18 +147,18 @@ merely rearranged.
; Calculate the average of all matches. ; Calculate the average of all matches.
sub ax,ax ;Assume we found no matches sub ax,ax ;Assume we found no matches
and bp,bp and bp,bp
jz Done ;We didn’t find any matches, return 0 jz Done ;We didn't find any matches, return 0
xchg ax,dx ;Prepare for division xchg ax,dx ;Prepare for division
div bp ;Return IDMatchSum / IDMatchCount div bp ;Return IDMatchSum / IDMatchCount
Done: pop si ;Restore C register variables Done: pop si ;Restore C register variables
pop di pop di
pop bp ;Restore caller’s stack frame pop bp ;Restore caller's stack frame
ret ret
_FindIDAverage2 ENDP _FindIDAverage2 ENDP
end end
The whole point of this rearrangement is to allow us to use **REP The whole point of this rearrangement is to allow us to use **REP
SCASW** to search through each block, and that’s exactly what SCASW** to search through each block, and that's exactly what
**FindIDAverage2** in Listing 8.6 does. The result: Listing 8.6 **FindIDAverage2** in Listing 8.6 does. The result: Listing 8.6
calculates the average about *three times* as fast as the original C calculates the average about *three times* as fast as the original C
implementation and more than twice as fast as Listing 8.4, heavily implementation and more than twice as fast as Listing 8.4, heavily
@ -167,7 +167,7 @@ optimized as the latter code is.
I trust you get the picture. The sort of instruction-by-instruction I trust you get the picture. The sort of instruction-by-instruction
optimization that so many of us love to do as a kind of puzzle is fun, optimization that so many of us love to do as a kind of puzzle is fun,
but compilers can do it nearly as well as you can, and in the future but compilers can do it nearly as well as you can, and in the future
will surely do it better. What a compiler *can’t* do is tie together the will surely do it better. What a compiler *can't* do is tie together the
needs of the program specification on the high end and the processor on needs of the program specification on the high end and the processor on
the low end, resulting in critical code that runs just about as fast as the low end, resulting in critical code that runs just about as fast as
the hardware permits. The only software that can do that is located the hardware permits. The only software that can do that is located

View file

@ -19,46 +19,46 @@ mantle of the Earth and possibly all the way through to China. Many
amusing cartoons were drawn to this effect. amusing cartoons were drawn to this effect.
Unfortunately, Barry was too busy drawing cartoons, or, alternatively, Unfortunately, Barry was too busy drawing cartoons, or, alternatively,
sleeping, to actually learn any math. In the long run, that didn’t turn sleeping, to actually learn any math. In the long run, that didn't turn
out to be a handicap for Barry, who went on to become vice-president of out to be a handicap for Barry, who went on to become vice-president of
sales for a ham-packing company, where presumably he was rarely called sales for a ham-packing company, where presumably he was rarely called
upon to derive the quadratic equation. Barry’s lack of scholarship upon to derive the quadratic equation. Barry's lack of scholarship
caused some problems back then, though. On one memorable occasion, Barry caused some problems back then, though. On one memorable occasion, Barry
was half-asleep, with his eyes open but unfocused and his chin balanced was half-asleep, with his eyes open but unfocused and his chin balanced
on his hand in the classic “if I fall asleep my head will fall off my on his hand in the classic "if I fall asleep my head will fall off my
hand and I’ll wake up” posture, when Mr. Bourgeis popped a killer hand and I'll wake up" posture, when Mr. Bourgeis popped a killer
problem: problem:
“Barry, solve this for X, please.” On the blackboard lay the equation: "Barry, solve this for X, please." On the blackboard lay the equation:
X - 1 = 0 X - 1 = 0
“Minus 1,” Barry said promptly. "Minus 1," Barry said promptly.
Mr. Bourgeis shook his head mournfully. “Try again.” Barry thought hard. Mr. Bourgeis shook his head mournfully. "Try again." Barry thought hard.
He knew the fundamental rule that the answer to most mathematical He knew the fundamental rule that the answer to most mathematical
questions is either 0, 1, infinity, -1, or minus infinity (do not apply questions is either 0, 1, infinity, -1, or minus infinity (do not apply
this rule to balancing your checkbook, however); unfortunately, that this rule to balancing your checkbook, however); unfortunately, that
gave him only a 25 percent chance of guessing right. gave him only a 25 percent chance of guessing right.
“One,” I whispered surreptitiously. "One," I whispered surreptitiously.
“Zero,” Barry announced. Mr. Bourgeis shook his head even more sadly. "Zero," Barry announced. Mr. Bourgeis shook his head even more sadly.
“One,” I whispered louder. Barry looked still more thoughtful—a bad "One," I whispered louder. Barry looked still more thoughtful—a bad
sign—so I whispered “one” again, even louder. Barry looked so thoughtful sign—so I whispered "one" again, even louder. Barry looked so thoughtful
that his eyes nearly rolled up into his head, and I realized that he was that his eyes nearly rolled up into his head, and I realized that he was
just doing his best to convince Mr. Bourgeis that Barry had solved this just doing his best to convince Mr. Bourgeis that Barry had solved this
one by himself. one by himself.
As Barry neared the climax of his stirring performance and opened his As Barry neared the climax of his stirring performance and opened his
mouth to speak, Mr. Bourgeis looked at him with great concern. “Barry, mouth to speak, Mr. Bourgeis looked at him with great concern. "Barry,
can you hear me all right?” can you hear me all right?"
“Yes, sir,” Barry replied. “Why?” "Yes, sir," Barry replied. "Why?"
“Well, I could hear the answer all the way up here. Surely you could "Well, I could hear the answer all the way up here. Surely you could
hear it just one row away?” hear it just one row away?"
The class went wild. They might as well have sent us home early for all The class went wild. They might as well have sent us home early for all
we accomplished the rest of the day. we accomplished the rest of the day.
@ -66,15 +66,15 @@ we accomplished the rest of the day.
I like to think I know more about performance programming than Barry I like to think I know more about performance programming than Barry
knew about math. Nonetheless, I always welcome good ideas and comments, knew about math. Nonetheless, I always welcome good ideas and comments,
and many readers have sent me a slew of those over the years. So in this and many readers have sent me a slew of those over the years. So in this
chapter, I think I’ll return the favor by devoting a chapter to reader chapter, I think I'll return the favor by devoting a chapter to reader
feedback. feedback.
#### Another Look at LEA {#Heading3} #### Another Look at LEA {#Heading3}
Several people have pointed out that while **LEA** is great for Several people have pointed out that while **LEA** is great for
performing certain additions (see Chapter 6), it isn’t a perfect performing certain additions (see Chapter 6), it isn't a perfect
replacement for **ADD**. What’s the difference? **LEA**, an addressing replacement for **ADD**. What's the difference? **LEA**, an addressing
instruction by trade, doesn’t affect the flags, while the arithmetic instruction by trade, doesn't affect the flags, while the arithmetic
**ADD** instruction most certainly does. This is no problem when **ADD** instruction most certainly does. This is no problem when
performing additions that involve only quantities that fit in one performing additions that involve only quantities that fit in one
machine word (32 bits in 386 protected mode, 16 bits otherwise), but it machine word (32 bits in 386 protected mode, 16 bits otherwise), but it
@ -89,7 +89,7 @@ could *not* be replaced
LEA EAX,[EAX+EBX] LEA EAX,[EAX+EBX]
ADC EDX,ECX ADC EDX,ECX
because **LEA** doesn’t affect the Carry flag. because **LEA** doesn't affect the Carry flag.
The no-carry characteristic of **LEA** becomes a distinct advantage when The no-carry characteristic of **LEA** becomes a distinct advantage when
performing pointer arithmetic, however. For instance, the following code performing pointer arithmetic, however. For instance, the following code
@ -103,43 +103,43 @@ variable to another such variable:
MOV EAX,[ESI] ;get the next element of one array MOV EAX,[ESI] ;get the next element of one array
ADC [EDI],EAX ;add it to the other array, with carry ADC [EDI],EAX ;add it to the other array, with carry
LEA ESI,[ESI+4] ;advance one array’s pointer LEA ESI,[ESI+4] ;advance one array's pointer
LEA EDI,[EDI+4] ;advance the other array’s pointer LEA EDI,[EDI+4] ;advance the other array's pointer
LOOP ADDLOOP LOOP ADDLOOP
(Yes, I could use **LODSD** instead of **MOV/LEA**; I’m just (Yes, I could use **LODSD** instead of **MOV/LEA**; I'm just
illustrating a point here. Besides, **LODS** is only 1 cycle faster than illustrating a point here. Besides, **LODS** is only 1 cycle faster than
**MOV/LEA** on the 386, and is actually more than twice as slow on the **MOV/LEA** on the 386, and is actually more than twice as slow on the
486.) If we used **ADD** rather than **LEA** to advance the pointers, 486.) If we used **ADD** rather than **LEA** to advance the pointers,
the carry from one **ADC** to the next would have to be preserved with the carry from one **ADC** to the next would have to be preserved with
either **PUSHF/POPF** or **LAHF/SAHF**. (Alternatively, we could use either **PUSHF/POPF** or **LAHF/SAHF**. (Alternatively, we could use
multiple **INC**s, since **INC** doesn’t affect the Carry flag.) multiple **INC**s, since **INC** doesn't affect the Carry flag.)
In short, **LEA** is indeed different from **ADD**. Sometimes it’s In short, **LEA** is indeed different from **ADD**. Sometimes it's
better. Sometimes not; that’s the nature of the various instruction better. Sometimes not; that's the nature of the various instruction
substitutions and optimizations that will occur to you over time. substitutions and optimizations that will occur to you over time.
There’s no such thing as “best” instructions on the x86; it all depends There's no such thing as "best" instructions on the x86; it all depends
on what you’re trying to do. on what you're trying to do.
But there sure are a lot of interesting options, aren’t there? But there sure are a lot of interesting options, aren't there?
#### The Kennedy Portfolio {#Heading4} #### The Kennedy Portfolio {#Heading4}
Reader John Kennedy regularly passes along intriguing assembly Reader John Kennedy regularly passes along intriguing assembly
programming tricks, many of which I’ve never seen mentioned anywhere programming tricks, many of which I've never seen mentioned anywhere
else. John likes to optimize for size, whereas I lean more toward speed, else. John likes to optimize for size, whereas I lean more toward speed,
but many of his optimizations are good for both purposes. Here are a few but many of his optimizations are good for both purposes. Here are a few
of my favorites: of my favorites:
John’s code for setting AX to its absolute value is: John's code for setting AX to its absolute value is:
CWD CWD
XOR AX,DX XOR AX,DX
SUB AX,DX SUB AX,DX
This does nothing when bit 15 of AX is 0 (that is, if AX is positive). This does nothing when bit 15 of AX is 0 (that is, if AX is positive).
When AX is negative, the code “nots” it and adds 1, which is exactly how When AX is negative, the code "nots" it and adds 1, which is exactly how
you perform a two’s complement negate. For the case where AX is not you perform a two's complement negate. For the case where AX is not
negative, this trick usually beats the stuffing out of the standard negative, this trick usually beats the stuffing out of the standard
absolute value code: absolute value code:
@ -148,11 +148,11 @@ absolute value code:
NEG AX ;yes,negate it NEG AX ;yes,negate it
IsPositive: IsPositive:
However, John’s code is slower on a 486; as you’re no doubt coming to However, John's code is slower on a 486; as you're no doubt coming to
realize (and as I’ll explain in Chapters 12 and 13), the 486 is an realize (and as I'll explain in Chapters 12 and 13), the 486 is an
optimization world unto itself. optimization world unto itself.
Here’s how John copies a block of bytes from DS:SI to ES:DI, moving as Here's how John copies a block of bytes from DS:SI to ES:DI, moving as
much data as possible a word at a time: much data as possible a word at a time:
SHR CX,1 ;word count SHR CX,1 ;word count

View file

@ -2,15 +2,15 @@
[Previous](09-01.html) [Table of Contents](index.html) [Next](09-03.html) [Previous](09-01.html) [Table of Contents](index.html) [Next](09-03.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
However, it generally is. Sure, if the length is odd, John’s approach However, it generally is. Sure, if the length is odd, John's approach
incurs a penalty approximately equal to the **REP** startup time for incurs a penalty approximately equal to the **REP** startup time for
**MOVSB**. However, if the length is even, John’s approach doesn’t **MOVSB**. However, if the length is even, John's approach doesn't
branch, saving cycles and not emptying the prefetch queue. If copy branch, saving cycles and not emptying the prefetch queue. If copy
lengths are evenly distributed between even and odd, John’s approach is lengths are evenly distributed between even and odd, John's approach is
faster in most x86 systems. (Not on the 486, though.) faster in most x86 systems. (Not on the 486, though.)
John also points out that on the 386, multiple **LEA**s can be combined John also points out that on the 386, multiple **LEA**s can be combined
to perform multiplications that can’t be handled by a single **LEA**, to perform multiplications that can't be handled by a single **LEA**,
much as multiple shifts and adds can be used for multiplication, only much as multiple shifts and adds can be used for multiplication, only
faster. **LEA** can be used to multiply in a single instruction on the faster. **LEA** can be used to multiply in a single instruction on the
386, but only by the values 2, 3, 4, 5, 8, and 9; several **LEA**s 386, but only by the values 2, 3, 4, 5, 8, and 9; several **LEA**s
@ -34,7 +34,7 @@ Using **LEA** on the 386, the above could be reduced to
LEA EAX,[EAX*8] ;*16 LEA EAX,[EAX*8] ;*16
LEA EAX,[EAX+EAX*4] ;*80 LEA EAX,[EAX+EAX*4] ;*80
which still isn’t as fast as using a lookup table like which still isn't as fast as using a lookup table like
MOV EAX,MultiplesOf80Table[EAX*4] MOV EAX,MultiplesOf80Table[EAX*4]
@ -55,7 +55,7 @@ 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 multiplication is to use shift and add, **LEA**, or a lookup table to
hard-code a multiplication operation for a fixed multiplier, as shown 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 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 (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 now on) by arranging your operands so that the multiplier (always the
rightmost operand following **MUL** or **IMUL**) is no larger than the rightmost operand following **MUL** or **IMUL**) is no larger than the
other operand. other operand.
@ -64,7 +64,7 @@ other operand.
![](images/i.jpg) *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.* ![](images/i.jpg) *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 (There's a minimum execution time on this trick; below 3 significant
multiplier bits, no additional cycles are saved.) For example, multiplier bits, no additional cycles are saved.) For example,
multiplication of 32,767 times 1 is 12 cycles faster than multiplication multiplication of 32,767 times 1 is 12 cycles faster than multiplication
of 1 times 32,727. of 1 times 32,727.
@ -82,7 +82,7 @@ This highlights another interesting point: **MUL** and **IMUL** on the
generally still faster, are worthwhile only in truly time-critical code. generally still faster, are worthwhile only in truly time-critical code.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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, All in all, **MUL** and **IMUL** are reasonable performers on the 386,
@ -90,10 +90,10 @@ 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 arranging your code to make the smaller operand the multiplier whenever
you know which operand is smaller. you know which operand is smaller.
That doesn’t mean that your code should test and swap operands to make 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 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 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 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 will always be small and the array elements may have any value, the
scale value is the logical choice for the multiplier. scale value is the logical choice for the multiplier.
@ -105,7 +105,7 @@ quick refresher, I described searching a buffer for a text string as
follows: Scan for the first byte of the text string with **REPNZ follows: Scan for the first byte of the text string with **REPNZ
SCASB**, then use **REPZ CMPS** to check for a full match whenever SCASB**, then use **REPZ CMPS** to check for a full match whenever
**REPNZ SCASB** finds a match for the first character, as shown in **REPNZ SCASB** finds a match for the first character, as shown in
Figure 9.1. The principle is that most buffer characters won’t match the Figure 9.1. The principle is that most buffer characters won't match the
first character of any given string, so **REPNZ SCASB**, by far the first character of any given string, so **REPNZ SCASB**, by far the
fastest way to search on the PC, can be used to eliminate most potential fastest way to search on the PC, can be used to eliminate most potential
matches; each remaining potential match can then be checked in its matches; each remaining potential match can then be checked in its
@ -114,7 +114,7 @@ entirety with **REPZ CMPS**.
![](images/09-01.jpg)\ ![](images/09-01.jpg)\
**Figure 9.1**  *Simple searching method for locating a text string.* **Figure 9.1**  *Simple searching method for locating a text string.*
Rob’s revelation, which he credits without explanation to Edgar Allen Rob's revelation, which he credits without explanation to Edgar Allen
Poe (search nevermore?), was that by far the slowest part of the whole Poe (search nevermore?), was that by far the slowest part of the whole
deal is handling **REPNZ SCASB** matches, which require checking the deal is handling **REPNZ SCASB** matches, which require checking the
remainder of the string with **REPZ CMPS** and restarting **REPNZ remainder of the string with **REPZ CMPS** and restarting **REPNZ
@ -124,11 +124,11 @@ SCASB** if no match is found.
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 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 "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 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” 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 is likely to occur much less often, resulting in many fewer whole-string
checks and much faster processing. checks and much faster processing.

View file

@ -5,19 +5,19 @@
Listing 9.1 implements the scan-on-first-character approach. Listing 9.2 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 scans for whatever character the caller specifies. Listing 9.3 is a test
program used to compare the two approaches. How much difference does program used to compare the two approaches. How much difference does
Rob’s revelation make? Plenty. Even when the entire C function call to Rob's revelation make? Plenty. Even when the entire C function call to
**FindString** is timed—**strlen** calls, parameter pushing, calling, **FindString** is timed—**strlen** calls, parameter pushing, calling,
setup, and all—the version of **FindString** in Listing 9.2, which is 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 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 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 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 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 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 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 *twice* as fast as Listing 9.1—a remarkable improvement over code that
already uses **REPNZ SCASB** and **REPZ CMPS**. already uses **REPNZ SCASB** and **REPZ CMPS**.
What I like so much about Rob’s approach is that it demonstrates that What I like so much about Rob's approach is that it demonstrates that
optimization involves much more than instruction selection and cycle optimization involves much more than instruction selection and cycle
counting. Listings 9.1 and 9.2 use pretty much the same instructions, 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 and even use the same approach of scanning with **REPNZ SCASB** and
@ -59,9 +59,9 @@ using **REPZ CMPS** to check scanning matches.
.code .code
public _FindString public _FindString
_FindStringprocnear _FindStringprocnear
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;point to our stack frame mov bp,sp ;point to our stack frame
push si ;preserve caller’s register variables push si ;preserve caller's register variables
push di push di
cld ;make string instructions increment pointers cld ;make string instructions increment pointers
mov si,[bp+SearchString] ;pointer to string to search for mov si,[bp+SearchString] ;pointer to string to search for
@ -80,12 +80,12 @@ using **REPZ CMPS** to check scanning matches.
mov di,[bp+Buffer] ;point ES:DI to buffer to search thru mov di,[bp+Buffer] ;point ES:DI to buffer to search thru
lodsb ;put the first byte of the search string in AL lodsb ;put the first byte of the search string in AL
mov bp,si ;set aside pointer to the second search byte mov bp,si ;set aside pointer to the second search byte
dec bx ;don’t need to compare the first byte of the dec bx ;don't need to compare the first byte of the
; string with CMPS; we’ll do it with SCAS ; string with CMPS; we'll do it with SCAS
FindStringLoop: FindStringLoop:
mov cx,dx ;put remaining buffer search length in CX mov cx,dx ;put remaining buffer search length in CX
repnz scasb ;scan for the first byte of the string repnz scasb ;scan for the first byte of the string
jnz FindStringNotFound ;not found, so there’s no match jnz FindStringNotFound ;not found, so there's no match
;found, so we have a potential match-check the ;found, so we have a potential match-check the
; rest of this candidate location ; rest of this candidate location
push di ;remember the address of the next byte to scan push di ;remember the address of the next byte to scan
@ -96,14 +96,14 @@ using **REPZ CMPS** to check scanning matches.
shr cx,1 ;convert to word for faster search shr cx,1 ;convert to word for faster search
jnc FindStringWord ;do word search if no odd byte jnc FindStringWord ;do word search if no odd byte
cmpsb ;compare the odd byte cmpsb ;compare the odd byte
jnz FindStringNoMatch ;odd byte doesn’t match, so we jnz FindStringNoMatch ;odd byte doesn't match, so we
; haven’t found the search string here ; haven't found the search string here
FindStringWord: FindStringWord:
jcxz FindStringFound ;test whether we’ve already checked jcxz FindStringFound ;test whether we've already checked
; the whole string; if so, this is a match ; the whole string; if so, this is a match
; bytes long; if so, we’ve found a match ; bytes long; if so, we've found a match
repz cmpsw ;check the rest of the string a word at a time repz cmpsw ;check the rest of the string a word at a time
jz FindStringFound ;it’s a match jz FindStringFound ;it's a match
FindStringNoMatch: FindStringNoMatch:
pop di ;get back pointer to the next byte to scan pop di ;get back pointer to the next byte to scan
and dx,dx ;is there anything left to check? and dx,dx ;is there anything left to check?
@ -117,9 +117,9 @@ using **REPZ CMPS** to check scanning matches.
; address of the byte after the start of the ; address of the byte after the start of the
; potential match) ; potential match)
FindStringDone: FindStringDone:
pop di ;restore caller’s register variables pop di ;restore caller's register variables
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
_FindStringendp _FindStringendp
end end

View file

@ -32,9 +32,9 @@
.code .code
public _FindString public _FindString
_FindStringprocnear _FindStringprocnear
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;point to our stack frame mov bp,sp ;point to our stack frame
push si ;preserve caller’s register variables push si ;preserve caller's register variables
push di push di
cld ;make string instructions increment pointers cld ;make string instructions increment pointers
mov si,[bp+SearchString] ;pointer to string to search for mov si,[bp+SearchString] ;pointer to string to search for
@ -62,7 +62,7 @@
FindStringLoop: FindStringLoop:
mov cx,dx ;put remaining buffer search length in CX mov cx,dx ;put remaining buffer search length in CX
repnz scasb ;scan for the scan byte repnz scasb ;scan for the scan byte
jnz FindStringNotFound ;not found, so there’s no match jnz FindStringNotFound ;not found, so there's no match
;found, so we have a potential match-check the ;found, so we have a potential match-check the
; rest of this candidate location ; rest of this candidate location
push di ;remember the address of the next byte to scan push di ;remember the address of the next byte to scan
@ -75,13 +75,13 @@
shr cx,1 ;convert to word for faster search shr cx,1 ;convert to word for faster search
jnc FindStringWord ;do word search if no odd byte jnc FindStringWord ;do word search if no odd byte
cmpsb ;compare the odd byte cmpsb ;compare the odd byte
jnz FindStringNoMatch ;odd byte doesn’t match, so we jnz FindStringNoMatch ;odd byte doesn't match, so we
; haven’t found the search string here ; haven't found the search string here
FindStringWord: FindStringWord:
jcxz FindStringFound ;if the string is only 1 byte long, jcxz FindStringFound ;if the string is only 1 byte long,
; we’ve found a match ; we've found a match
repz cmpsw ;check the rest of the string a word at a time repz cmpsw ;check the rest of the string a word at a time
jz FindStringFound ;it’s a match jz FindStringFound ;it's a match
FindStringNoMatch: FindStringNoMatch:
pop di ;get back pointer to the next byte to scan pop di ;get back pointer to the next byte to scan
and dx,dx ;is there anything left to check? and dx,dx ;is there anything left to check?
@ -94,9 +94,9 @@
sub ax,bx ; string was found (earlier we pushed the sub ax,bx ; string was found (earlier we pushed the
; address of the byte after the scan match) ; address of the byte after the scan match)
FindStringDone: FindStringDone:
pop di ;restore caller’s register variables pop di ;restore caller's register variables
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
_FindStringendp _FindStringendp
end end
@ -111,26 +111,26 @@
extern unsigned char * FindString(unsigned char *, unsigned int, extern unsigned char * FindString(unsigned char *, unsigned int,
unsigned char *, unsigned int, unsigned int); unsigned char *, unsigned int, unsigned int);
void main(void); void main(void);
static unsigned char TestBuffer[] = “When, in the course of human \ static unsigned char TestBuffer[] = "When, in the course of human \
events, it becomes necessary for one people to dissolve the \ events, it becomes necessary for one people to dissolve the \
political bands which have connected them with another, and to \ political bands which have connected them with another, and to \
assume among the powers of the earth the separate and equal station \ assume among the powers of the earth the separate and equal station \
to which the laws of nature and of nature’s God entitle them...”; to which the laws of nature and of nature's God entitle them...";
void main() { void main() {
static unsigned char TestString[] = “equal”; static unsigned char TestString[] = "equal";
unsigned char TempBuffer[DISPLAY_LENGTH+1]; unsigned char TempBuffer[DISPLAY_LENGTH+1];
unsigned char *MatchPtr; unsigned char *MatchPtr;
/* Search for TestString and report the results */ /* Search for TestString and report the results */
if ((MatchPtr = FindString(TestBuffer, if ((MatchPtr = FindString(TestBuffer,
(unsigned int) strlen(TestBuffer), TestString, (unsigned int) strlen(TestBuffer), TestString,
(unsigned int) strlen(TestString), 1)) == NULL) { (unsigned int) strlen(TestString), 1)) == NULL) {
/* TestString wasn’t found */ /* TestString wasn't found */
printf(“\”%s\“ not found\n”, TestString); printf("\"%s\" not found\n", TestString);
} else { } else {
/* TestString was found. Zero-terminate TempBuffer; strncpy /* TestString was found. Zero-terminate TempBuffer; strncpy
won’t do it if DISPLAY_LENGTH characters are copied */ won't do it if DISPLAY_LENGTH characters are copied */
TempBuffer[DISPLAY_LENGTH] = 0; TempBuffer[DISPLAY_LENGTH] = 0;
printf(“\”%s\“ found. Next %d characters at match:\n\”%s\“\n”, printf("\"%s\" found. Next %d characters at match:\n\"%s\"\n",
TestString, DISPLAY_LENGTH, TestString, DISPLAY_LENGTH,
strncpy(TempBuffer, MatchPtr, DISPLAY_LENGTH)); strncpy(TempBuffer, MatchPtr, DISPLAY_LENGTH));
} }

View file

@ -2,15 +2,15 @@
[Previous](09-04.html) [Table of Contents](index.html) [Next](09-06.html) [Previous](09-04.html) [Table of Contents](index.html) [Next](09-06.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
You’ll notice that in Listing 9.2 I didn’t use a table of character You'll notice that in Listing 9.2 I didn't use a table of character
frequencies in English text to determine the character for which to frequencies in English text to determine the character for which to
scan, but rather let the caller make that choice. Each buffer of bytes scan, but rather let the caller make that choice. Each buffer of bytes
has unique characteristics, and English-letter frequency could well be has unique characteristics, and English-letter frequency could well be
inappropriate. What if the buffer is filled with French text? Cyrillic? inappropriate. What if the buffer is filled with French text? Cyrillic?
What if it isn’t text that’s being searched? It might be worthwhile for What if it isn't text that's being searched? It might be worthwhile for
an application to build a dynamic frequency table for each buffer so an application to build a dynamic frequency table for each buffer so
that the best scan character could be chosen for each search. Or perhaps that the best scan character could be chosen for each search. Or perhaps
not, if the search isn’t time-critical or the buffer is small. not, if the search isn't time-critical or the buffer is small.
The point is that you can improve performance dramatically by The point is that you can improve performance dramatically by
understanding the nature of the data with which you work. (This is understanding the nature of the data with which you work. (This is
@ -23,14 +23,14 @@ heads.
#### Short Sorts {#Heading7} #### Short Sorts {#Heading7}
David Stafford (recently of Borland and Borland Japan) who happens to be David Stafford (recently of Borland and Borland Japan) who happens to be
one of the best assembly language programmers I’ve ever met, has written one of the best assembly language programmers I've ever met, has written
a C-callable routine that sorts an array of integers in ascending order. a C-callable routine that sorts an array of integers in ascending order.
That wouldn’t be particularly noteworthy, except that David’s routine, That wouldn't be particularly noteworthy, except that David's routine,
shown in Listing 9.4, is exactly *25 bytes* long. Look at the code; shown in Listing 9.4, is exactly *25 bytes* long. Look at the code;
you’ll keep saying to yourself, “But this doesn’t work...oh, yes, I you'll keep saying to yourself, "But this doesn't work...oh, yes, I
guess it does.” As they say in the Prego spaghetti sauce ads, *it’s in guess it does." As they say in the Prego spaghetti sauce ads, *it's in
there*—and what a job of packing. Anyway, David says that a 24-byte sort there*—and what a job of packing. Anyway, David says that a 24-byte sort
routine eludes him, and he’d like to know if anyone can come up with routine eludes him, and he'd like to know if anyone can come up with
one. one.
**LISTING 9.4 L9-4.ASM** **LISTING 9.4 L9-4.ASM**
@ -72,18 +72,18 @@ one.
One of the most annoying limitations of the x86 is that while the One of the most annoying limitations of the x86 is that while the
dividend operand to the **DIV** instruction can be 32 bits in size, both dividend operand to the **DIV** instruction can be 32 bits in size, both
the divisor and the result must be 16 bits. That’s particularly annoying the divisor and the result must be 16 bits. That's particularly annoying
in regards to the result because sometimes you just don’t know whether in regards to the result because sometimes you just don't know whether
the ratio of the dividend to the divisor is greater than 64K-1 or the ratio of the dividend to the divisor is greater than 64K-1 or
not—and if you guess wrong, you get that godawful Divide By Zero not—and if you guess wrong, you get that godawful Divide By Zero
interrupt. So, what is one to do when the result might not fit in 16 interrupt. So, what is one to do when the result might not fit in 16
bits, or when the dividend is larger than 32 bits? Fall back to a bits, or when the dividend is larger than 32 bits? Fall back to a
software division approach? That will work—but oh so slowly. software division approach? That will work—but oh so slowly.
There’s another technique that’s much faster than a pure software There's another technique that's much faster than a pure software
approach, albeit not so flexible. This technique allows arbitrarily approach, albeit not so flexible. This technique allows arbitrarily
large dividends and results, but the divisor is still limited to16 bits. large dividends and results, but the divisor is still limited to16 bits.
That’s not perfect, but it does solve a number of problems, in That's not perfect, but it does solve a number of problems, in
particular eliminating the possibility of a Divide By Zero interrupt particular eliminating the possibility of a Divide By Zero interrupt
from a too-large result. from a too-large result.

View file

@ -34,12 +34,12 @@
.code .code
public _Div public _Div
_Divprocnear _Divprocnear
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;point to our stack frame mov bp,sp ;point to our stack frame
push si ;preserve caller’s register variables push si ;preserve caller's register variables
push di push di
std ;we’re working from msb to lsb std ;we're working from msb to lsb
mov ax,ds mov ax,ds
mov es,ax ;for STOS mov es,ax ;for STOS
mov cx,[bp+DividendLength] mov cx,[bp+DividendLength]
@ -64,9 +64,9 @@
loop DivLoop loop DivLoop
mov ax,dx ;return the remainder mov ax,dx ;return the remainder
cld ;restore default Direction flag setting cld ;restore default Direction flag setting
pop di ;restore caller’s register variables pop di ;restore caller's register variables
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
_Divendp _Divendp
end end
@ -74,7 +74,7 @@
**LISTING 9.6 L9-6.C** **LISTING 9.6 L9-6.C**
/* Sample use of Div function to perform division when the result /* Sample use of Div function to perform division when the result
doesn’t fit in 16 bits */ doesn't fit in 16 bits */
#include <stdio.h> #include <stdio.h>
@ -87,15 +87,15 @@
unsigned int k, j = 0x10; unsigned int k, j = 0x10;
k = Div((unsigned int *)&i, sizeof(i), j, (unsigned int *)&m); k = Div((unsigned int *)&i, sizeof(i), j, (unsigned int *)&m);
printf(“%lu / %u = %lu r %u\n”, i, j, m, k); printf("%lu / %u = %lu r %u\n", i, j, m, k);
} }
#### Sweet Spot Revisited {#Heading9} #### Sweet Spot Revisited {#Heading9}
Way back in Volume 1, Number 1 of *PC TECHNIQUES*, (April/May 1990) I 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 wrote the very first of that magazine's HAX (\#1), which extolled the
virtues of placing your most commonly-used automatic (stack-based) virtues of placing your most commonly-used automatic (stack-based)
variables within the stack’s “sweet spot,” the area between +127 to -128 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 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 8088 can store addressing displacements that fall within that range in a
single byte; larger displacements require a full word of storage, single byte; larger displacements require a full word of storage,
@ -117,19 +117,19 @@ 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 prefix bytes often cost a cycle; on a 386SX, increased code size often
slows performance because instructions must be fetched through the slows performance because instructions must be fetched through the
half-pint 16-bit bus; on a 386, the effect depends on the instruction half-pint 16-bit bus; on a 386, the effect depends on the instruction
mix and whether there’s a cache. mix and whether there's a cache.
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 In C, however, you'll have to figure out the allocation scheme your
compiler uses to allocate automatic variables, and declare automatics compiler uses to allocate automatic variables, and declare automatics
appropriately to produce the desired effect. It can be done: I did it in 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, 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 a large one) by several K—not bad, when you consider that the "sweet
spot” optimization is essentially free, with no code reorganization, spot" optimization is essentially free, with no code reorganization,
change in logic, or heavy thinking involved. change in logic, or heavy thinking involved.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -5,28 +5,28 @@
#### Hard-Core Cycle Counting {#Heading10} #### Hard-Core Cycle Counting {#Heading10}
Next, we come to an item that cycle counters will love, especially since Next, we come to an item that cycle counters will love, especially since
it involves apparently incorrect documentation on Intel’s part. it involves apparently incorrect documentation on Intel's part.
According to Intel’s documents, all **RCR** and **RCL** instructions, According to Intel's documents, all **RCR** and **RCL** instructions,
which perform rotations through the Carry flag, as shown in Figure 9.4, which perform rotations through the Carry flag, as shown in Figure 9.4,
take 9 cycles on the 386 when working with a register operand. My take 9 cycles on the 386 when working with a register operand. My
measurements indicate that the 9-cycle execution time almost holds true measurements indicate that the 9-cycle execution time almost holds true
for *multibit* rotate-through-carries, which I’ve timed at 8 cycles for *multibit* rotate-through-carries, which I've timed at 8 cycles
apiece; for example, **RCR AX,CL** takes 8 cycles on *my* 386, as does apiece; for example, **RCR AX,CL** takes 8 cycles on *my* 386, as does
**RCL DX,2**. Contrast that with **ROR** and **ROL**, which can rotate **RCL DX,2**. Contrast that with **ROR** and **ROL**, which can rotate
the contents of a register any number of bits in just 3 cycles. the contents of a register any number of bits in just 3 cycles.
However, rotating by one bit through the Carry flag does *not* take 9 However, rotating by one bit through the Carry flag does *not* take 9
cycles, contrary to Intel’s *80386 Programmer’s Reference Manual*, or cycles, contrary to Intel's *80386 Programmer's Reference Manual*, or
even 8 cycles. In fact, **RCR** *reg*,1 and **RCL** *reg*,1 take 3 even 8 cycles. In fact, **RCR** *reg*,1 and **RCL** *reg*,1 take 3
cycles, just like **ROR, ROL, SHR,** and **SHL**. At least, that’s how cycles, just like **ROR, ROL, SHR,** and **SHL**. At least, that's how
fast they run on my 386, and I very much doubt that you’ll find fast they run on my 386, and I very much doubt that you'll find
different execution times on other 386s. (Please let me know if you do, different execution times on other 386s. (Please let me know if you do,
though!) though!)
![](images/09-04.jpg)\ ![](images/09-04.jpg)\
**Figure 9.4**  *Performing rotate instructions using the Carry flag.* **Figure 9.4**  *Performing rotate instructions using the Carry flag.*
Interestingly, according to Intel’s *i486 Microprocessor Programmer’s Interestingly, according to Intel's *i486 Microprocessor Programmer's
Reference Manual*, the 486 can **RCR** or **RCL** a register by one bit Reference Manual*, the 486 can **RCR** or **RCL** a register by one bit
in 3 cycles, but takes between 8 and 30 cycles to perform a multibit in 3 cycles, but takes between 8 and 30 cycles to perform a multibit
register **RCR** or **RCL**! register **RCR** or **RCL**!
@ -38,10 +38,10 @@ with a grain of salt.
#### Hardwired Far Jumps {#Heading11} #### Hardwired Far Jumps {#Heading11}
Did you ever wonder how to code a far jump to an absolute address in Did you ever wonder how to code a far jump to an absolute address in
assembly language? Probably not, but if you ever do, you’re going to be assembly language? Probably not, but if you ever do, you're going to be
glad for this next item, because the obvious solution doesn’t work. You glad for this next item, because the obvious solution doesn't work. You
might think all it would take to jump to, say, 1000:5 would be **JMP FAR might think all it would take to jump to, say, 1000:5 would be **JMP FAR
PTR 1000:5**, but you’d be wrong. That won’t even assemble. You might PTR 1000:5**, but you'd be wrong. That won't even assemble. You might
then think to construct in memory a far pointer containing 1000:5, as in then think to construct in memory a far pointer containing 1000:5, as in
the following: the following:
@ -58,8 +58,8 @@ almost certainly, some cycles for instruction fetching). On a 386, an
indirect far jump is documented to take at least 43 cycles in real mode indirect far jump is documented to take at least 43 cycles in real mode
(31 in protected mode); a direct far jump is documented to take at least (31 in protected mode); a direct far jump is documented to take at least
12 cycles, about three times faster. In truth, the difference between 12 cycles, about three times faster. In truth, the difference between
those two is nowhere near that big; the fastest I’ve measured for a those two is nowhere near that big; the fastest I've measured for a
direct far jump is 21 cycles, and I’ve measured indirect far jumps as direct far jump is 21 cycles, and I've measured indirect far jumps as
fast as 30 cycles, so direct is still faster, but not by so much. (Oh, fast as 30 cycles, so direct is still faster, but not by so much. (Oh,
those cycle-time documentation blues!) Also, a direct far jump is those cycle-time documentation blues!) Also, a direct far jump is
documented to take at least 27 cycles in protected mode; why the big documented to take at least 27 cycles in protected mode; why the big
@ -70,18 +70,18 @@ Although an indirect far jump will work, a direct far jump is still
preferable. preferable.
Listing 9.7 shows a short program that performs a direct far call to Listing 9.7 shows a short program that performs a direct far call to
1000:5. (Don’t run it, unless you want to crash your system!) It does 1000:5. (Don't run it, unless you want to crash your system!) It does
this by creating a dummy segment at 1000H, so that the label this by creating a dummy segment at 1000H, so that the label
**FarLabel** can be created with the desired far attribute at the proper **FarLabel** can be created with the desired far attribute at the proper
location. (Segments created with “AT” don’t cause the generation of any location. (Segments created with "AT" don't cause the generation of any
actual bytes or the allocation of any memory; they’re just templates.) actual bytes or the allocation of any memory; they're just templates.)
It’s a little kludgey, but at least it does work. There may be a better It's a little kludgey, but at least it does work. There may be a better
solution; if you have one, pass it along. solution; if you have one, pass it along.
**LISTING 9.7 L9-7.ASM** **LISTING 9.7 L9-7.ASM**
; Program to perform a direct far jump to address 1000:5. ; Program to perform a direct far jump to address 1000:5.
; *** Do not run this program! It’s just an example of how *** ; *** Do not run this program! It's just an example of how ***
; *** to build a direct far jump to an absolute address *** ; *** to build a direct far jump to an absolute address ***
; ;
; Tested with TASM 2 and MASM 5. ; Tested with TASM 2 and MASM 5.
@ -97,10 +97,10 @@ solution; if you have one, pass it along.
jmp FarLabel jmp FarLabel
end start end start
By the way, if you’re wondering how I figured this out, I merely applied By the way, if you're wondering how I figured this out, I merely applied
my good friend Dan Illowsky’s long-standing rule for dealing with MASM: my good friend Dan Illowsky's long-standing rule for dealing with MASM:
If the obvious doesn’t work (and it usually doesn’t), just try If the obvious doesn't work (and it usually doesn't), just try
everything you can think of, no matter how ridiculous, until you find everything you can think of, no matter how ridiculous, until you find
something that does—a rule with plenty of history on its side. something that does—a rule with plenty of history on its side.
@ -117,9 +117,9 @@ takes 4 cycles to execute, but is only 3 bytes long, while
mov eax,1 mov eax,1
takes only 2 cycles to execute, but is 5 bytes long (because native mode takes only 2 cycles to execute, but is 5 bytes long (because native mode
constants are dwords and the **MOV** instruction doesn’t sign-extend). constants are dwords and the **MOV** instruction doesn't sign-extend).
Both code fragments are ways to set **EAX** to 1 (although the first Both code fragments are ways to set **EAX** to 1 (although the first
affects the flags and the second doesn’t); this is a classic trade-off affects the flags and the second doesn't); this is a classic trade-off
of speed for space. Second, of speed for space. Second,
or ebx,-1 or ebx,-1
@ -129,14 +129,14 @@ takes 2 cycles to execute and is 3 bytes long, while
move bx,-1 move bx,-1
takes 2 cycles to execute and is 5 bytes long. Both instructions set takes 2 cycles to execute and is 5 bytes long. Both instructions set
**EBX** to -1; this is a classic trade-off of—gee, it’s not a trade-off **EBX** to -1; this is a classic trade-off of—gee, it's not a trade-off
at all, is it? **OR** is a better way to set a 32-bit register to all at all, is it? **OR** is a better way to set a 32-bit register to all
1-bits, just as **SUB** or **XOR** is a better way to set a register to 1-bits, just as **SUB** or **XOR** is a better way to set a register to
all 0-bits. Who woulda thunk it? Just goes to show how the 32-bit all 0-bits. Who woulda thunk it? Just goes to show how the 32-bit
displacements and constants of 386 native mode change the familiar displacements and constants of 386 native mode change the familiar
landscape of 80x86 optimization. landscape of 80x86 optimization.
Be warned, though, that I’ve found **OR, AND, ADD**, and the like to be Be warned, though, that I've found **OR, AND, ADD**, and the like to be
a cycle slower than **MOV** when working with immediate operands on the a cycle slower than **MOV** when working with immediate operands on the
386 under some circumstances, for reasons that thus far escape me. This 386 under some circumstances, for reasons that thus far escape me. This
just reinforces the first rule of optimization: Measure your code in just reinforces the first rule of optimization: Measure your code in

View file

@ -13,65 +13,65 @@ In ink. With nary a blemish.
The relevance of which will become apparent in a trice. The relevance of which will become apparent in a trice.
What my grandfather is, is a pattern matcher *par excellence*. You’re a What my grandfather is, is a pattern matcher *par excellence*. You're a
pattern matcher, too. So am I. We can’t help it; it comes with the pattern matcher, too. So am I. We can't help it; it comes with the
territory. Try focusing on text and not reading it. Can’t do it. Can you territory. Try focusing on text and not reading it. Can't do it. Can you
hear the voice of someone you know and not recognize it? I can’t. And hear the voice of someone you know and not recognize it? I can't. And
how in the Nine Billion Names of God is it that we’re capable of how in the Nine Billion Names of God is it that we're capable of
instantly recognizing one face out of the thousands we’ve seen in our instantly recognizing one face out of the thousands we've seen in our
lifetimes—even years later, from a different angle and in different lifetimes—even years later, from a different angle and in different
light? Although we take them for granted, our pattern-matching light? Although we take them for granted, our pattern-matching
capabilities are surely a miracle on the order of loaves and fishes. capabilities are surely a miracle on the order of loaves and fishes.
By “pattern matching,” I mean more than just recognition, though. I mean By "pattern matching," I mean more than just recognition, though. I mean
that we are generally able to take complex and often seemingly woefully that we are generally able to take complex and often seemingly woefully
inadequate data, instantaneously match it in an incredibly flexible way inadequate data, instantaneously match it in an incredibly flexible way
to our past experience, extrapolate, and reach amazing conclusions, to our past experience, extrapolate, and reach amazing conclusions,
something that computers can scarcely do at all. Crossword puzzles are something that computers can scarcely do at all. Crossword puzzles are
an excellent example; given a couple of letters and a cryptic clue, an excellent example; given a couple of letters and a cryptic clue,
we’re somehow able to come up with one out of several hundred thousand we're somehow able to come up with one out of several hundred thousand
words that we know. Try writing a program to do that! What’s more, we words that we know. Try writing a program to do that! What's more, we
don’t process data in the serial brute-force way that computers do. don't process data in the serial brute-force way that computers do.
Solutions tend to be virtually instantaneous or not at all; none of Solutions tend to be virtually instantaneous or not at all; none of
those “N log N” or “N^2”^ execution times for us. those "N log N" or "N^2"^ execution times for us.
It goes without saying that pattern matching is good; more than that, 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's a large part of what we are, and, generally, the faster we are at
it, the better. Not always, though. Sometimes insufficient information it, the better. Not always, though. Sometimes insufficient information
really is insufficient, and, in our haste to get the heady rush of really is insufficient, and, in our haste to get the heady rush of
coming up with a solution, incorrect or less-than-optimal conclusions coming up with a solution, incorrect or less-than-optimal conclusions
are reached, as anyone who has ever done the *Times* Sunday crossword are reached, as anyone who has ever done the *Times* Sunday crossword
will attest. Still, my grandfather does that puzzle every Sunday *in will attest. Still, my grandfather does that puzzle every Sunday *in
ink*. What’s his secret? Patience and discipline. He never fills a word 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 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 how strong the urge may be to put something down where he can see it and
feel like he’s getting somewhere. feel like he's getting somewhere.
There’s a surprisingly close parallel to programming here. Programming There's a surprisingly close parallel to programming here. Programming
is certainly a sort of pattern matching in the sense I’ve described is certainly a sort of pattern matching in the sense I've described
above, and, as with crossword puzzles, following your programming above, and, as with crossword puzzles, following your programming
instincts too quickly can be a liability. For many programmers, myself instincts too quickly can be a liability. For many programmers, myself
included, there’s a strong urge to find a workable approach to a included, there's a strong urge to find a workable approach to a
particular problem and start coding it *right now*, what some people particular problem and start coding it *right now*, what some people
call “hacking” a program. Going with the first thing your programming 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 pattern matcher comes up with can be a lot of fun; there's instant
gratification and a feeling of unbounded creativity. Personally, I’ve gratification and a feeling of unbounded creativity. Personally, I've
always hungered to get results from my work as soon as possible; I always hungered to get results from my work as soon as possible; I
gravitated toward graphics for its instant and very visible gravitated toward graphics for its instant and very visible
gratification. Over time, however, I’ve learned patience. gratification. Over time, however, I've learned patience.
------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 case history that nicely points up the wisdom of delaying gratification
when faced with programming problems, so that your mind has time to chew 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 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 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 even differ greatly from your initial solutions. Often, however, they
will be much better—and you’ll never even have the chance to decide 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 whether they're better or not if you take the first thing that comes
into your head and run with it. into your head and run with it.
#### The Case for Delayed Gratification {#Heading3} #### The Case for Delayed Gratification {#Heading3}
@ -80,38 +80,38 @@ Once upon a time, I set out to read *Algorithms*, by Robert Sedgewick
(Addison-Wesley), which turned out to be a wonderful, stimulating, and (Addison-Wesley), which turned out to be a wonderful, stimulating, and
most useful book, one that I recommend highly. My story, however, most useful book, one that I recommend highly. My story, however,
involves only what happened in the first 12 pages, for it was in those involves only what happened in the first 12 pages, for it was in those
pages that Sedgewick discussed Euclid’s algorithm. pages that Sedgewick discussed Euclid's algorithm.
Euclid’s algorithm (discovered by Euclid, of Euclidean geometry fame, a Euclid's algorithm (discovered by Euclid, of Euclidean geometry fame, a
very long time ago, way back when computers still used core memory) is a very long time ago, way back when computers still used core memory) is a
straightforward algorithm that solves one of the simplest problems straightforward algorithm that solves one of the simplest problems
imaginable: finding the greatest common integer divisor (GCD) of two imaginable: finding the greatest common integer divisor (GCD) of two
positive integers. Sedgewick points out that this is useful for reducing positive integers. Sedgewick points out that this is useful for reducing
a fraction to its lowest terms. I’m sure it’s useful for other things, a fraction to its lowest terms. I'm sure it's useful for other things,
as well, although none spring to mind. (A long time ago, I wrote an as well, although none spring to mind. (A long time ago, I wrote an
article about optimizing a bit of code that wasn’t even vaguely article about optimizing a bit of code that wasn't even vaguely
time-critical, and got swamped with letters telling me so. I knew it time-critical, and got swamped with letters telling me so. I knew it
wasn’t time-critical; it was just a good example. So for now, close your wasn't time-critical; it was just a good example. So for now, close your
eyes and *imagine* that finding the GCD is not only necessary but must eyes and *imagine* that finding the GCD is not only necessary but must
also be done as quickly as possible, because it’s perfect for the point also be done as quickly as possible, because it's perfect for the point
I want to make here and now. Okay?) I want to make here and now. Okay?)
The problem at hand, then, is simply this: Find the largest integer The problem at hand, then, is simply this: Find the largest integer
value that evenly divides two arbitrary positive integers. That’s all value that evenly divides two arbitrary positive integers. That's all
there is to it. So warm up your pattern matchers...and go! there is to it. So warm up your pattern matchers...and go!
### The Brute-Force Syndrome {#Heading4} ### The Brute-Force Syndrome {#Heading4}
I have a funny feeling that you’d already figured out how to find the I have a funny feeling that you'd already figured out how to find the
GCD before I even said “go.” That’s what I did when reading GCD before I even said "go." That's what I did when reading
*Algorithms;* before I read another word, I had to figure it out for *Algorithms;* before I read another word, I had to figure it out for
myself. Programmers are like that; give them a problem and their eyes myself. Programmers are like that; give them a problem and their eyes
immediately glaze over as they try to solve it before you’ve even shut immediately glaze over as they try to solve it before you've even shut
your mouth. That sort of instant response can certainly be impressive, your mouth. That sort of instant response can certainly be impressive,
but it can backfire, too, as it did in my case. but it can backfire, too, as it did in my case.
You see, I fell victim to a common programming pitfall, the You see, I fell victim to a common programming pitfall, the
“brute-force” syndrome. The basis of this syndrome is that there are "brute-force" syndrome. The basis of this syndrome is that there are
many problems that have obvious, brute-force solutions—with one small many problems that have obvious, brute-force solutions—with one small
drawback. The drawback is that if you were to try to apply a brute-force drawback. The drawback is that if you were to try to apply a brute-force
solution by hand—that is, work a single problem out with pencil and solution by hand—that is, work a single problem out with pencil and
@ -120,11 +120,11 @@ patience and discipline to work on the problem for approximately seven
hundred years, not counting eating and sleeping, in order to get an hundred years, not counting eating and sleeping, in order to get an
answer. Finding all the prime numbers less than 1,000,000 is a good answer. Finding all the prime numbers less than 1,000,000 is a good
example; just divide each number up to 1,000,000 by every lesser number, example; just divide each number up to 1,000,000 by every lesser number,
and see what’s left standing. For most of the history of humankind, and see what's left standing. For most of the history of humankind,
people were forced to think of cleverer solutions, such as the Sieve of people were forced to think of cleverer solutions, such as the Sieve of
Eratosthenes (we’d have been in big trouble if the ancient Greeks had Eratosthenes (we'd have been in big trouble if the ancient Greeks had
had computers), mainly because after about five minutes of brute had computers), mainly because after about five minutes of brute
force-type work, people’s attention gets diverted to other important force-type work, people's attention gets diverted to other important
matters, such as how far a paper airplane will fly from a second-story matters, such as how far a paper airplane will fly from a second-story
window. window.

View file

@ -2,14 +2,14 @@
[Previous](10-01.html) [Table of Contents](index.html) [Next](10-03.html) [Previous](10-01.html) [Table of Contents](index.html) [Next](10-03.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
Not so nowadays, though. Computers love boring work; they’re very Not so nowadays, though. Computers love boring work; they're very
patient and disciplined, and, besides, one human year = seven dog years patient and disciplined, and, besides, one human year = seven dog years
= two zillion computer years. So when we’re faced with a problem that = two zillion computer years. So when we're faced with a problem that
has an obvious but exceedingly lengthy solution, we’re apt to say, “Ah, has an obvious but exceedingly lengthy solution, we're apt to say, "Ah,
let the computer do that, it’s fast,” and go back to making paper let the computer do that, it's fast," and go back to making paper
airplanes. Unfortunately, brute-force solutions tend to be slow even airplanes. Unfortunately, brute-force solutions tend to be slow even
when performed by modern-day microcomputers, which are capable of when performed by modern-day microcomputers, which are capable of
several MIPS except when I’m late for an appointment and want to finish several MIPS except when I'm late for an appointment and want to finish
a compile and run just one more test before I leave, in which case the a compile and run just one more test before I leave, in which case the
crystal in my computer is apparently designed to automatically revert to crystal in my computer is apparently designed to automatically revert to
1 Hz.) 1 Hz.)
@ -18,7 +18,7 @@ The solution that I instantly came up with to finding the GCD is about
as brute- force as you can get: Divide both the larger integer (iL) and as brute- force as you can get: Divide both the larger integer (iL) and
the smaller integer (iS) by every integer equal to or less than the the smaller integer (iS) by every integer equal to or less than the
smaller integer, until a number is found that divides both evenly, as smaller integer, until a number is found that divides both evenly, as
shown in Figure 10.1. This works, but it’s a lousy solution, requiring shown in Figure 10.1. This works, but it's a lousy solution, requiring
as many as iS\*2 divisions; *very* expensive, especially for large as many as iS\*2 divisions; *very* expensive, especially for large
values of iS. For example, finding the GCD of 30,001 and 30,002 would values of iS. For example, finding the GCD of 30,001 and 30,002 would
require 60,002 divisions, which alone, disregarding tests and branches, require 60,002 divisions, which alone, disregarding tests and branches,
@ -88,7 +88,7 @@ Integer pairs for which to find GCD
**Listing 10.3**\ **Listing 10.3**\
(Division: code recursive\ (Division: code recursive\
Euclid’s algorithm) Euclid's algorithm)
20\ 20\
(33%) (33%)
@ -106,7 +106,7 @@ Integer pairs for which to find GCD
(0.12%) (0.12%)
**Listing 10.4**\ **Listing 10.4**\
(C version of data recursive Euclid’s algorithm; normal optimization) (C version of data recursive Euclid's algorithm; normal optimization)
12\ 12\
(20%) (20%)
@ -142,7 +142,7 @@ Integer pairs for which to find GCD
(0.05%) (0.05%)
**Listing 10.5**\ **Listing 10.5**\
(Assembly version of data recursive Euclid’s algorithm) (Assembly version of data recursive Euclid's algorithm)
10\ 10\
(17%) (17%)
@ -200,7 +200,7 @@ Table 10.1 Performance of GCD algorithm implementations.
#### Wasted Breakthroughs {#Heading5} #### Wasted Breakthroughs {#Heading5}
Sedgewick’s first solution to the GCD problem was pretty much the one I Sedgewick's first solution to the GCD problem was pretty much the one I
came up with. He then pointed out that the GCD of iL and iS is the same came up with. He then pointed out that the GCD of iL and iS is the same
as the GCD of iL-iS and iS. This was obvious (once Sedgewick pointed it as the GCD of iL-iS and iS. This was obvious (once Sedgewick pointed it
out); by the very nature of division, any number that divides iL evenly out); by the very nature of division, any number that divides iL evenly
@ -213,13 +213,13 @@ in Listing 10.2.
/* Finds and returns the greatest common divisor of two positive /* Finds and returns the greatest common divisor of two positive
integers. Works by subtracting the smaller integer from the integers. Works by subtracting the smaller integer from the
larger integer until either the values match (in which case larger integer until either the values match (in which case
that’s the gcd), or the larger integer becomes the smaller of that's the gcd), or the larger integer becomes the smaller of
the two, in which case the two integers swap roles and the the two, in which case the two integers swap roles and the
subtraction process continues. */ subtraction process continues. */
unsigned int gcd(unsigned int int1, unsigned int int2) { unsigned int gcd(unsigned int int1, unsigned int int2) {
unsigned int temp; unsigned int temp;
/* If the two integers are the same, that’s the gcd and we’re /* If the two integers are the same, that's the gcd and we're
done */ done */
if (int1 == int2) { if (int1 == int2) {
return(int1); return(int1);

View file

@ -3,22 +3,22 @@
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
Listing 10.2 repeatedly subtracts iS from iL until iL becomes less than Listing 10.2 repeatedly subtracts iS from iL until iL becomes less than
or equal to iS. If iL becomes equal to iS, then that’s the GCD; or equal to iS. If iL becomes equal to iS, then that's the GCD;
alternatively, if iL becomes *less* than iS, iL and iS switch values, alternatively, if iL becomes *less* than iS, iL and iS switch values,
and the process is repeated, as shown in Figure 10.2. The number of and the process is repeated, as shown in Figure 10.2. The number of
iterations this approach requires relative to Listing 10.1 depends iterations this approach requires relative to Listing 10.1 depends
heavily on the values of iL and iS, so it’s not always faster, but, as heavily on the values of iL and iS, so it's not always faster, but, as
Table 10.1 indicates, Listing 10.2 is generally much better code. Table 10.1 indicates, Listing 10.2 is generally much better code.
![](images/10-02.jpg)\ ![](images/10-02.jpg)\
**Figure 10.2**  *Using repeated subtraction algorithm to find a GCD.* **Figure 10.2**  *Using repeated subtraction algorithm to find a GCD.*
Listing 10.2 is a far graver misstep than Listing 10.1, for all that Listing 10.2 is a far graver misstep than Listing 10.1, for all that
it’s faster. Listing 10.1 is obviously a hacked-up, brute-force it's faster. Listing 10.1 is obviously a hacked-up, brute-force
approach; no one could mistake it for anything else. It could be speeded approach; no one could mistake it for anything else. It could be speeded
up in any of a number of ways with a little thought. (Simply skipping up in any of a number of ways with a little thought. (Simply skipping
testing all the divisors between iS and iS/2, not inclusive, would cut testing all the divisors between iS and iS/2, not inclusive, would cut
the worst-case time in half, for example; that’s not a particularly the worst-case time in half, for example; that's not a particularly
*good* optimization, but it illustrates how easily Listing 10.1 can be *good* optimization, but it illustrates how easily Listing 10.1 can be
improved.) Listing 10.1 is a hack job, crying out for inspiration. improved.) Listing 10.1 is a hack job, crying out for inspiration.
@ -26,20 +26,20 @@ Listing 10.2, on the other hand, has gotten the inspiration—and largely
wasted it through haste. Had Sedgewick not told me otherwise, I might wasted it through haste. Had Sedgewick not told me otherwise, I might
well have assumed that Listing 10.2 was optimized, a mistake I would well have assumed that Listing 10.2 was optimized, a mistake I would
never have made with Listing 10.1. I experienced a conceptual never have made with Listing 10.1. I experienced a conceptual
breakthrough when I understood Sedgewick’s point: A smaller number can breakthrough when I understood Sedgewick's point: A smaller number can
be subtracted from a larger number without affecting their GCD, thereby be subtracted from a larger number without affecting their GCD, thereby
inexpensively reducing the scale of the problem. And, in my hurry to inexpensively reducing the scale of the problem. And, in my hurry to
make this breakthrough reality, I missed its full scope. As Sedgewick make this breakthrough reality, I missed its full scope. As Sedgewick
says on the very next page, the number that one gets by subtracting iS says on the very next page, the number that one gets by subtracting iS
from iL until iL is less than iS is precisely the same as the remainder from iL until iL is less than iS is precisely the same as the remainder
that one gets by dividing iL by iS—again, this is inherent in the nature that one gets by dividing iL by iS—again, this is inherent in the nature
of division—and *that* is the basis for Euclid’s algorithm, shown in of division—and *that* is the basis for Euclid's algorithm, shown in
Figure 10.3. Listing 10.3 is an implementation of Euclid’s algorithm. Figure 10.3. Listing 10.3 is an implementation of Euclid's algorithm.
**LISTING 10.3 L10-3.C** **LISTING 10.3 L10-3.C**
/* Finds and returns the greatest common divisor of two integers. /* Finds and returns the greatest common divisor of two integers.
Uses Euclid’s algorithm: divides the larger integer by the Uses Euclid's algorithm: divides the larger integer by the
smaller; if the remainder is 0, the smaller integer is the GCD, smaller; if the remainder is 0, the smaller integer is the GCD,
otherwise the smaller integer becomes the larger integer, the otherwise the smaller integer becomes the larger integer, the
remainder becomes the smaller integer, and the process is remainder becomes the smaller integer, and the process is
@ -49,7 +49,7 @@ Figure 10.3. Listing 10.3 is an implementation of Euclid’s algorithm.
unsigned int gcd(unsigned int int1, unsigned int int2) { unsigned int gcd(unsigned int int1, unsigned int int2) {
unsigned int temp; unsigned int temp;
/* If the two integers are the same, that’s the GCD and we’re /* If the two integers are the same, that's the GCD and we're
done */ done */
if (int1 == int2) { if (int1 == int2) {
return(int1); return(int1);
@ -82,32 +82,32 @@ Figure 10.3. Listing 10.3 is an implementation of Euclid’s algorithm.
return(gcd_recurs(smaller_int, temp)); return(gcd_recurs(smaller_int, temp));
} }
As you can see from Table 10.1, Euclid’s algorithm is superior, 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 especially for large numbers (and imagine if we were working with large
*longs!*). *longs!*).
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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.*
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/10-03.jpg)\ ![](images/10-03.jpg)\
**Figure 10.3**  *Using Euclid’s algorithm to find a GCD.* **Figure 10.3**  *Using Euclid's algorithm to find a GCD.*
Give your mind time and space to wander around the edges of important Give your mind time and space to wander around the edges of important
programming problems before you settle on any one approach. I titled programming problems before you settle on any one approach. I titled
this book’s first chapter “The Best Optimizer Is between Your Ears,” and this book's first chapter "The Best Optimizer Is between Your Ears," and
that’s still true; what’s even more true is that the optimizer between that's still true; what's even more true is that the optimizer between
your ears does its best work not at the implementation stage, but at the your ears does its best work not at the implementation stage, but at the
very beginning, when you try to imagine how what you want to do and what very beginning, when you try to imagine how what you want to do and what
a computer is capable of doing can best be brought together. a computer is capable of doing can best be brought together.
### Recursion {#Heading6} ### Recursion {#Heading6}
Euclid’s algorithm lends itself to recursion beautifully, so much so Euclid's algorithm lends itself to recursion beautifully, so much so
that an implementation like Listing 10.3 comes almost without thought. that an implementation like Listing 10.3 comes almost without thought.
Again, though, take a moment to stop and consider what’s really going Again, though, take a moment to stop and consider what's really going
on, at the assembly language level, in Listing 10.3. There’s recursion on, at the assembly language level, in Listing 10.3. There's recursion
and then there’s recursion; code recursion and data recursion, to be and then there's recursion; code recursion and data recursion, to be
exact. Listing 10.3 is code recursion—recursion through calls—the sort exact. Listing 10.3 is code recursion—recursion through calls—the sort
most often used because it is conceptually simplest. However, code most often used because it is conceptually simplest. However, code
recursion tends to be slow because it pushes parameters and calls a recursion tends to be slow because it pushes parameters and calls a
@ -120,7 +120,7 @@ recursive operations that Listing 10.3 does.
**LISTING 10.4 L10-4.C** **LISTING 10.4 L10-4.C**
/* Finds and returns the greatest common divisor of two integers. /* Finds and returns the greatest common divisor of two integers.
Uses Euclid’s algorithm: divides the larger integer by the Uses Euclid's algorithm: divides the larger integer by the
smaller; if the remainder is 0, the smaller integer is the GCD, smaller; if the remainder is 0, the smaller integer is the GCD,
otherwise the smaller integer becomes the larger integer, the otherwise the smaller integer becomes the larger integer, the
remainder becomes the smaller integer, and the process is remainder becomes the smaller integer, and the process is
@ -136,7 +136,7 @@ recursive operations that Listing 10.3 does.
int2 = temp; int2 = temp;
} }
/* Now loop, dividing int1 by int2 and checking the remainder, /* Now loop, dividing int1 by int2 and checking the remainder,
until the remainder is 0. At each step, if the remainder isn’t until the remainder is 0. At each step, if the remainder isn't
0, assign int2 to int1, and the remainder to int2, then 0, assign int2 to int1, and the remainder to int2, then
repeat */ repeat */
for (;;) { for (;;) {
@ -154,9 +154,9 @@ recursive operations that Listing 10.3 does.
#### Patient Optimization {#Heading7} #### Patient Optimization {#Heading7}
At long last, we’re ready to optimize GCD determination in the classic At long last, we're ready to optimize GCD determination in the classic
sense. Table 10.1 shows the performance of Listing 10.4 with and without sense. Table 10.1 shows the performance of Listing 10.4 with and without
Microsoft C/C++’s maximum optimization, and also shows the performance Microsoft C/C++'s maximum optimization, and also shows the performance
of Listing 10.5, an assembly language version of Listing 10.4. Sure, the of Listing 10.5, an assembly language version of Listing 10.4. Sure, the
optimized versions are faster than the unoptimized version of Listing optimized versions are faster than the unoptimized version of Listing
10.4—but the gains are small compared to those realized from the 10.4—but the gains are small compared to those realized from the

View file

@ -5,7 +5,7 @@
**LISTING 10.5 L10-5.ASM** **LISTING 10.5 L10-5.ASM**
; Finds and returns the greatest common divisor of two integers. ; Finds and returns the greatest common divisor of two integers.
; Uses Euclid’s algorithm: divides the larger integer by the ; Uses Euclid's algorithm: divides the larger integer by the
; smaller; if the remainder is 0, the smaller integer is the GCD, ; smaller; if the remainder is 0, the smaller integer is the GCD,
; otherwise the smaller integer becomes the larger integer, the ; otherwise the smaller integer becomes the larger integer, the
; remainder becomes the smaller integer, and the process is ; remainder becomes the smaller integer, and the process is
@ -29,21 +29,21 @@
public _gcd public _gcd
align 2 align 2
_gcd proc near _gcd proc near
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;set up our stack frame mov bp,sp ;set up our stack frame
push si ;preserve caller’s register variables push si ;preserve caller's register variables
push di push di
;Swap if necessary to make sure that int1 >= int2 ;Swap if necessary to make sure that int1 >= int2
mov ax,int1[bp] mov ax,int1[bp]
mov bx,int2[bp] mov bx,int2[bp]
cmp ax,bx ;is int1 >= int2? cmp ax,bx ;is int1 >= int2?
jnb IntsSet ;yes, so we’re all set jnb IntsSet ;yes, so we're all set
xchg ax,bx ;no, so swap int1 and int2 xchg ax,bx ;no, so swap int1 and int2
IntsSet: IntsSet:
; Now loop, dividing int1 by int2 and checking the remainder, until ; Now loop, dividing int1 by int2 and checking the remainder, until
; the remainder is 0. At each step, if the remainder isn’t 0, assign ; the remainder is 0. At each step, if the remainder isn't 0, assign
; int2 to int1, and the remainder to int2, then repeat. ; int2 to int1, and the remainder to int2, then repeat.
GCDLoop: GCDLoop:
;if the remainder of int1 divided by ;if the remainder of int1 divided by
@ -85,18 +85,18 @@
align2 align2
Done: Done:
mov ax,bx ;return the GCD mov ax,bx ;return the GCD
pop di ;restore caller’s register variables pop di ;restore caller's register variables
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
_gcd endp _gcd endp
end end
Assembly language optimization is pattern matching on a local scale. Assembly language optimization is pattern matching on a local scale.
Frankly, it’s also the sort of boring, brute-force work that people are 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 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 tied behind their back *if* they knew as much about the code you're
writing as you do, which they don’t. writing as you do, which they don't.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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.*
@ -106,7 +106,7 @@ Computers are *much* worse at that sort of pattern matching than humans;
computers have no way to integrate vast amounts of disparate computers have no way to integrate vast amounts of disparate
information, much of it only vaguely defined or subject to change. information, much of it only vaguely defined or subject to change.
People, oddly enough, are *better* at global optimization than at local People, oddly enough, are *better* at global optimization than at local
optimization. For one thing, it’s more interesting. For another, it’s optimization. For one thing, it's more interesting. For another, it's
complex and imprecise enough to allow intuition and inspiration, two complex and imprecise enough to allow intuition and inspiration, two
vastly underrated programming tools, to come to the fore. And, as I vastly underrated programming tools, to come to the fore. And, as I
pointed out earlier, people tend to perform instantaneous solutions to pointed out earlier, people tend to perform instantaneous solutions to
@ -122,12 +122,12 @@ 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 problem and run through it two or three times, from different angles, to
see what unexpected solutions it can come up with. see what unexpected solutions it can come up with.
Pull back the reins a little. Don’t measure progress by lines of code Pull back the reins a little. Don't measure progress by lines of code
written today; measure it instead by overall progress and by quality. written today; measure it instead by overall progress and by quality.
Relax and listen to that quiet inner voice that provides the real Relax and listen to that quiet inner voice that provides the real
breakthroughs. Stop, look, listen—and think. Not only will you find that breakthroughs. Stop, look, listen—and think. Not only will you find that
it’s a more productive and creative way to program—but you’ll also find it's a more productive and creative way to program—but you'll also find
that it’s more fun. that it's more fun.
And think what you could do with all those extra computer years! And think what you could do with all those extra computer years!

View file

@ -22,7 +22,7 @@ architecture, to a greater degree than you might expect, for the x86
family came into full maturity with the 386; the 486 and the Pentium are family came into full maturity with the 386; the 486 and the Pentium are
really nothing more than faster 386s, with very little in the way of new really nothing more than faster 386s, with very little in the way of new
functionality. In contrast, the 286 added a number of instructions, functionality. In contrast, the 286 added a number of instructions,
respectable performance, and protected mode to the 8088’s capabilities, respectable performance, and protected mode to the 8088's capabilities,
and the 386 added more instructions and a whole new set of addressing and the 386 added more instructions and a whole new set of addressing
modes, and brought the x86 family into the 32-bit world that represents modes, and brought the x86 family into the 32-bit world that represents
the future (and, increasingly, the present) of personal computing. This the future (and, increasingly, the present) of personal computing. This
@ -41,34 +41,34 @@ numerous permutations of the 486, and now the Pentium—really matter.
The 8088 is now all but extinct in the PC arena. The 8086 was used The 8088 is now all but extinct in the PC arena. The 8086 was used
fairly widely for a while, but has now all but disappeared. The 80186 fairly widely for a while, but has now all but disappeared. The 80186
and 80188 never really caught on for use in PC and don’t require further and 80188 never really caught on for use in PC and don't require further
discussion. discussion.
That leaves us with the high-end chips: the 286, the 386SX, the 386, the That leaves us with the high-end chips: the 286, the 386SX, the 386, the
486, and the Pentium. At this writing, the 386SX is fast going the way 486, and the Pentium. At this writing, the 386SX is fast going the way
of the 8088; people are realizing that its relatively small cost of the 8088; people are realizing that its relatively small cost
advantage over the 386 isn’t enough to offset its relatively large advantage over the 386 isn't enough to offset its relatively large
performance disadvantage. After all, the 386SX suffers from the same performance disadvantage. After all, the 386SX suffers from the same
debilitating problem that looms over the 8088—a too-small bus. debilitating problem that looms over the 8088—a too-small bus.
Internally, the 386SX is a 32-bit processor, but externally, it’s a Internally, the 386SX is a 32-bit processor, but externally, it's a
16-bit processor, a non-optimal architecture, especially for 32-bit 16-bit processor, a non-optimal architecture, especially for 32-bit
code. code.
I’m not going to discuss the 386SX in detail. If you do find yourself I'm not going to discuss the 386SX in detail. If you do find yourself
programming for the 386SX, follow the same general rules you should programming for the 386SX, follow the same general rules you should
follow for the 8088: use short instructions, use the registers as follow for the 8088: use short instructions, use the registers as
heavily as possible, and don’t branch. In other words, avoid memory, heavily as possible, and don't branch. In other words, avoid memory,
since the 386SX is by definition better at processing data internally since the 386SX is by definition better at processing data internally
than it is at accessing memory. than it is at accessing memory.
The 486 is a world unto itself for the purposes of optimization, and the The 486 is a world unto itself for the purposes of optimization, and the
Pentium is a *universe* unto itself. We’ll treat them separately in Pentium is a *universe* unto itself. We'll treat them separately in
later chapters. later chapters.
This leaves us with just two processors: the 286 and the 386. Each was This leaves us with just two processors: the 286 and the 386. Each was
*the* PC standard in its day. The 286 is no longer used in new systems, *the* PC standard in its day. The 286 is no longer used in new systems,
but there are millions of 286-based systems still in daily use. The 386 but there are millions of 286-based systems still in daily use. The 386
is still being used in new systems, although it’s on the downhill leg of is still being used in new systems, although it's on the downhill leg of
its lifespan, and it is in even wider use than the 286. The future its lifespan, and it is in even wider use than the 286. The future
clearly belongs to the 486 and Pentium, but the 286 and 386 are still clearly belongs to the 486 and Pentium, but the 286 and 386 are still
very much a part of the present-day landscape. very much a part of the present-day landscape.
@ -78,16 +78,16 @@ very much a part of the present-day landscape.
Apart from vastly improved performance, the biggest difference between Apart from vastly improved performance, the biggest difference between
the 8088 and the 286 and 386 (as well as the later Intel CPUs) is that the 8088 and the 286 and 386 (as well as the later Intel CPUs) is that
the 286 introduced protected mode, and the 386 greatly expanded the the 286 introduced protected mode, and the 386 greatly expanded the
capabilities of protected mode. We’re only going to talk about real-mode capabilities of protected mode. We're only going to talk about real-mode
operation of the 286 and 386 in this book, however. Protected mode operation of the 286 and 386 in this book, however. Protected mode
offers a whole new memory management scheme, one that isn’t supported by offers a whole new memory management scheme, one that isn't supported by
the 8088. Only code specifically written for protected mode can run in the 8088. Only code specifically written for protected mode can run in
that mode; it’s an alien and hostile environment for MS-DOS programs. that mode; it's an alien and hostile environment for MS-DOS programs.
In particular, segments are different creatures in protected mode. In particular, segments are different creatures in protected mode.
They’re *selectors*—indexes into a table of segment descriptors—rather They're *selectors*—indexes into a table of segment descriptors—rather
than plain old registers, and can’t be set to arbitrary values. That than plain old registers, and can't be set to arbitrary values. That
means that segments can’t be used for temporary storage or as part of a means that segments can't be used for temporary storage or as part of a
fast indivisible 32-bit load from memory, as in fast indivisible 32-bit load from memory, as in
les ax,dword ptr [LongVar] les ax,dword ptr [LongVar]
@ -111,37 +111,37 @@ any one program may depend far less on code quality than on how
efficiently the program uses operating system services and how often and efficiently the program uses operating system services and how often and
under what circumstances the operating system preempts the program. under what circumstances the operating system preempts the program.
Protected mode programs are often mostly collections of operating system Protected mode programs are often mostly collections of operating system
calls, and the performance of whatever code *isn’t* operating-system calls, and the performance of whatever code *isn't* operating-system
oriented may depend primarily on how large a time slice the operating oriented may depend primarily on how large a time slice the operating
system gives that code to run in. system gives that code to run in.
In short, taken as a whole, protected mode programming is a different In short, taken as a whole, protected mode programming is a different
kettle of fish altogether from what I’ve been describing in this book. kettle of fish altogether from what I've been describing in this book.
There’s certainly a knack to optimizing specifically for protected mode There's certainly a knack to optimizing specifically for protected mode
under a given operating system...but it’s not what we’ve been learning, under a given operating system...but it's not what we've been learning,
and now is not the time to pursue it further. In general, though, the and now is not the time to pursue it further. In general, though, the
optimization strategies discussed in this book still hold true in optimization strategies discussed in this book still hold true in
protected mode; it’s just issues specific to protected mode or a protected mode; it's just issues specific to protected mode or a
particular operating system that we won’t discuss. particular operating system that we won't discuss.
#### In the Lair of the Cycle-Eaters, Part II {#Heading5} #### In the Lair of the Cycle-Eaters, Part II {#Heading5}
Under the programming interface, the 286 and 386 differ considerably Under the programming interface, the 286 and 386 differ considerably
from the 8088. Nonetheless, with one exception and one addition, the from the 8088. Nonetheless, with one exception and one addition, the
cycle-eaters remain much the same on computers built around the 286 and cycle-eaters remain much the same on computers built around the 286 and
386. Next, we’ll review each of the familiar cycle-eaters I covered in 386. Next, we'll review each of the familiar cycle-eaters I covered in
Chapter 4 as they apply to the 286 and 386, and we’ll look at the new Chapter 4 as they apply to the 286 and 386, and we'll look at the new
member of the gang, the data alignment cycle-eater. member of the gang, the data alignment cycle-eater.
The one cycle-eater that vanishes on the 286 and 386 is the 8-bit bus The one cycle-eater that vanishes on the 286 and 386 is the 8-bit bus
cycle-eater. The 286 is a 16-bit processor both internally and cycle-eater. The 286 is a 16-bit processor both internally and
externally, and the 386 is a 32-bit processor both internally and externally, and the 386 is a 32-bit processor both internally and
externally, so the Execution Unit/Bus Interface Unit size mismatch that externally, so the Execution Unit/Bus Interface Unit size mismatch that
plagues the 8088 is eliminated. Consequently, there’s no longer any need plagues the 8088 is eliminated. Consequently, there's no longer any need
to use byte-sized memory variables in preference to word-sized to use byte-sized memory variables in preference to word-sized
variables, at least so long as word-sized variables start at even variables, at least so long as word-sized variables start at even
addresses, as we’ll see shortly. On the other hand, access to byte-sized addresses, as we'll see shortly. On the other hand, access to byte-sized
variables still isn’t any *slower* than access to word-sized variables, variables still isn't any *slower* than access to word-sized variables,
so you can use whichever size suits a given task best. so you can use whichever size suits a given task best.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -10,12 +10,12 @@ have larger prefetch queues than the 8088 (6 bytes for the 286, 16 bytes
for the 386) and can perform memory accesses, including instruction for the 386) and can perform memory accesses, including instruction
fetches, in far fewer cycles than the 8088. fetches, in far fewer cycles than the 8088.
However, the prefetch queue cycle-eater *doesn’t* vanish on either the However, the prefetch queue cycle-eater *doesn't* vanish on either the
286 or the 386, for several reasons. For one thing, branching 286 or the 386, for several reasons. For one thing, branching
instructions still empty the prefetch queue, so instruction fetching instructions still empty the prefetch queue, so instruction fetching
still slows things down after most branches; when the prefetch queue is still slows things down after most branches; when the prefetch queue is
empty, it doesn’t much matter how big it is. (Even apart from emptying empty, it doesn't much matter how big it is. (Even apart from emptying
the prefetch queue, branches aren’t particularly fast on the 286 or the the prefetch queue, branches aren't particularly fast on the 286 or the
386, at a minimum of seven-plus cycles apiece. Avoid branching whenever 386, at a minimum of seven-plus cycles apiece. Avoid branching whenever
possible.) possible.)
@ -52,8 +52,8 @@ with a 286 is fairly expensive, and memory that can keep up with a 386
is *very* expensive. Instead, computer designers use alternative memory is *very* expensive. Instead, computer designers use alternative memory
architectures that offer more performance for the dollar—but less architectures that offer more performance for the dollar—but less
performance overall—than zero-wait-state memory. (It *is* possible to 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 build zero-wait-state systems for the 286 and 386; it's just so
expensive that it’s rarely done.) expensive that it's rarely done.)
The IBM AT and true compatibles use one-wait-state memory (some AT 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 clones use zero-wait-state memory, but such clones are less common than
@ -62,7 +62,7 @@ systems—including high-speed caches, interleaved memory, and
static-column RAM—that insert anywhere from 0 to about 5 wait states 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 (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 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 interaction between the code being executed and the memory system it's
running on. running on.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -70,16 +70,16 @@ running on.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
The many memory systems in use make it impossible for us to optimize for 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. 286/386 computers with the precision that's possible on the 8088.
Instead, we must write code that runs reasonably well under the varying Instead, we must write code that runs reasonably well under the varying
conditions found in the 286/386 arena. conditions found in the 286/386 arena.
The wait states that occur on most accesses to system memory in 286 and The wait states that occur on most accesses to system memory in 286 and
386 computers mean that nearly every access to system memory—memory in 386 computers mean that nearly every access to system memory—memory in
the DOS’s normal 640K memory area—is slowed down. (Accesses in computers the DOS's normal 640K memory area—is slowed down. (Accesses in computers
with high-speed caches may be wait-state-free if the desired data is with high-speed caches may be wait-state-free if the desired data is
already in the cache, but will certainly encounter wait states if the already in the cache, but will certainly encounter wait states if the
data isn’t cached; this phenomenon produces highly variable instruction data isn't cached; this phenomenon produces highly variable instruction
execution times.) While this is our first encounter with system memory execution times.) While this is our first encounter with system memory
wait states, we have run into a wait-state cycle-eater before: the wait states, we have run into a wait-state cycle-eater before: the
display adapter cycle-eater, which we discussed along with the other display adapter cycle-eater, which we discussed along with the other
@ -104,14 +104,14 @@ Unit.
And that, my friend, is unmistakably the prefetch queue cycle-eater. I And that, my friend, is unmistakably the prefetch queue cycle-eater. I
might add that the prefetch queue cycle-eater is in rare good form in might add that the prefetch queue cycle-eater is in rare good form in
the above example: A 4-to-1 ratio of instruction fetch time to execution the above example: A 4-to-1 ratio of instruction fetch time to execution
time is in a class with the best (or worst!) that’s found on the 8088. time is in a class with the best (or worst!) that's found on the 8088.
Let’s check out the prefetch queue cycle-eater in action. Listing 11.1 Let's check out the prefetch queue cycle-eater in action. Listing 11.1
times **MOV [WordVar],0**. The Zen timer reports that on a times **MOV [WordVar],0**. The Zen timer reports that on a
one-wait-state 10 MHz 286-based AT clone (the computer used for all one-wait-state 10 MHz 286-based AT clone (the computer used for all
tests in this chapter), Listing 11.1 runs in 1.27 µs per instruction. tests in this chapter), Listing 11.1 runs in 1.27 µs per instruction.
That’s 12.7 cycles per instruction, just as we calculated. (That extra That's 12.7 cycles per instruction, just as we calculated. (That extra
seven-tenths of a cycle comes from DRAM refresh, which we’ll get to seven-tenths of a cycle comes from DRAM refresh, which we'll get to
shortly.) shortly.)
**LISTING 11.1 L11-1.ASM** **LISTING 11.1 L11-1.ASM**
@ -137,7 +137,7 @@ shortly.)
call ZTimerOff call ZTimerOff
What does this mean? It means that, practically speaking, the 286 as What does this mean? It means that, practically speaking, the 286 as
used in the AT doesn’t have a 16-bit bus. From a performance used in the AT doesn't have a 16-bit bus. From a performance
perspective, the 286 in an AT has two-thirds of a 16-bit bus (a 10.7-bit perspective, the 286 in an AT has two-thirds of a 16-bit bus (a 10.7-bit
bus?), since every bus access on an AT takes 50 percent longer than it bus?), since every bus access on an AT takes 50 percent longer than it
should. A 286 running at 10 MHz *should* be able to access memory at a should. A 286 running at 10 MHz *should* be able to access memory at a

View file

@ -6,7 +6,7 @@ In short, a close relative of our old friend the 8-bit bus
cycle-eater—the system memory wait state cycle-eater—haunts us still on cycle-eater—the system memory wait state cycle-eater—haunts us still on
all but zero-wait-state 286 and 386 computers, and that means that the all but zero-wait-state 286 and 386 computers, and that means that the
prefetch queue cycle-eater is alive and well. (The system memory wait prefetch queue cycle-eater is alive and well. (The system memory wait
state cycle-eater isn’t really a new cycle-eater, but rather a variant state cycle-eater isn't really a new cycle-eater, but rather a variant
of the general wait state cycle-eater, of which the display adapter of the general wait state cycle-eater, of which the display adapter
cycle-eater is yet another variant.) While the 286 in the AT can fetch cycle-eater is yet another variant.) While the 286 in the AT can fetch
instructions much faster than can the 8088 in the PC, it can execute instructions much faster than can the 8088 in the PC, it can execute
@ -21,7 +21,7 @@ outrun even zero—5 cycles longer than the official execution time.)
To summarize: To summarize:
- Memory-accessing instructions don’t run at their official speeds on - Memory-accessing instructions don't run at their official speeds on
non-zero-wait-state 286/386 computers. non-zero-wait-state 286/386 computers.
- The prefetch queue cycle-eater reduces performance on 286/386 - The prefetch queue cycle-eater reduces performance on 286/386
computers, particularly when non-zero-wait-state memory is used. computers, particularly when non-zero-wait-state memory is used.
@ -31,21 +31,21 @@ To summarize:
performance varies from one 286/386 computer to another, making performance varies from one 286/386 computer to another, making
precise optimization impossible. precise optimization impossible.
What’s to be learned from all this? Several things: What's to be learned from all this? Several things:
- Keep your instructions short. - Keep your instructions short.
- Keep it in the registers; avoid memory, since memory generally can’t - Keep it in the registers; avoid memory, since memory generally can't
keep up with the processor. keep up with the processor.
- Don’t jump. - Don't jump.
Of course, those are exactly the rules that apply to 8088 optimization Of course, those are exactly the rules that apply to 8088 optimization
as well. Isn’t it convenient that the same general rules apply across as well. Isn't it convenient that the same general rules apply across
the board? the board?
#### Data Alignment {#Heading7 align="center"} #### Data Alignment {#Heading7 align="center"}
Thanks to its 16-bit bus, the 286 can access word-sized memory variables Thanks to its 16-bit bus, the 286 can access word-sized memory variables
just as fast as byte-sized variables. There’s a catch, however: That’s just as fast as byte-sized variables. There's a catch, however: That's
only true for word-sized variables that start at even addresses. When only true for word-sized variables that start at even addresses. When
the 286 is asked to perform a word-sized access starting at an odd the 286 is asked to perform a word-sized access starting at an odd
address, it actually performs two separate accesses, each of which address, it actually performs two separate accesses, each of which
@ -62,20 +62,20 @@ address is easy to calculate: Two accesses take twice as long as one
access. access.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 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 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 differently—occurring only at odd addresses—and is avoided with a
different workaround, we’ll consider it to be a new cycle-eater.) different workaround, we'll consider it to be a new cycle-eater.)
![](images/11-01.jpg)\ ![](images/11-01.jpg)\
**Figure 11.1**  *The data alignment cycle-eater.* **Figure 11.1**  *The data alignment cycle-eater.*
The way to deal with the data alignment cycle-eater is straightforward: The way to deal with the data alignment cycle-eater is straightforward:
*Don’t perform word-sized accesses to odd addresses on the 286 if you *Don't perform word-sized accesses to odd addresses on the 286 if you
can help it*. The easiest way to avoid the data alignment cycle-eater is can help it*. The easiest way to avoid the data alignment cycle-eater is
to place the directive **EVEN** before each of your word-sized to place the directive **EVEN** before each of your word-sized
variables. **EVEN** forces the offset of the next byte assembled to be variables. **EVEN** forces the offset of the next byte assembled to be
@ -85,7 +85,7 @@ by the 286 simply by preceding it with **EVEN**.
Listing 11.2, which accesses memory a word at a time with each word Listing 11.2, which accesses memory a word at a time with each word
starting at an odd address, runs on a 10 MHz AT clone in 1.27 ms per starting at an odd address, runs on a 10 MHz AT clone in 1.27 ms per
repetition of **MOVSW**, or 0.64 ms per word-sized memory access. That’s repetition of **MOVSW**, or 0.64 ms per word-sized memory access. That's
6-plus cycles per word-sized access, which breaks down to two separate 6-plus cycles per word-sized access, which breaks down to two separate
memory accesses—3 cycles to access the high byte of each word and 3 memory accesses—3 cycles to access the high byte of each word and 3
cycles to access the low byte of each word, the inevitable result of cycles to access the low byte of each word, the inevitable result of
@ -115,7 +115,7 @@ refresh.
On the other hand, Listing 11.3, which is exactly the same as Listing On the other hand, Listing 11.3, which is exactly the same as Listing
11.2 save that the memory accesses are word-aligned (start at even 11.2 save that the memory accesses are word-aligned (start at even
addresses), runs in 0.64 ms per repetition of **MOVSW**, or 0.32 µs per addresses), runs in 0.64 ms per repetition of **MOVSW**, or 0.32 µs per
word-sized memory access. That’s 3 cycles per word-sized access—exactly word-sized memory access. That's 3 cycles per word-sized access—exactly
twice as fast as the non-word-aligned accesses of Listing 11.2, just as twice as fast as the non-word-aligned accesses of Listing 11.2, just as
we predicted. we predicted.
@ -142,7 +142,7 @@ The data alignment cycle-eater has intriguing implications for speeding
up 286/386 code. The expenditure of a little care and a few bytes to up 286/386 code. The expenditure of a little care and a few bytes to
make sure that word-sized variables and memory blocks are word-aligned make sure that word-sized variables and memory blocks are word-aligned
can literally double the performance of certain code running on the 286. can literally double the performance of certain code running on the 286.
Even if it doesn’t double performance, word alignment usually helps and Even if it doesn't double performance, word alignment usually helps and
never hurts. never hurts.
#### Code Alignment {#Heading8} #### Code Alignment {#Heading8}
@ -150,11 +150,11 @@ never hurts.
Lack of word alignment can also interfere with instruction fetching on Lack of word alignment can also interfere with instruction fetching on
the 286, although not to the extent that it interferes with access to the 286, although not to the extent that it interferes with access to
word-sized memory variables. The 286 prefetches instructions a word at a word-sized memory variables. The 286 prefetches instructions a word at a
time; even if a given instruction doesn’t begin at an even address, the time; even if a given instruction doesn't begin at an even address, the
286 simply fetches the first byte of that instruction at the same time 286 simply fetches the first byte of that instruction at the same time
that it fetches the last byte of the previous instruction, as shown in that it fetches the last byte of the previous instruction, as shown in
Figure 11.2, then separates the bytes internally. That means that in Figure 11.2, then separates the bytes internally. That means that in
most cases, instructions run just as fast whether they’re word-aligned most cases, instructions run just as fast whether they're word-aligned
or not. or not.
There is, however, a non-word-alignment penalty on *branches* to odd There is, however, a non-word-alignment penalty on *branches* to odd
@ -163,7 +163,7 @@ addresses. On a branch to an odd address, the 286 is only able to fetch
shown in Figure 11.3. In other words, lack of word alignment of the shown in Figure 11.3. In other words, lack of word alignment of the
target instruction for any branch effectively cuts the target instruction for any branch effectively cuts the
instruction-fetching power of the 286 in half for the first instruction instruction-fetching power of the 286 in half for the first instruction
fetch after that branch. While that may not sound like much, you’d be fetch after that branch. While that may not sound like much, you'd be
surprised at what it can do to tight loops; in fact, a brief story is in surprised at what it can do to tight loops; in fact, a brief story is in
order. order.

View file

@ -41,14 +41,14 @@ cycles per loop:
call ZTimerOff call ZTimerOff
While word-aligning branch destinations can improve branching While word-aligning branch destinations can improve branching
performance, it’s a nuisance and can increase code size a good deal, so 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** 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 instruction if necessary, and the time required to execute a **NOP** can
sometimes cancel the performance advantage of having a word-aligned sometimes cancel the performance advantage of having a word-aligned
branch destination. branch destination.
------------------- ----------------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Consequently, it’s best to word-align only those branch destinations that can be reached solely by branching.* ![](images/i.jpg) *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 I recommend that you only go out of your way to word-align the start
@ -65,16 +65,16 @@ time-critical loops.
#### Alignment and the 386 {#Heading9 align="center"} #### Alignment and the 386 {#Heading9 align="center"}
So far we’ve only discussed alignment as it pertains to the 286. What, So far we've only discussed alignment as it pertains to the 286. What,
you may well ask, of the 386? you may well ask, of the 386?
The 386 adds the issue of *doubleword* alignment (that is, alignment to The 386 adds the issue of *doubleword* alignment (that is, alignment to
addresses that are multiples of four.) The rule for the 386 is: addresses that are multiples of four.) The rule for the 386 is:
Word-sized memory accesses should be word-aligned (it’s impossible for Word-sized memory accesses should be word-aligned (it's impossible for
word-aligned word-sized accesses to cross doubleword boundaries), and word-aligned word-sized accesses to cross doubleword boundaries), and
doubleword-sized memory accesses should be doubleword-aligned. However, doubleword-sized memory accesses should be doubleword-aligned. However,
in real (as opposed to 32-bit protected) mode, doubleword-sized memory in real (as opposed to 32-bit protected) mode, doubleword-sized memory
accesses are rare, so the simple word-alignment rule we’ve developed for accesses are rare, so the simple word-alignment rule we've developed for
the 286 serves for the 386 in real mode as well. the 286 serves for the 386 in real mode as well.
As for code alignment...the subroutine-start word-alignment rule of the As for code alignment...the subroutine-start word-alignment rule of the
@ -99,7 +99,7 @@ the mouse. I know of a Forth programmer who vastly improved the
performance of a complex application on the AT simply by forcing the performance of a complex application on the AT simply by forcing the
Forth interpreter to maintain an even stack pointer at all times. Forth interpreter to maintain an even stack pointer at all times.
An interesting corollary to this rule is that you shouldn’t **INC SP** 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**. 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 The stack pointer is odd between the first and second **INC**, so any
interrupt occurring between the two instructions will be serviced more interrupt occurring between the two instructions will be serviced more
@ -112,7 +112,7 @@ slowly than it normally would. The same goes for decrementing twice; use
#### The DRAM Refresh Cycle-Eater: Still an Act of God {#Heading11 align="center"} #### The DRAM Refresh Cycle-Eater: Still an Act of God {#Heading11 align="center"}
The DRAM refresh cycle-eater is the cycle-eater that’s least changed 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 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 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 than it uses in the PC, but in the same ballpark. While the DRAM refresh
@ -123,16 +123,16 @@ generally suffer less from DRAM refresh), the 5 percent figure is a good
rule of thumb. rule of thumb.
Basically, the effect of the DRAM refresh cycle-eater is pretty much the Basically, the effect of the DRAM refresh cycle-eater is pretty much the
same throughout the PC-compatible world: fairly small, so it doesn’t same throughout the PC-compatible world: fairly small, so it doesn't
greatly affect performance; unavoidable, so there’s no point in worrying greatly affect performance; unavoidable, so there's no point in worrying
about it anyway; and a nuisance since it results in fractional cycle about it anyway; and a nuisance since it results in fractional cycle
counts when using the Zen timer. Just as with the PC, a given code counts when using the Zen timer. Just as with the PC, a given code
sequence on the AT can execute at varying speeds at different times as a sequence on the AT can execute at varying speeds at different times as a
result of the interaction between the code and DRAM refresh. result of the interaction between the code and DRAM refresh.
There’s nothing much new with DRAM refresh on 286/386 computers, then. There's nothing much new with DRAM refresh on 286/386 computers, then.
Be aware of it, but don’t overly concern yourself—DRAM refresh is still Be aware of it, but don't overly concern yourself—DRAM refresh is still
an act of God, and there’s not a blessed thing you can do about it. an act of God, and there's not a blessed thing you can do about it.
Happily, the internal caches of the 486 and Pentium make DRAM refresh Happily, the internal caches of the 486 and Pentium make DRAM refresh
largely a performance non-issue on those processors. largely a performance non-issue on those processors.
@ -140,8 +140,8 @@ largely a performance non-issue on those processors.
Finally we come to the last of the cycle-eaters, the display adapter Finally we come to the last of the cycle-eaters, the display adapter
cycle-eater. There are two ways of looking at this cycle-eater on cycle-eater. There are two ways of looking at this cycle-eater on
286/386 computers: (1) It’s much worse than it was on the PC, or (2) 286/386 computers: (1) It's much worse than it was on the PC, or (2)
it’s just about the same as it was on the PC. it's just about the same as it was on the PC.
Either way, the display adapter cycle-eater is extremely bad news on Either way, the display adapter cycle-eater is extremely bad news on
286/386 computers and on 486s and Pentiums as well. In fact, this 286/386 computers and on 486s and Pentiums as well. In fact, this
@ -149,14 +149,14 @@ cycle-eater on those systems is largely responsible for the popularity
of VESA local bus (VLB). of VESA local bus (VLB).
The two ways of looking at the display adapter cycle-eater on 286/386 The two ways of looking at the display adapter cycle-eater on 286/386
computers are actually the same. As you’ll recall from my earlier computers are actually the same. As you'll recall from my earlier
discussion of the matter in Chapter 4, display adapters offer only a discussion of the matter in Chapter 4, display adapters offer only a
limited number of accesses to display memory during any given period of limited number of accesses to display memory during any given period of
time. The 8088 is capable of making use of most but not all of those time. The 8088 is capable of making use of most but not all of those
slots with **REP MOVSW**, so the number of memory accesses allowed by a slots with **REP MOVSW**, so the number of memory accesses allowed by a
display adapter such as a standard VGA is reasonably well-matched to an display adapter such as a standard VGA is reasonably well-matched to an
8088’s memory access speed. Granted, access to a VGA slows the 8088 down 8088's memory access speed. Granted, access to a VGA slows the 8088 down
considerably—but, as we’re about to find out, “considerably” is a considerably—but, as we're about to find out, "considerably" is a
relative term. What a VGA does to PC performance is nothing compared to relative term. What a VGA does to PC performance is nothing compared to
what it does to faster computers. what it does to faster computers.

View file

@ -10,8 +10,8 @@ anything but ideal for a 286. For one thing, most display adapters are
8-bit devices, although newer adapters are 16-bit in nature. One 8-bit devices, although newer adapters are 16-bit in nature. One
consequence of that is that only 1 byte can be read or written per consequence of that is that only 1 byte can be read or written per
access to display memory; word-sized accesses to 8-bit devices are access to display memory; word-sized accesses to 8-bit devices are
automatically split into 2 separate byte-sized accesses by the AT’s bus. automatically split into 2 separate byte-sized accesses by the AT's bus.
Another consequence is that accesses are simply slower; the AT’s bus Another consequence is that accesses are simply slower; the AT's bus
inserts additional wait states on accesses to 8-bit devices since it inserts additional wait states on accesses to 8-bit devices since it
must assume that such devices were designed for PCs and may not run must assume that such devices were designed for PCs and may not run
reliably at AT speeds. reliably at AT speeds.
@ -25,9 +25,9 @@ it this way: If **REP MOVSW** on a PC can use more than half of all
available accesses to display memory, then how much faster can code available accesses to display memory, then how much faster can code
running on a 286 or 386 possibly run when accessing display memory? running on a 286 or 386 possibly run when accessing display memory?
That’s right—less than twice as fast. That's right—less than twice as fast.
In other words, instructions that access display memory won’t run a 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 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 explains one of the two viewpoints expressed at the beginning of this
section: The display adapter cycle-eater is just about the same on section: The display adapter cycle-eater is just about the same on
@ -40,7 +40,7 @@ performance of instructions that access display memory to the *maximum*
performance of those instructions. Instructions that access display performance of those instructions. Instructions that access display
memory receive many more wait states when running on a 286 than they do 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 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 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 memory is determined not by processor speed but by the display adapter
itself. As a result, both processors are actually allowed just about the 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 same maximum number of accesses to display memory in any given time. By
@ -50,19 +50,19 @@ the 8088.
And that explains the second viewpoint expressed above regarding the And that explains the second viewpoint expressed above regarding the
display adapter cycle-eater vis-a-vis the 286 and 386. The display 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 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 indeed much worse on AT-class computers than it is on the PC, and it's
worse still on more powerful computers. worse still on more powerful computers.
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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, 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, no matter how fast the processor is. In fact, the faster the processor,
the more the display adapter cycle-eater hurts the performance of the more the display adapter cycle-eater hurts the performance of
instructions that access display memory. The display adapter cycle-eater instructions that access display memory. The display adapter cycle-eater
is not only still present in 286/386 computers, it’s worse than ever. is not only still present in 286/386 computers, it's worse than ever.
What can we do about this new, more virulent form of the display adapter What can we do about this new, more virulent form of the display adapter
cycle-eater? The workaround is the same as it was on the PC: Access cycle-eater? The workaround is the same as it was on the PC: Access
@ -73,15 +73,15 @@ display memory as little as you possibly can.
The 286 and 386 offer a number of new instructions. The 286 has a The 286 and 386 offer a number of new instructions. The 286 has a
relatively small number of instructions that the 8088 lacks, while the relatively small number of instructions that the 8088 lacks, while the
386 has those instructions and quite a few more, along with new 386 has those instructions and quite a few more, along with new
addressing modes and data sizes. We’ll discuss the 286 and the 386 addressing modes and data sizes. We'll discuss the 286 and the 386
separately in this regard. separately in this regard.
The 286 has a number of instructions designed for protected-mode The 286 has a number of instructions designed for protected-mode
operations. As I’ve said, we’re not going to discuss protected mode in operations. As I've said, we're not going to discuss protected mode in
this book; in any case, protected-mode instructions are generally used this book; in any case, protected-mode instructions are generally used
only by operating systems. (I should mention that the 286’s protected only by operating systems. (I should mention that the 286's protected
mode brings with it the ability to address 16 MB of memory, a mode brings with it the ability to address 16 MB of memory, a
considerable improvement over the 8088’s 1 MB. In real mode, however, considerable improvement over the 8088's 1 MB. In real mode, however,
programs are still limited to 1 MB of addressable memory on the 286. In programs are still limited to 1 MB of addressable memory on the 286. In
either mode, each segment is still limited to 64K.) either mode, each segment is still limited to 64K.)
@ -104,7 +104,7 @@ constant number of bits.
#### New Instructions and Features: The 386 {#Heading14} #### New Instructions and Features: The 386 {#Heading14}
The 386 is somewhat more complex than the 286 regarding new features. The 386 is somewhat more complex than the 286 regarding new features.
Once again, we won’t discuss protected mode, which on the 386 comes with Once again, we won't discuss protected mode, which on the 386 comes with
the ability to address up to 4 gigabytes per segment and 64 terabytes in the ability to address up to 4 gigabytes per segment and 64 terabytes in
all. In real mode (and in virtual-86 mode, which allows the 386 to all. In real mode (and in virtual-86 mode, which allows the 386 to
multitask MS-DOS applications, and which is identical to real mode so multitask MS-DOS applications, and which is identical to real mode so
@ -112,10 +112,10 @@ far as MS-DOS programs are concerned), programs running on the 386 are
still limited to 1 MB of addressable memory and 64K per segment. still limited to 1 MB of addressable memory and 64K per segment.
The 386 has many new instructions, as well as new registers, addressing The 386 has many new instructions, as well as new registers, addressing
modes and data sizes that have trickled down from protected mode. Let’s modes and data sizes that have trickled down from protected mode. Let's
take a quick look at these new real-mode features. take a quick look at these new real-mode features.
Even in real mode, it’s possible to access many of the 386’s new and Even in real mode, it's possible to access many of the 386's new and
extended registers. Most of these registers are simply 32-bit extensions extended registers. Most of these registers are simply 32-bit extensions
of the 16-bit registers of the 8088. For example, EAX is a 32-bit of the 16-bit registers of the 8088. For example, EAX is a 32-bit
register containing AX as its lower 16 bits, EBX is a 32-bit register register containing AX as its lower 16 bits, EBX is a 32-bit register
@ -125,15 +125,15 @@ segment registers: FS and GS.
The 386 also comes with a slew of new real-mode instructions beyond The 386 also comes with a slew of new real-mode instructions beyond
those supported by the 8088 and 286. These instructions can scan data on those supported by the 8088 and 286. These instructions can scan data on
a bit-by-bit basis, set the Carry flag to the value of a specified bit, a bit-by-bit basis, set the Carry flag to the value of a specified bit,
sign-extend or zero-extend data as it’s moved, set a register or memory sign-extend or zero-extend data as it's moved, set a register or memory
variable to 1 or 0 on the basis of any of the conditions that can be variable to 1 or 0 on the basis of any of the conditions that can be
tested with conditional jumps, and more. (Again, beware: Many of these tested with conditional jumps, and more. (Again, beware: Many of these
complex 386-specific instructions are slower than equivalent sequences complex 386-specific instructions are slower than equivalent sequences
of simple instructions on the 486 and especially on the Pentium.) What’s of simple instructions on the 486 and especially on the Pentium.) What's
more, both old and new instructions support 32-bit operations on the more, both old and new instructions support 32-bit operations on the
386. For example, it’s relatively simple to copy data in chunks of 4 386. For example, it's relatively simple to copy data in chunks of 4
bytes on a 386, even in real mode, by using the **MOVSD** (“move string bytes on a 386, even in real mode, by using the **MOVSD** ("move string
double”) instruction, or to negate a 32-bit value with **NEG eax**. double") instruction, or to negate a 32-bit value with **NEG eax**.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](11-04.html) [Table of Contents](index.html) [Next](11-06.html) [Previous](11-04.html) [Table of Contents](index.html) [Next](11-06.html)

View file

@ -2,32 +2,32 @@
[Previous](11-05.html) [Table of Contents](index.html) [Next](11-07.html) [Previous](11-05.html) [Table of Contents](index.html) [Next](11-07.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
Finally, it’s possible in real mode to use the 386’s new addressing 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 modes, in which *any* 32-bit general-purpose register or pair of
registers can be used to address memory. What’s more, multiplication 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, memory-addressing registers by 2, 4, or 8 for look-ups in word,
doubleword, or quadword tables can be built right into the memory doubleword, or quadword tables can be built right into the memory
addressing mode. (The 32-bit addressing modes are discussed further in addressing mode. (The 32-bit addressing modes are discussed further in
later chapters.) In protected mode, these new addressing modes allow you 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 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, limited to 64K, even with 32-bit registers and the new addressing modes,
unless you play some unorthodox tricks with the segment registers. unless you play some unorthodox tricks with the segment registers.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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... {#Heading15 align="center"} #### Optimization Rules: The More Things Change... {#Heading15 align="center"}
Let’s see what we’ve learned about 286/386 optimization. Mostly what 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 we've learned is that our familiar PC cycle-eaters still apply, although
in somewhat different forms, and that the major optimization rules for 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 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 any of these computers if you keep your instructions short, use the
registers heavily and avoid memory, don’t branch, and avoid accessing registers heavily and avoid memory, don't branch, and avoid accessing
display memory like the plague. display memory like the plague.
Although we haven’t touched on them, repeated string instructions are Although we haven't touched on them, repeated string instructions are
still desirable on the 286 and 386 since they provide a great deal of still desirable on the 286 and 386 since they provide a great deal of
functionality per instruction byte and eliminate both the prefetch queue functionality per instruction byte and eliminate both the prefetch queue
cycle-eater and branching. However, string instructions are not quite so cycle-eater and branching. However, string instructions are not quite so
@ -35,8 +35,8 @@ spectacularly superior on the 286 and 386 as they are on the 8088 since
non-string memory-accessing instructions have been speeded up non-string memory-accessing instructions have been speeded up
considerably on the newer processors. considerably on the newer processors.
There’s one cycle-eater with new implications on the 286 and 386, and There's one cycle-eater with new implications on the 286 and 386, and
that’s the data alignment cycle-eater. From the data alignment that's the data alignment cycle-eater. From the data alignment
cycle-eater we get a new rule: Word-align your word-sized variables, and cycle-eater we get a new rule: Word-align your word-sized variables, and
start your subroutines at even addresses. start your subroutines at even addresses.
@ -58,8 +58,8 @@ calculation time. Where an 8088 takes from 5 to 12 cycles to calculate
an EA, a 286 or 386 usually takes no time whatsoever to perform the an EA, a 286 or 386 usually takes no time whatsoever to perform the
calculation. If a base+index+displacement addressing mode, such as **MOV calculation. If a base+index+displacement addressing mode, such as **MOV
AX,[WordArray+bx+si]**, is used on a 286 or 386, 1 cycle is taken to AX,[WordArray+bx+si]**, is used on a 286 or 386, 1 cycle is taken to
perform the EA calculation, but that’s both the worst case and the only perform the EA calculation, but that's both the worst case and the only
case in which there’s any EA overhead at all. case in which there's any EA overhead at all.
The elimination of EA calculation time means that the EU execution time The elimination of EA calculation time means that the EU execution time
of memory-addressing instructions is much closer to the EU execution of memory-addressing instructions is much closer to the EU execution
@ -69,10 +69,10 @@ time of register-only instructions. For instance, on the 8088 **ADD
**ADD [WordVar],100H** is a 7-cycle instruction, while **ADD DX,100H** **ADD [WordVar],100H** is a 7-cycle instruction, while **ADD DX,100H**
is a 3-cycle instruction—a ratio of just 2.3 to 1. is a 3-cycle instruction—a ratio of just 2.3 to 1.
It would seem, then, that it’s less necessary to use the registers on It would seem, then, that it's less necessary to use the registers on
the 286 than it was on the 8088, but that’s simply not the case, for the 286 than it was on the 8088, but that's simply not the case, for
reasons we’ve already seen. The key is this: The 286 can execute reasons we've already seen. The key is this: The 286 can execute
memory-addressing instructions so fast that there’s no spare instruction memory-addressing instructions so fast that there's no spare instruction
prefetching time during those instructions, so the prefetch queue runs prefetching time during those instructions, so the prefetch queue runs
dry, especially on the AT, with its one-wait-state memory. On the AT, dry, especially on the AT, with its one-wait-state memory. On the AT,
the 6-byte instruction **ADD [WordVar],100H** is effectively at least a the 6-byte instruction **ADD [WordVar],100H** is effectively at least a
@ -82,15 +82,15 @@ and write the result back to memory.
Granted, the register-only instruction **ADD DX,100H** also slows Granted, the register-only instruction **ADD DX,100H** also slows
down—to 6 cycles—because of instruction prefetching, leaving a ratio of down—to 6 cycles—because of instruction prefetching, leaving a ratio of
2.5 to 1. Now, however, let’s look at the performance of the same code 2.5 to 1. Now, however, let's look at the performance of the same code
on an 8088. The register-only code would run in 16 cycles (4 instruction on an 8088. The register-only code would run in 16 cycles (4 instruction
bytes at 4 cycles per byte), while the memory-accessing code would run bytes at 4 cycles per byte), while the memory-accessing code would run
in 40 cycles (6 instruction bytes at 4 cycles per byte, plus 2 in 40 cycles (6 instruction bytes at 4 cycles per byte, plus 2
word-sized memory accesses at 8 cycles per word). That’s a ratio of 2.5 word-sized memory accesses at 8 cycles per word). That's a ratio of 2.5
to 1, *exactly the same as on the 286*. to 1, *exactly the same as on the 286*.
This is all theoretical. We put our trust not in theory but in actual This is all theoretical. We put our trust not in theory but in actual
performance, so let’s run this code through the Zen timer. On a PC, performance, so let's run this code through the Zen timer. On a PC,
Listing 11.4, which performs register-only addition, runs in 3.62 ms, Listing 11.4, which performs register-only addition, runs in 3.62 ms,
while Listing 11.5, which performs addition to a memory variable, runs while Listing 11.5, which performs addition to a memory variable, runs
in 10.05 ms. On a 10 MHz AT clone, Listing 11.4 runs in 0.64 ms, while in 10.05 ms. On a 10 MHz AT clone, Listing 11.4 runs in 0.64 ms, while

View file

@ -24,7 +24,7 @@
endm endm
call ZTimerOff call ZTimerOff
What’s going on? Simply this: Instruction fetching is controlling What's going on? Simply this: Instruction fetching is controlling
overall execution time on *both* processors. Both the 8088 in a PC and overall execution time on *both* processors. Both the 8088 in a PC and
the 286 in an AT can execute the bytes of the instructions in Listings the 286 in an AT can execute the bytes of the instructions in Listings
11.4 and 11.5 faster than they can be fetched. Since the instructions 11.4 and 11.5 faster than they can be fetched. Since the instructions
@ -44,13 +44,13 @@ memory-accessing instructions on the 286 and 386 are much faster
Given the system wait states prevalent on 286 and 386 computers, Given the system wait states prevalent on 286 and 386 computers,
however, the prefetch queue is likely to be empty quite a bit, however, the prefetch queue is likely to be empty quite a bit,
especially when code consisting of instructions with short EU execution especially when code consisting of instructions with short EU execution
times is executed. Of course, that’s just the sort of code we’re likely times is executed. Of course, that's just the sort of code we're likely
to write when we’re optimizing, so the performance of high-speed code is to write when we're optimizing, so the performance of high-speed code is
more likely to be controlled by instruction size than by EU execution more likely to be controlled by instruction size than by EU execution
time on most 286 and 386 computers, just as it is on the PC. time on most 286 and 386 computers, just as it is on the PC.
All of which is just a way of saying that faster memory access and EA All of which is just a way of saying that faster memory access and EA
calculation notwithstanding, it’s just as desirable to keep instructions calculation notwithstanding, it's just as desirable to keep instructions
short and memory accesses to a minimum on the 286 and 386 as it is on short and memory accesses to a minimum on the 286 and 386 as it is on
the 8088. And the way to do that is to use the registers as heavily as the 8088. And the way to do that is to use the registers as heavily as
possible, use string instructions, use short forms of instructions, and possible, use string instructions, use short forms of instructions, and
@ -60,45 +60,45 @@ The more things change, the more they remain the same....
#### POPF and the 286 {#Heading17} #### POPF and the 286 {#Heading17}
We’ve one final 286-related item to discuss: the hardware malfunction of We've one final 286-related item to discuss: the hardware malfunction of
**POPF** under certain circumstances on the 286. **POPF** under certain circumstances on the 286.
The problem is this: Sometimes **POPF** permits interrupts to occur when The problem is this: Sometimes **POPF** permits interrupts to occur when
interrupts are initially off and the setting popped into the Interrupt interrupts are initially off and the setting popped into the Interrupt
flag from the stack keeps interrupts off. In other words, an interrupt flag from the stack keeps interrupts off. In other words, an interrupt
can happen even though the Interrupt flag is never set to 1. Now, I can happen even though the Interrupt flag is never set to 1. Now, I
don’t want to blow this particular bug out of proportion. It only causes don't want to blow this particular bug out of proportion. It only causes
problems in code that cannot tolerate interrupts under any problems in code that cannot tolerate interrupts under any
circumstances, and that’s a rare sort of code, especially in user circumstances, and that's a rare sort of code, especially in user
programs. However, some code really does need to have interrupts programs. However, some code really does need to have interrupts
absolutely disabled, with no chance of an interrupt sneaking through. absolutely disabled, with no chance of an interrupt sneaking through.
For example, a critical portion of a disk BIOS might need to retrieve For example, a critical portion of a disk BIOS might need to retrieve
data from the disk controller the instant it becomes available; even a data from the disk controller the instant it becomes available; even a
few hundred microseconds of delay could result in a sector’s worth of few hundred microseconds of delay could result in a sector's worth of
data misread. In this case, one misplaced interrupt during a **POPF** data misread. In this case, one misplaced interrupt during a **POPF**
could result in a trashed hard disk if that interrupt occurs while the could result in a trashed hard disk if that interrupt occurs while the
disk BIOS is reading a sector of the File Allocation Table. disk BIOS is reading a sector of the File Allocation Table.
There is a workaround for the **POPF** bug. While the workaround is easy There is a workaround for the **POPF** bug. While the workaround is easy
to use, it’s considerably slower than **POPF**, and costs a few bytes as to use, it's considerably slower than **POPF**, and costs a few bytes as
well, so you won’t want to use it in code that can tolerate interrupts. well, so you won't want to use it in code that can tolerate interrupts.
On the other hand, in code that truly cannot be interrupted, you should On the other hand, in code that truly cannot be interrupted, you should
view those extra cycles and bytes as cheap insurance against mysterious view those extra cycles and bytes as cheap insurance against mysterious
and erratic program crashes. and erratic program crashes.
One obvious reason to discuss the **POPF** workaround is that it’s One obvious reason to discuss the **POPF** workaround is that it's
useful. Another reason is that the workaround is an excellent example of useful. Another reason is that the workaround is an excellent example of
Zen-level assembly coding, in that there’s a well-defined goal to be Zen-level assembly coding, in that there's a well-defined goal to be
achieved but no obvious way to do so. The goal is to reproduce the achieved but no obvious way to do so. The goal is to reproduce the
functionality of the **POPF** instruction without using **POPF**, and functionality of the **POPF** instruction without using **POPF**, and
the place to start is by asking exactly what **POPF** does. the place to start is by asking exactly what **POPF** does.
All **POPF** does is pop the word on top of the stack into the FLAGS All **POPF** does is pop the word on top of the stack into the FLAGS
register, as shown in Figure 11.4. How can we do that without **POPF**? register, as shown in Figure 11.4. How can we do that without **POPF**?
Of course, the 286’s designers intended us to use **POPF** for this Of course, the 286's designers intended us to use **POPF** for this
purpose, and didn’t intentionally provide any alternative approach, so purpose, and didn't intentionally provide any alternative approach, so
we’ll have to devise an alternative approach of our own. To do that, we'll have to devise an alternative approach of our own. To do that,
we’ll have to search for instructions that contain some of the same we'll have to search for instructions that contain some of the same
functionality as **POPF**, in the hope that one of those instructions functionality as **POPF**, in the hope that one of those instructions
can be used in some way to replace **POPF**. can be used in some way to replace **POPF**.

View file

@ -2,10 +2,10 @@
[Previous](11-07.html) [Table of Contents](index.html) [Next](12-01.html) [Previous](11-07.html) [Table of Contents](index.html) [Next](12-01.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
Well, there’s only one instruction other than **POPF** that loads the Well, there's only one instruction other than **POPF** that loads the
FLAGS register directly from the stack, and that’s **IRET**, which loads FLAGS register directly from the stack, and that's **IRET**, which loads
the FLAGS register from the stack as it branches, as shown in Figure the FLAGS register from the stack as it branches, as shown in Figure
11.5. iret has no known bugs of the sort that plague **POPF**, so it’s 11.5. iret has no known bugs of the sort that plague **POPF**, so it's
certainly a candidate to replace popf in non-interruptible applications. certainly a candidate to replace popf in non-interruptible applications.
Unfortunately, **IRET** loads the FLAGS register with the *third* word Unfortunately, **IRET** loads the FLAGS register with the *third* word
down on the stack, not the word on top of the stack, as is the case with down on the stack, not the word on top of the stack, as is the case with
@ -14,13 +14,13 @@ between the top of the stack and the word popped into the FLAGS
register. register.
Obviously, the segment:offset that **IRET** expects to find on the stack Obviously, the segment:offset that **IRET** expects to find on the stack
above the pushed flags isn’t present when the stack is set up for above the pushed flags isn't present when the stack is set up for
**POPF**, so we’ll have to adjust the stack a bit before we can **POPF**, so we'll have to adjust the stack a bit before we can
substitute **IRET** for **POPF**. What we’ll have to do is push the substitute **IRET** for **POPF**. What we'll have to do is push the
segment:offset of the instruction after our workaround code onto the segment:offset of the instruction after our workaround code onto the
stack right above the pushed flags. **IRET** will then branch to that stack right above the pushed flags. **IRET** will then branch to that
address and pop the flags, ending up at the instruction after the address and pop the flags, ending up at the instruction after the
workaround code with the flags popped. That’s just the result that would workaround code with the flags popped. That's just the result that would
have occurred had we executed **POPF**—WITH the bonus that no interrupts have occurred had we executed **POPF**—WITH the bonus that no interrupts
can accidentally occur when the Interrupt flag is 0 both before and can accidentally occur when the Interrupt flag is 0 both before and
after the pop. after the pop.
@ -32,7 +32,7 @@ How can we push the segment:offset of the next instruction? Well,
finding the offset of the next instruction by performing a near call to finding the offset of the next instruction by performing a near call to
that instruction is a tried-and-true trick. We can do something similar that instruction is a tried-and-true trick. We can do something similar
here, but in this case we need a far call, since **IRET** requires both here, but in this case we need a far call, since **IRET** requires both
a segment and an offset. We’ll also branch backward so that the address a segment and an offset. We'll also branch backward so that the address
pushed on the stack will point to the instruction we want to continue pushed on the stack will point to the instruction we want to continue
with. The code works out like this: with. The code works out like this:
@ -72,7 +72,7 @@ shrinking the workaround code by 1 byte:
call popfiret call popfiret
endm endm
By the way, the flags can be popped much more quickly if you’re willing By the way, the flags can be popped much more quickly if you're willing
to alter a register in the process. For example, the following macro to alter a register in the process. For example, the following macro
emulates **POPF** with just one branch, but wipes out AX: emulates **POPF** with just one branch, but wipes out AX:
@ -83,11 +83,11 @@ emulates **POPF** with just one branch, but wipes out AX:
iret iret
endm endm
It’s not a perfect substitute for **POPF**, since **POPF** doesn’t alter It's not a perfect substitute for **POPF**, since **POPF** doesn't alter
any registers, but it’s faster and shorter than **EMULATE\_POPF** when any registers, but it's faster and shorter than **EMULATE\_POPF** when
you can spare the register. If you’re using 286-specific instructions, you can spare the register. If you're using 286-specific instructions,
you can use which is shorter still, alters no registers, and branches you can use which is shorter still, alters no registers, and branches
just once. (Of course, this version of **EMULATE\_POPF** won’t work on just once. (Of course, this version of **EMULATE\_POPF** won't work on
an 8088.) an 8088.)
.286 .286
@ -102,16 +102,16 @@ an 8088.)
**Figure 11.6**  *Workaround code for the POPF bug.* **Figure 11.6**  *Workaround code for the POPF bug.*
The standard version of **EMULATE\_POPF** is 6 bytes longer than The standard version of **EMULATE\_POPF** is 6 bytes longer than
**POPF** and much slower, as you’d expect given that it involves three **POPF** and much slower, as you'd expect given that it involves three
branches. Anyone in his/her right mind would prefer **POPF** to a branches. Anyone in his/her right mind would prefer **POPF** to a
larger, slower, three-branch macro—given a choice. In noncode, however, larger, slower, three-branch macro—given a choice. In noncode, however,
there’s no choice here; the safer—if slower—approach is the best. there's no choice here; the safer—if slower—approach is the best.
(Having people associate your programs with crashed computers is *not* a (Having people associate your programs with crashed computers is *not* a
desirable situation, no matter how unfair the circumstances under which desirable situation, no matter how unfair the circumstances under which
it occurs.) it occurs.)
And now you know the nature of and the workaround for the **POPF** bug. And now you know the nature of and the workaround for the **POPF** bug.
Whether you ever need the workaround or not, it’s a neatly packaged Whether you ever need the workaround or not, it's a neatly packaged
example of the tremendous flexibility of the x86 instruction set. example of the tremendous flexibility of the x86 instruction set.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -6,43 +6,43 @@ Chapter 12\
Pushing the 486 {#Heading1} Pushing the 486 {#Heading1}
---------------- ----------------
### It’s Not Just a Bigger 386 {#Heading2} ### It's Not Just a Bigger 386 {#Heading2}
So this traveling salesman is walking down a road, and he sees a group So this traveling salesman is walking down a road, and he sees a group
of men digging a ditch with their bare hands. “Whoa, there!” he says. of men digging a ditch with their bare hands. "Whoa, there!" he says.
“What you guys need is a Model 8088 ditch digger!” And he whips out a "What you guys need is a Model 8088 ditch digger!" And he whips out a
trowel and sells it to them. trowel and sells it to them.
A few days later, he stops back around. They’re happy with the trowel, A few days later, he stops back around. They're happy with the trowel,
but he sells them the latest ditch-digging technology, the Model 80286 but he sells them the latest ditch-digging technology, the Model 80286
spade. That keeps them content until he stops by again with a Model spade. That keeps them content until he stops by again with a Model
80386 shovel (a full 32 inches wide, with a narrow point to emulate the 80386 shovel (a full 32 inches wide, with a narrow point to emulate the
trowel), and *that* holds them until he comes back around with what they trowel), and *that* holds them until he comes back around with what they
really need: a Model 80486 bulldozer. really need: a Model 80486 bulldozer.
Having reached the top of the line, the salesman doesn’t pay them a call Having reached the top of the line, the salesman doesn't pay them a call
for a while. When he does, not only are they none too friendly, but for a while. When he does, not only are they none too friendly, but
they’re digging with the 80386 shovel; the bulldozer is sitting off to they're digging with the 80386 shovel; the bulldozer is sitting off to
one side. “Why on earth are you using that shovel?” the salesman asks. one side. "Why on earth are you using that shovel?" the salesman asks.
“Why aren’t you digging with the bulldozer?” "Why aren't you digging with the bulldozer?"
“Well, Lord knows we tried,” says the foreman, “but it was all we could "Well, Lord knows we tried," says the foreman, "but it was all we could
do just to lift the damn thing!” do just to lift the damn thing!"
Substitute “processor” for the various digging implements, and you get Substitute "processor" for the various digging implements, and you get
an idea of just how different the optimization rules for the 486 are an idea of just how different the optimization rules for the 486 are
from what you’re used to. Okay, it’s not quite *that* bad—but upon from what you're used to. Okay, it's not quite *that* bad—but upon
encountering a processor where string instructions are often to be encountering a processor where string instructions are often to be
avoided and memory-to-register **MOV**s are frequently as fast as avoided and memory-to-register **MOV**s are frequently as fast as
register-to-register **MOV**s, Dorothy was heard to exclaim (before she register-to-register **MOV**s, Dorothy was heard to exclaim (before she
sank out of sight in a swirl of hopelessly mixed metaphors), “I don’t sank out of sight in a swirl of hopelessly mixed metaphors), "I don't
think we’re in Kansas anymore, Toto.” think we're in Kansas anymore, Toto."
#### Enter the 486 {#Heading3} #### Enter the 486 {#Heading3}
No chip that is a direct, fully compatible descendant of the 8088, 286, No chip that is a direct, fully compatible descendant of the 8088, 286,
and 386 could ever be called a RISC chip, but the 486 certainly contains and 386 could ever be called a RISC chip, but the 486 certainly contains
RISC elements, and it’s those elements that are most responsible for RISC elements, and it's those elements that are most responsible for
making 486 optimization unique. Simple, common instructions are executed making 486 optimization unique. Simple, common instructions are executed
in a single cycle by a RISC-like core processor, but other instructions in a single cycle by a RISC-like core processor, but other instructions
are executed pretty much as they were on the 386, where every are executed pretty much as they were on the 386, where every
@ -57,23 +57,23 @@ sometimes startling: **FMUL** (floating point multiply) is usually
faster on the 486 than **IMUL** (integer multiply)! faster on the 486 than **IMUL** (integer multiply)!
An encyclopedic approach to 486 optimization would take a book all by 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 itself, so in this chapter I'm only going to hit the highlights of 486
optimization, touching on several optimization rules, some documented, optimization, touching on several optimization rules, some documented,
some not. You might also want to check out the following sources of 486 some not. You might also want to check out the following sources of 486
information: *i486 Microprocessor Programmer’s Reference Manual,* from information: *i486 Microprocessor Programmer's Reference Manual,* from
Intel; “8086 Optimization: Aim Down the Middle and Pray,” in the March, 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 1991 *Dr. Dobb's Journal*; and "Peak Performance: On to the 486," in the
November, 1990 *Programmer’s Journal.* November, 1990 *Programmer's Journal.*
### Rules to Optimize By {#Heading4} ### Rules to Optimize By {#Heading4}
In Appendix G of the *i486 Microprocessor Programmer*’*s* *Reference In Appendix G of the *i486 Microprocessor Programmer*'*s* *Reference
Manual*, Intel lists a number of optimization techniques for the 486. Manual*, Intel lists a number of optimization techniques for the 486.
While neither exhaustive (we’ll look at two undocumented optimizations While neither exhaustive (we'll look at two undocumented optimizations
shortly) nor entirely accurate (we’ll correct two of the rules here), 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 Intel's list is certainly a good starting point. In particular, the list
conveys the extent to which 486 optimization differs from optimization conveys the extent to which 486 optimization differs from optimization
for earlier x86 processors. Generally, I’ll be discussing optimization for earlier x86 processors. Generally, I'll be discussing optimization
for real mode (it being the most widely used mode at the moment), 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. although many of the rules should apply to protected mode as well.
@ -84,7 +84,7 @@ although many of the rules should apply to protected mode as well.
In other words, for cached code (which time-critical code almost always In other words, for cached code (which time-critical code almost always
is), performance is predictable and can be calculated with good is), performance is predictable and can be calculated with good
precision, and those calculations will apply on any 486. However, precision, and those calculations will apply on any 486. However,
“predictable” doesn’t mean “trivial”; the cycle times printed for the "predictable" doesn't mean "trivial"; the cycle times printed for the
various instructions are not the whole story. You must be aware of all various instructions are not the whole story. You must be aware of all
the rules, documented and undocumented, that go into calculating actual the rules, documented and undocumented, that go into calculating actual
execution times—and uncovering some of those rules is exactly what this execution times—and uncovering some of those rules is exactly what this
@ -95,15 +95,15 @@ chapter is about.
Rule \#1: Avoid indexed addressing (that is, try not to use either two Rule \#1: Avoid indexed addressing (that is, try not to use either two
registers or scaled addressing to point to memory). registers or scaled addressing to point to memory).
Intel cautions against using indexing to address memory because there’s Intel cautions against using indexing to address memory because there's
a one-cycle penalty for indexed addressing. True enough—but “indexed a one-cycle penalty for indexed addressing. True enough—but "indexed
addressing” might not mean what you expect. addressing" might not mean what you expect.
Traditionally, SI and DI are considered the index registers of the x86 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, CPUs. That is not the sense in which "indexed addressing" is meant here,
however. In real mode, indexed addressing means that two registers, however. In real mode, indexed addressing means that two registers,
rather than one or none, are used to point to memory. (In this context, 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 the use of one register to address memory is "base addressing," no
matter what register is used.) **MOV AX, [BX+DI]** and **MOV CL, matter what register is used.) **MOV AX, [BX+DI]** and **MOV CL,
[BP+SI+10]** perform indexed addressing; **MOV AX,[BX]** and **MOV DL, [BP+SI+10]** perform indexed addressing; **MOV AX,[BX]** and **MOV DL,
[SI+1]** do not. [SI+1]** do not.

View file

@ -18,7 +18,7 @@ calculations take a *minimum* of 5 cycles. On the 486, however, 1 cycle
is a big deal because many instructions, including most register-only is a big deal because many instructions, including most register-only
instructions (**MOV**, **ADD**, **CMP**, and so on) execute in just 1 instructions (**MOV**, **ADD**, **CMP**, and so on) execute in just 1
cycle. In particular, **MOV**s to and from memory execute in 1 cycle—if cycle. In particular, **MOV**s to and from memory execute in 1 cycle—if
they’re not hampered by something like indexed addressing, in which case they're not hampered by something like indexed addressing, in which case
they slow to half speed (or worse, as we will see shortly). they slow to half speed (or worse, as we will see shortly).
For example, consider the summing example shown earlier. The version For example, consider the summing example shown earlier. The version
@ -32,7 +32,7 @@ In a key loop on the 486, 1 cycle can indeed matter.
#### Calculate Memory Pointers Ahead of Time {#Heading6} #### Calculate Memory Pointers Ahead of Time {#Heading6}
Rule \#2: Don’t use a register as a memory pointer during the next two Rule \#2: Don't use a register as a memory pointer during the next two
cycles after loading it. cycles after loading it.
Intel states that if the destination of one instruction is used as the Intel states that if the destination of one instruction is used as the
@ -44,16 +44,16 @@ start of the instruction that will need it, as shown in Figure 12.1;
this effectively makes the address calculation time vanish, because it this effectively makes the address calculation time vanish, because it
happens while the preceding instruction executes. happens while the preceding instruction executes.
Of course, the 486 *can’t* perform an effective address calculation for Of course, the 486 *can't* perform an effective address calculation for
a target instruction ahead of time if one of the address components a target instruction ahead of time if one of the address components
isn’t known until the instruction starts, and that’s exactly the case isn't known until the instruction starts, and that's exactly the case
when the preceding instruction modifies one of the target instruction’s when the preceding instruction modifies one of the target instruction's
addressing registers. For example, in the code addressing registers. For example, in the code
MOV BX,OFFSET MemVar MOV BX,OFFSET MemVar
MOV AX,[BX] MOV AX,[BX]
there’s no way that the 486 can calculate the address referenced by there's no way that the 486 can calculate the address referenced by
**MOV AX,[BX]** until **MOV BX,OFFSET MemVar** finishes, so pipelining **MOV AX,[BX]** until **MOV BX,OFFSET MemVar** finishes, so pipelining
that calculation ahead of time is not possible. A good workaround is that calculation ahead of time is not possible. A good workaround is
rearranging your code so that at least one instruction lies between the rearranging your code so that at least one instruction lies between the
@ -78,7 +78,7 @@ 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 important comment: My observations indicate that for real-mode code, the
documentation understates the extent of the penalty for interrupting the documentation understates the extent of the penalty for interrupting the
address calculation pipeline by loading a memory pointer just before address calculation pipeline by loading a memory pointer just before
it’s used. it's used.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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!* ![](images/i.jpg) *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!*
@ -87,17 +87,17 @@ it’s used.
In 32-bit protected mode, however, the penalty is, in fact, the 1 cycle In 32-bit protected mode, however, the penalty is, in fact, the 1 cycle
that Intel . that Intel .
Considering that **MOV** normally takes only one cycle total, that’s Considering that **MOV** normally takes only one cycle total, that's
quite a loss. For example, the postdecrement loop shown above is 2 full quite a loss. For example, the postdecrement loop shown above is 2 full
cycles faster than the preincrement loop, resulting in a 29 percent cycles faster than the preincrement loop, resulting in a 29 percent
improvement in the performance of the entire loop. But wait, there’s improvement in the performance of the entire loop. But wait, there's
more. If a register is loaded 2 cycles (which generally means 2 more. If a register is loaded 2 cycles (which generally means 2
instructions, but, because some 486 instructions take more than 1 cycle, instructions, but, because some 486 instructions take more than 1 cycle,
![](images/12-01.jpg)\ ![](images/12-01.jpg)\
**Figure 12.1**  *One-cycle-ahead address pipelining.* **Figure 12.1**  *One-cycle-ahead address pipelining.*
the 2 are not always equivalent) before it’s used to point to memory, 1 the 2 are not always equivalent) before it's used to point to memory, 1
cycle is lost. Therefore, whereas this code cycle is lost. Therefore, whereas this code
mov bx,offset MemVar mov bx,offset MemVar
@ -123,16 +123,16 @@ loses only one cycle, and this code
mov ax,[bx] mov ax,[bx]
jnz LoopTop jnz LoopTop
loses no cycles at all. Apparently, the 486’s addressing calculation loses no cycles at all. Apparently, the 486's addressing calculation
pipeline actually starts 2 cycles ahead, as shown in Figure 12.2. (In pipeline actually starts 2 cycles ahead, as shown in Figure 12.2. (In
truth, my best guess at the moment is that the addressing pipeline truth, my best guess at the moment is that the addressing pipeline
really does start only 1 cycle ahead; the additional cycle crops up when really does start only 1 cycle ahead; the additional cycle crops up when
the addressing pipeline has to wait for a register to be written into the addressing pipeline has to wait for a register to be written into
the register file before it can read it out for use in addressing the register file before it can read it out for use in addressing
calculations. However, I’m guessing here, and the 2-cycle-ahead model in calculations. However, I'm guessing here, and the 2-cycle-ahead model in
Figure 12.2 will do just fine for optimization purposes.) Figure 12.2 will do just fine for optimization purposes.)
Clearly, there’s considerable optimization potential in careful Clearly, there's considerable optimization potential in careful
rearrangement of 486 code. rearrangement of 486 code.
![](images/12-02.jpg)\ ![](images/12-02.jpg)\
@ -140,17 +140,17 @@ rearrangement of 486 code.
### Caveat Programmor {#Heading7} ### Caveat Programmor {#Heading7}
A caution: I’m quite certain that the 2-cycle-ahead addressing pipeline A caution: I'm quite certain that the 2-cycle-ahead addressing pipeline
interruption penalty I’ve described exists in the two 486s I’ve tested. interruption penalty I've described exists in the two 486s I've tested.
However, there’s no guarantee that Intel won’t change this aspect of the However, there's no guarantee that Intel won't change this aspect of the
486 in the future, especially given that the documentation indicates 486 in the future, especially given that the documentation indicates
otherwise. Perhaps the 2-cycle penalty is the result of a bug in the otherwise. Perhaps the 2-cycle penalty is the result of a bug in the
initial steps of the 486, and will revert to the documented 1-cycle initial steps of the 486, and will revert to the documented 1-cycle
penalty someday; likewise for the undocumented optimizations I’ll penalty someday; likewise for the undocumented optimizations I'll
describe below. Nonetheless, none of the optimizations I suggest would describe below. Nonetheless, none of the optimizations I suggest would
hurt performance even if the undocumented performance characteristics of hurt performance even if the undocumented performance characteristics of
the 486 were to vanish, and they certainly will help performance on at the 486 were to vanish, and they certainly will help performance on at
least some 486s right now, so I feel they’re well worth using. least some 486s right now, so I feel they're well worth using.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](12-01.html) [Table of Contents](index.html) [Next](12-03.html) [Previous](12-01.html) [Table of Contents](index.html) [Next](12-03.html)

View file

@ -2,11 +2,11 @@
[Previous](12-02.html) [Table of Contents](index.html) [Next](12-04.html) [Previous](12-02.html) [Table of Contents](index.html) [Next](12-04.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
There is, of course, no guarantee that I’m entirely correct about the There is, of course, no guarantee that I'm entirely correct about the
optimizations discussed in this chapter. Without knowing the internals optimizations discussed in this chapter. Without knowing the internals
of the 486, all I can do is time code and make inferences from the of the 486, all I can do is time code and make inferences from the
results; I invite you to deduce your own rules and cross-check them results; I invite you to deduce your own rules and cross-check them
against mine. Also, most likely there are other optimizations that I’m against mine. Also, most likely there are other optimizations that I'm
unaware of. If you have further information on these or any other unaware of. If you have further information on these or any other
undocumented optimizations, please write and let me know. And, of undocumented optimizations, please write and let me know. And, of
course, if anyone from Intel is reading this and wants to give us the course, if anyone from Intel is reading this and wants to give us the
@ -27,11 +27,11 @@ of the first set of instructions and is then immediately used to address
memory by one of the second set. This raises the specter of unpleasant memory by one of the second set. This raises the specter of unpleasant
programming contortions such as intermixing **PUSH**es and **POP**s with programming contortions such as intermixing **PUSH**es and **POP**s with
other instructions to avoid interrupting the addressing pipeline. other instructions to avoid interrupting the addressing pipeline.
Fortunately, matters are actually not so grim as Intel’s documentation Fortunately, matters are actually not so grim as Intel's documentation
would indicate; my tests indicate that the addressing pipeline penalty would indicate; my tests indicate that the addressing pipeline penalty
pops up only spottily when the stack pointer is involved. pops up only spottily when the stack pointer is involved.
For example, you’d certainly expect a sequence such as For example, you'd certainly expect a sequence such as
: :
pop ax pop ax
@ -69,11 +69,11 @@ the sequence
loses two cycles for the same reason. loses two cycles for the same reason.
I certainly haven’t tried all possible combinations, but the results so I certainly haven't tried all possible combinations, but the results so
far indicate that the stack pointer incurs the addressing pipeline far indicate that the stack pointer incurs the addressing pipeline
penalty only if (E)SP is the *explicit* destination of one instruction penalty only if (E)SP is the *explicit* destination of one instruction
and is then used by one of the two following instructions to address and is then used by one of the two following instructions to address
memory. So, for instance, SP isn’t the explicit operand of **POP AX—**AX memory. So, for instance, SP isn't the explicit operand of **POP AX—**AX
is—and no cycles are lost if **POP AX** is followed by **POP** or is—and no cycles are lost if **POP AX** is followed by **POP** or
**RET**. Happily, then, we need not worry about the sequence in which we **RET**. Happily, then, we need not worry about the sequence in which we
use **PUSH** and **POP**. However, adding to, moving to, or subtracting use **PUSH** and **POP**. However, adding to, moving to, or subtracting
@ -84,7 +84,7 @@ pointer to address memory.
#### Problems with Byte Registers {#Heading9} #### Problems with Byte Registers {#Heading9}
There are two ways to lose cycles by using byte registers, and neither There are two ways to lose cycles by using byte registers, and neither
of them is documented by Intel, so far as I know. Let’s start with the of them is documented by Intel, so far as I know. Let's start with the
lesser and simpler of the two. lesser and simpler of the two.
Rule \#3: Do not load a byte portion of a register during one Rule \#3: Do not load a byte portion of a register during one
@ -101,7 +101,7 @@ So, for example, it would be a bad idea to do this
because AL is loaded by one instruction, then AX is used as the source because AL is loaded by one instruction, then AX is used as the source
register for the next instruction. A cycle can be saved simply by register for the next instruction. A cycle can be saved simply by
rearranging the instructions so that the byte register load isn’t rearranging the instructions so that the byte register load isn't
immediately followed by the word register usage, like so: immediately followed by the word register usage, like so:
mov ah,o mov ah,o
@ -115,16 +115,16 @@ Basically, when a byte destination register is part of a word source
register for the next instruction, the 486 is unable to directly use the register for the next instruction, the 486 is unable to directly use the
result from the first instruction as the source for the second result from the first instruction as the source for the second
instruction, because only part of the register required by the second instruction, because only part of the register required by the second
instruction is contained in the first instruction’s result. The full, instruction is contained in the first instruction's result. The full,
updated register value must be read from the register file, and that updated register value must be read from the register file, and that
value can’t be read out until the result from the first instruction has value can't be read out until the result from the first instruction has
been written *into* the register file, a process that takes an extra been written *into* the register file, a process that takes an extra
cycle. I’m not going to explain this in great detail because it’s not cycle. I'm not going to explain this in great detail because it's not
important that you understand why this rule exists (only that it *does* important that you understand why this rule exists (only that it *does*
in fact exist), but it is an interesting window on the way the 486 in fact exist), but it is an interesting window on the way the 486
works. works.
In case you’re curious, there’s no such penalty for the typical **XLAT** In case you're curious, there's no such penalty for the typical **XLAT**
sequence like sequence like
mov bx,offset MemTable mov bx,offset MemTable
@ -139,17 +139,17 @@ is so slow—4 cycles—that it gives the 486 time to perform addressing
calculations during the course of the instruction. calculations during the course of the instruction.
------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 In general, penalties for interrupting the 486's pipeline apply
primarily to the fast core instructions of the 486, most notably primarily to the fast core instructions of the 486, most notably
register-only instructions and **MOV**, although arithmetic and logical register-only instructions and **MOV**, although arithmetic and logical
operations that access memory are also often affected. I don’t know all 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 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 out would be a big, boring job of little value. Basically, on the 486
you should concentrate on using those fast core instructions when you should concentrate on using those fast core instructions when
performance matters, and all the rules I’ll discuss do indeed apply to performance matters, and all the rules I'll discuss do indeed apply to
those instructions. those instructions.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -2,17 +2,17 @@
[Previous](12-03.html) [Table of Contents](index.html) [Next](13-01.html) [Previous](12-03.html) [Table of Contents](index.html) [Next](13-01.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
You don’t need to understand every corner of the 486 universe unless You don't need to understand every corner of the 486 universe unless
you’re a diehard ASMhead who does this stuff for fun. Just learn enough you're a diehard ASMhead who does this stuff for fun. Just learn enough
to be able to speed up the key portions of your programs, and spend the to be able to speed up the key portions of your programs, and spend the
rest of your time on a fast design and overall implementation. rest of your time on a fast design and overall implementation.
#### More Fun with Byte Registers {#Heading10} #### More Fun with Byte Registers {#Heading10}
Rule \#4: Don’t load *any* byte register exactly 2 cycles before using Rule \#4: Don't load *any* byte register exactly 2 cycles before using
*any* register to address memory. *any* register to address memory.
This, the last of this chapter’s rules, is the strangest of the lot. If This, the last of this chapter's rules, is the strangest of the lot. If
any byte register is loaded, and then two cycles later any register is any byte register is loaded, and then two cycles later any register is
used to point to memory, one cycle is lost. So, for example, this code used to point to memory, one cycle is lost. So, for example, this code
@ -43,7 +43,7 @@ runs in the expected three cycles.
In truth, I do not know why this happens. Clearly, it has something to In truth, I do not know why this happens. Clearly, it has something to
do with interrupting the start of the addressing pipeline, and I have my do with interrupting the start of the addressing pipeline, and I have my
theories about how this works, but at this point they’re pure theories about how this works, but at this point they're pure
speculation. Whatever the reason for this rule, ignorance of it—and of speculation. Whatever the reason for this rule, ignorance of it—and of
its interaction with the other rules—could lead to considerable its interaction with the other rules—could lead to considerable
performance loss in seemingly air-tight code. For instance, a casual performance loss in seemingly air-tight code. For instance, a casual
@ -68,7 +68,7 @@ pipeline is now on its first cycle: the one that loading a byte register
can affect. can affect.
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 {#Heading11} #### Timing Your Own 486 Code {#Heading11}
@ -82,7 +82,7 @@ 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 (120 µs/1000 repetitions = 120 ns per repetition; 120 ns
per repetition/30 ns per cycle = 4 cycles per repetition); Listing 12.2 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 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 costs a cycle only when it's performed exactly 2 cycles before
addressing memory. addressing memory.
**LISTING 12.1 LST12-1.ASM** **LISTING 12.1 LST12-1.ASM**
@ -90,7 +90,7 @@ addressing memory.
; Measures the effect of loading a byte register 2 cycles before ; Measures the effect of loading a byte register 2 cycles before
; using a register to address memory. ; using a register to address memory.
mov bp,2 ;run the test code twice to make sure mov bp,2 ;run the test code twice to make sure
; it’s cached ; it's cached
sub bx,bx sub bx,bx
CacheFillLoop: CacheFillLoop:
call ZTimerOn ;start timing call ZTimerOn ;start timing
@ -110,7 +110,7 @@ addressing memory.
; Measures the effect of loading a byte register 1 cycle before ; Measures the effect of loading a byte register 1 cycle before
; using a register to address memory. ; using a register to address memory.
mov bp,2 ;run the test code twice to make sure mov bp,2 ;run the test code twice to make sure
; it’s cached ; it's cached
sub bx,bx sub bx,bx
CacheFillLoop: CacheFillLoop:
call ZTimerOn ;start timing call ZTimerOn ;start timing
@ -129,31 +129,31 @@ 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 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 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 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 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 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 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 seven-tenths of a cycle comes from fetching non-cached instruction
bytes. bytes.
------------------- ----------------------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Whenever you see non-integral timing results of this sort, it’s a good bet that the test code or data isn’t cached.* ![](images/i.jpg) *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 {#Heading12} ### The Story Continues {#Heading12}
There’s certainly plenty more 486 lore to explore, including the 486’s There's certainly plenty more 486 lore to explore, including the 486's
unique prefetch queue, more optimization rules, branching optimizations, unique prefetch queue, more optimization rules, branching optimizations,
performance implications of the cache, the cost of cache misses for performance implications of the cache, the cost of cache misses for
reads, and the implications of cache write-through for writes. reads, and the implications of cache write-through for writes.
Nonetheless, we’ve covered quite a bit of ground in this chapter, and I 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 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 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 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 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 much per cycle as the best 386 code—which makes it perhaps 50 times
as fast as optimized code for the original PC. as fast as optimized code for the original PC.
Sometimes it *is* hard to believe we’re still in Kansas! Sometimes it *is* hard to believe we're still in Kansas!
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](12-03.html) [Table of Contents](index.html) [Next](13-01.html) [Previous](12-03.html) [Table of Contents](index.html) [Next](13-01.html)

View file

@ -8,15 +8,15 @@ Chapter 13\
### Pipelines and Other Hazards of the High End {#Heading2} ### Pipelines and Other Hazards of the High End {#Heading2}
It’s a sad but true fact that 84 percent of American schoolchildren are It's a sad but true fact that 84 percent of American schoolchildren are
ignorant of 92 percent of American history. Not my daughter, though. We ignorant of 92 percent of American history. Not my daughter, though. We
recently visited historical Revolutionary-War-vintage Fort Ticonderoga, recently visited historical Revolutionary-War-vintage Fort Ticonderoga,
and she’s now 97 percent aware of a key element of our national and she's now 97 percent aware of a key element of our national
heritage: that the basic uniform for soldiers in those days was what heritage: that the basic uniform for soldiers in those days was what
appears to be underwear, plus a hat so that no one could complain that appears to be underwear, plus a hat so that no one could complain that
they were undermining family values. Ha! Just kidding! Actually, what they were undermining family values. Ha! Just kidding! Actually, what
she learned was that in those days, it was pure coincidence if a she learned was that in those days, it was pure coincidence if a
cannonball actually hit anything it was aimed at, which isn’t surprising cannonball actually hit anything it was aimed at, which isn't surprising
considering the lack of rifling, precision parts, and ballistics. The considering the lack of rifling, precision parts, and ballistics. The
guides at the fort shot off three cannons; the closest they came to the guides at the fort shot off three cannons; the closest they came to the
target was about 50 feet, and that was only because the wind helped. I target was about 50 feet, and that was only because the wind helped. I
@ -26,21 +26,21 @@ necessarily, the enemy.
Nowadays, of course, we have automatic weapons that allow a teenager to Nowadays, of course, we have automatic weapons that allow a teenager to
singlehandedly defeat the entire U.S. Army, not to mention so-called singlehandedly defeat the entire U.S. Army, not to mention so-called
“smart” bombs, which are smart in the sense that they can seek out and "smart" bombs, which are smart in the sense that they can seek out and
empty a taxpayer’s wallet without being detected by radar. There’s an empty a taxpayer's wallet without being detected by radar. There's an
obvious lesson here about progress, which I leave you to deduce for obvious lesson here about progress, which I leave you to deduce for
yourselves. yourselves.
Here’s the same lesson, in another form. Ten years ago, we had a slow Here's the same lesson, in another form. Ten years ago, we had a slow
processor, the 8088, for which it was devilishly hard to optimize, and processor, the 8088, for which it was devilishly hard to optimize, and
for which there was no good optimization documentation available. Now we for which there was no good optimization documentation available. Now we
have a processor, the 486, that’s 50 to 100 times faster than the have a processor, the 486, that's 50 to 100 times faster than the
8088—and for which there is no good optimization documentation 8088—and for which there is no good optimization documentation
available. Sure, Intel provides a few tidbits on optimization in the available. Sure, Intel provides a few tidbits on optimization in the
back of the *i486 Microprocessor Programmer’s Reference Manual,* but, as back of the *i486 Microprocessor Programmer's Reference Manual,* but, as
I discussed in Chapter 12, that information is both incomplete and not I discussed in Chapter 12, that information is both incomplete and not
entirely correct. Besides, most assembly language programmers don’t entirely correct. Besides, most assembly language programmers don't
bother to read Intel’s manuals (which are extremely informative and well bother to read Intel's manuals (which are extremely informative and well
done, but only slightly more fun to read than the phone book), and go done, but only slightly more fun to read than the phone book), and go
right on programming the 486 using outdated 8088 optimization right on programming the 486 using outdated 8088 optimization
techniques, blissfully unaware of a new and heavily mutated generation techniques, blissfully unaware of a new and heavily mutated generation
@ -53,20 +53,20 @@ instructions.
#### 486 Pipeline Optimization {#Heading3} #### 486 Pipeline Optimization {#Heading3}
I’ve mentioned Terje Mathisen in my writings before. Terje is an I've mentioned Terje Mathisen in my writings before. Terje is an
assembly language programmer extraordinaire, and author of the assembly language programmer extraordinaire, and author of the
incredibly fast public-domain word-counting program WC (which comes incredibly fast public-domain word-counting program WC (which comes
complete with source code; well worth a look, if you want to see what complete with source code; well worth a look, if you want to see what
*really* fast code looks like). Terje’s a regular participant in the *really* fast code looks like). Terje's a regular participant in the
ibm.pc/fast.code topic on Bix. In a thread titled “486 Pipeline ibm.pc/fast.code topic on Bix. In a thread titled "486 Pipeline
Optimization, or TANSTATFC (There Ain’t No Such Thing As The Fastest Optimization, or TANSTATFC (There Ain't No Such Thing As The Fastest
Code),” he detailed the following optimization to WC, perhaps the best Code)," he detailed the following optimization to WC, perhaps the best
example of 486 pipeline optimization I’ve yet seen. example of 486 pipeline optimization I've yet seen.
Terje’s inner loop originally looked something like the code in Listing Terje's inner loop originally looked something like the code in Listing
13.1. (I’ve taken a few liberties for illustrative purposes.) Of course, 13.1. (I've taken a few liberties for illustrative purposes.) Of course,
Terje unrolls this loop a few times (128 times, to be exact). By the Terje unrolls this loop a few times (128 times, to be exact). By the
way, in Listing 13.1 you’ll notice that Terje counts not only words but way, in Listing 13.1 you'll notice that Terje counts not only words but
also lines, at a rate of three instructions for every two characters! also lines, at a rate of three instructions for every two characters!
**LISTING 13.1 L13-1.ASM** **LISTING 13.1 L13-1.ASM**
@ -81,13 +81,13 @@ instructions, one two-cycle instruction, and no branches. It *is* tight,
but those three instructions actually take a minimum of 8 cycles to but those three instructions actually take a minimum of 8 cycles to
execute, as shown in Figure 13.1. The problem is that DI is loaded just execute, as shown in Figure 13.1. The problem is that DI is loaded just
before being used to address memory, and that costs 2 cycles because it before being used to address memory, and that costs 2 cycles because it
interrupts the 486’s internal instruction pipeline. Likewise, BX is interrupts the 486's internal instruction pipeline. Likewise, BX is
loaded just before being used to address memory, costing another two loaded just before being used to address memory, costing another two
cycles. Thus, this loop takes twice as long as cycle counts would seem cycles. Thus, this loop takes twice as long as cycle counts would seem
to indicate, simply because two registers are loaded immediately before to indicate, simply because two registers are loaded immediately before
being used, disrupting the 486’s pipeline. being used, disrupting the 486's pipeline.
Listing 13.2 shows Terje’s immediate response to these pipelining Listing 13.2 shows Terje's immediate response to these pipelining
problems; he simply swapped the instructions that load DI and BL. This problems; he simply swapped the instructions that load DI and BL. This
one change cut execution time per character pair from eight cycles to one change cut execution time per character pair from eight cycles to
five cycles! The load of BL is now separated by one instruction from the five cycles! The load of BL is now separated by one instruction from the
@ -95,7 +95,7 @@ use of BX to address memory, so the pipeline penalty is reduced from two
cycles to one cycle. The load of DI is also separated by one instruction cycles to one cycle. The load of DI is also separated by one instruction
from the use of DI to address memory (remember, the loop is unrolled, so from the use of DI to address memory (remember, the loop is unrolled, so
the last instruction is followed by the first instruction), but because the last instruction is followed by the first instruction), but because
the intervening instruction takes two cycles, there’s no penalty at all. the intervening instruction takes two cycles, there's no penalty at all.
![](images/13-01.jpg)\ ![](images/13-01.jpg)\
**Figure 13.1**  *Cycle-eaters in the original WC.* **Figure 13.1**  *Cycle-eaters in the original WC.*
@ -114,7 +114,7 @@ the intervening instruction takes two cycles, there’s no penalty at all.
At this point, Terje had nearly doubled the performance of this code At this point, Terje had nearly doubled the performance of this code
simply by moving one instruction. (Note that swapping the instructions simply by moving one instruction. (Note that swapping the instructions
also made it necessary to preload DI at the start of the loop; Listing also made it necessary to preload DI at the start of the loop; Listing
13.2 is not exactly equivalent to Listing 13.1.) I’ll let Terje describe 13.2 is not exactly equivalent to Listing 13.1.) I'll let Terje describe
his next optimization in his own words: his next optimization in his own words:
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -2,7 +2,7 @@
[Previous](13-01.html) [Table of Contents](index.html) [Next](13-03.html) [Previous](13-01.html) [Table of Contents](index.html) [Next](13-03.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
“When I looked closely as this, I realized that the two cycles for the "When I looked closely as this, I realized that the two cycles for the
final **ADD** is just the sum of 1 cycle to load the data from memory, final **ADD** is just the sum of 1 cycle to load the data from memory,
and 1 cycle to add it to DX, so the code could just as well have been and 1 cycle to add it to DX, so the code could just as well have been
written as shown in Listing 13.3. The final breakthrough came when I written as shown in Listing 13.3. The final breakthrough came when I
@ -10,7 +10,7 @@ realized that by initializing AX to zero outside the loop, I could
rearrange it as shown in Listing 13.4 and do the final **ADD DX,AX** rearrange it as shown in Listing 13.4 and do the final **ADD DX,AX**
after the loop. This way there are two single-cycle instructions between after the loop. This way there are two single-cycle instructions between
the first and the fourth line, avoiding all pipeline stalls, for a total the first and the fourth line, avoiding all pipeline stalls, for a total
throughput of two cycles/char.” throughput of two cycles/char."
**LISTING 13.3 L13-3.ASM** **LISTING 13.3 L13-3.ASM**
@ -27,11 +27,11 @@ throughput of two cycles/char.”
; appropriately for the pair ; appropriately for the pair
mov ax,[bx+8000h] ;get increments for next time mov ax,[bx+8000h] ;get increments for next time
I’d like to point out two fairly remarkable things. First, the single I'd like to point out two fairly remarkable things. First, the single
cycle that Terje saved in Listing 13.4 sped up his entire word-counting cycle that Terje saved in Listing 13.4 sped up his entire word-counting
engine by 25 percent or more; Listing 13.4 is fully twice as fast as engine by 25 percent or more; Listing 13.4 is fully twice as fast as
Listing 13.1—all the result of nothing more than shifting an instruction Listing 13.1—all the result of nothing more than shifting an instruction
and splitting another into two operations. Second, Terje’s word-counting and splitting another into two operations. Second, Terje's word-counting
engine can process more than 16 million characters *per second* on a engine can process more than 16 million characters *per second* on a
486/33. 486/33.
@ -55,7 +55,7 @@ significant byte first in memory, or *big endian*), like so:
**BSWAP** can also be useful for reversing the order of pixel bits from **BSWAP** can also be useful for reversing the order of pixel bits from
a bitmap so that they can be rotated 32 bits at a time with an a bitmap so that they can be rotated 32 bits at a time with an
instruction such as **ROR EAX,1**. Intel’s byte ordering for multiword instruction such as **ROR EAX,1**. Intel's byte ordering for multiword
values (least-significant byte first) loads pixels in the wrong order, values (least-significant byte first) loads pixels in the wrong order,
so far as word rotation is concerned, but **BSWAP** can take care of so far as word rotation is concerned, but **BSWAP** can take care of
that. that.
@ -76,14 +76,14 @@ the obvious conclusion that it would be great if there were some way to
use the upper and lower 16 bits of selected 386 registers as separate use the upper and lower 16 bits of selected 386 registers as separate
16-bit registers, effectively increasing the available register space. 16-bit registers, effectively increasing the available register space.
Unfortunately, the x86 instruction set doesn’t provide any way to work Unfortunately, the x86 instruction set doesn't provide any way to work
directly with only the upper half of a 32-bit register. The next best directly with only the upper half of a 32-bit register. The next best
solution is to rotate the register to give you access in the lower 16 solution is to rotate the register to give you access in the lower 16
bits to the half you need at any particular time, with code along the bits to the half you need at any particular time, with code along the
lines of that in Listing 13.5. Having to rotate the 16-bit fields into lines of that in Listing 13.5. Having to rotate the 16-bit fields into
position certainly isn’t as good as having direct access to the upper position certainly isn't as good as having direct access to the upper
half, but surely it’s better than having to get the values out of half, but surely it's better than having to get the values out of
memory, isn’t it? memory, isn't it?
**LISTING 13.5 L13-5.ASM** **LISTING 13.5 L13-5.ASM**
@ -102,12 +102,12 @@ memory, isn’t it?
Not necessarily. Shifts and rotates are among the worst performing Not necessarily. Shifts and rotates are among the worst performing
instructions of the 486, taking 2 to 3 cycles to execute. Thus, it takes instructions of the 486, taking 2 to 3 cycles to execute. Thus, it takes
2 cycles to rotate the skip value into CX in Listing 13.5, and 2 more 2 cycles to rotate the skip value into CX in Listing 13.5, and 2 more
cycles to rotate it back to the upper half of ECX. I’d say four cycles cycles to rotate it back to the upper half of ECX. I'd say four cycles
is a pretty steep price to pay, especially considering that a **MOV** to is a pretty steep price to pay, especially considering that a **MOV** to
or from memory takes only one cycle. Basically, using **ROR** to access or from memory takes only one cycle. Basically, using **ROR** to access
a 16-bit value in the upper half of a 16-bit register is a pretty a 16-bit value in the upper half of a 16-bit register is a pretty
marginal technique, unless for some reason you can’t access memory at marginal technique, unless for some reason you can't access memory at
all (for example, if you’re using BP as a working register, temporarily all (for example, if you're using BP as a working register, temporarily
making the stack frame inaccessible). making the stack frame inaccessible).
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -47,13 +47,13 @@ and the only cost is that the previous contents of AX are destroyed.
Likewise, popping a memory location takes six cycles, but popping a Likewise, popping a memory location takes six cycles, but popping a
register and writing it to memory takes only two cycles combined. The register and writing it to memory takes only two cycles combined. The
*i486 Microprocessor Programmer’s Reference Manual* lists a 4-cycle *i486 Microprocessor Programmer's Reference Manual* lists a 4-cycle
execution time for popping a register, but pay that no mind; popping a execution time for popping a register, but pay that no mind; popping a
register takes only 1 cycle. register takes only 1 cycle.
Why is it that such a convenient operation as pushing or popping memory 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 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 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 complex operations, which must be carried out in microcode just as they
were on the 386, are almost all relatively slow. Slow, complex were on the 386, are almost all relatively slow. Slow, complex
operations include all the string instructions except **REP MOVS,** as operations include all the string instructions except **REP MOVS,** as
@ -61,7 +61,7 @@ well as **XLAT, LOOP,** and, of course, **PUSH *mem*** and **POP
*mem.*** *mem.***
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 {#Heading6} ### Optimal 1-Bit Shifts and Rotates {#Heading6}
@ -72,9 +72,9 @@ forms—as in **ROR AX,1** and **SHL BX,1—**are *3-cycle* instructions. Go
figure. figure.
Assemblers default to the 1-bit instruction for 1-bit shifts and Assemblers default to the 1-bit instruction for 1-bit shifts and
rotates. That’s not unreasonable since the 1-bit form is a byte shorter rotates. That's not unreasonable since the 1-bit form is a byte shorter
and is just as fast as the n-bit forms on a 386 and faster on a 286, and and is just as fast as the n-bit forms on a 386 and faster on a 286, and
the n-bit form doesn’t even exist on an 8088. In a really critical loop, the n-bit form doesn't even exist on an 8088. In a really critical loop,
however, it might be worth hand-assembling the n-bit form of a however, it might be worth hand-assembling the n-bit form of a
single-bit shift or rotate in order to save that cycle. The easiest way single-bit shift or rotate in order to save that cycle. The easiest way
to do this is to assemble a 2-bit form of the desired instruction, as in to do this is to assemble a 2-bit form of the desired instruction, as in
@ -92,14 +92,14 @@ code as follows:
At the end of this sequence, DX will contain 2, and the fast n-bit At the end of this sequence, DX will contain 2, and the fast n-bit
version of **SHL AX,1** will have executed. If you use this approach, version of **SHL AX,1** will have executed. If you use this approach,
I’d recommend using a macro, rather than sticking DBs in the middle of I'd recommend using a macro, rather than sticking DBs in the middle of
your code. your code.
Again, this technique is advantageous *only* on a 486. It also doesn’t Again, this technique is advantageous *only* on a 486. It also doesn't
apply to **RCL** and **RCR,** where you definitely want to use the 1-bit apply to **RCL** and **RCR,** where you definitely want to use the 1-bit
versions whenever you can, because the n-bit versions are horrendously versions whenever you can, because the n-bit versions are horrendously
slow. But if you’re optimizing for the 486, these tidbits can save a few slow. But if you're optimizing for the 486, these tidbits can save a few
critical cycles—and Lord knows that if you’re optimizing for the critical cycles—and Lord knows that if you're optimizing for the
486—that is, if you need even more performance than you get from 486—that is, if you need even more performance than you get from
unoptimized code on a 486—you almost certainly need all the speed you unoptimized code on a 486—you almost certainly need all the speed you
can get. can get.

View file

@ -15,7 +15,7 @@ uses a perfectly valid 32-bit address, with the byte accessed being the
one at the offset in DS pointed to by the sum of EDX times 4 plus the one at the offset in DS pointed to by the sum of EDX times 4 plus the
offset of **BaseTable** plus ECX. This is a very powerful memory offset of **BaseTable** plus ECX. This is a very powerful memory
addressing scheme, far superior to 8088-style 16-bit addressing, but addressing scheme, far superior to 8088-style 16-bit addressing, but
it’s not without its quirks and costs, so let’s take a quick look at it's not without its quirks and costs, so let's take a quick look at
32-bit addressing. (By the way, 32-bit addressing is not limited to 32-bit addressing. (By the way, 32-bit addressing is not limited to
protected mode; 32-bit instructions may be used in real mode, although protected mode; 32-bit instructions may be used in real mode, although
each instruction that uses 32-bit addressing must have an address-size each instruction that uses 32-bit addressing must have an address-size
@ -26,18 +26,18 @@ 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 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 in arrays and tables.) The same register may serve as both base and
index register, except for ESP, which can only be the base. index register, except for ESP, which can only be the base.
Incidentally, it makes sense that ESP can’t be scaled; ESP presumably 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 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 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. its nature, a base rather than index pointer.
That’s all there is to the functionality of 32-bit addressing; it’s very 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 simple, much simpler than 16-bit addressing, with its sharply limited
memory addressing register combinations. The costs of 32-bit addressing memory addressing register combinations. The costs of 32-bit addressing
are a bit more subtle. The only performance cost (apart from the are a bit more subtle. The only performance cost (apart from the
aforementioned 1-cycle penalty for using 32-bit addressing in real mode) 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 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 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. 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 **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. does **MOV CL,[EAX+EDX]; MOV CL,[EAX+100H]** is not indexed, however.
@ -45,7 +45,7 @@ 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 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 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 byte, which Intel calls the Mod-R/M byte, which is placed immediately
after each instruction’s opcode to describe the memory addressing mode, after each instruction's opcode to describe the memory addressing mode,
plus 1 or 2 optional bytes of addressing displacement—that is, a 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 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 continues to use the Mod-R/M byte, albeit with a different
@ -60,15 +60,15 @@ example, **MOV AL, [EBX]** is a 2-byte instruction; **MOV AL,
------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
However, because 32-bit addressing supports many more addressing However, because 32-bit addressing supports many more addressing
combinations than 16-bit addressing, the Mod-R/M byte can’t describe all combinations than 16-bit addressing, the Mod-R/M byte can't describe all
the combinations. Therefore, whenever an index register (as described the combinations. Therefore, whenever an index register (as described
above) is involved, a second byte, the SIB byte, follows the Mod-R/M above) is involved, a second byte, the SIB byte, follows the Mod-R/M
byte to provide additional address information. Consequently, whenever byte to provide additional address information. Consequently, whenever
you use a scaled memory addressing register or use the sum of two 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 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 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 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 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: calculation outside the loop as, for example, in a loop like this:
LoopTop: LoopTop:
@ -87,7 +87,7 @@ You could change this to the following for greater performance:
jnz LoopTop jnz LoopTop
shr ebx,1 ;ebx*2/2 shr ebx,1 ;ebx*2/2
I’ll end this chapter with two more quirks of 32-bit addressing. First, I'll end this chapter with two more quirks of 32-bit addressing. First,
as with 16-bit addressing, addressing that uses EBP as a base register as with 16-bit addressing, addressing that uses EBP as a base register
both accesses the SS segment by default and always has a displacement of both accesses the SS segment by default and always has a displacement of
at least 1 byte. This reflects the common use of EBP to address a stack at least 1 byte. This reflects the common use of EBP to address a stack
@ -97,12 +97,12 @@ address non-stack memory.
Lastly, as I mentioned, ESP cannot be scaled. In fact, ESP cannot be an Lastly, as I mentioned, ESP cannot be scaled. In fact, ESP cannot be an
index register; it must be a base register. Ironically, however, ESP is index register; it must be a base register. Ironically, however, ESP is
the one register that cannot be used to address memory without the the one register that cannot be used to address memory without the
presence of an SIB byte, even if it’s used without an index register. presence of an SIB byte, even if it's used without an index register.
This is an outcome of the way in which the SIB byte extends the This is an outcome of the way in which the SIB byte extends the
capabilities of the Mod-R/M byte, and there’s nothing to be done about capabilities of the Mod-R/M byte, and there's nothing to be done about
it, but it’s at least worth noting that ESP-based, non-indexed it, but it's at least worth noting that ESP-based, non-indexed
addressing makes for instructions that are a byte larger than other addressing makes for instructions that are a byte larger than other
non-indexed addressing (but not any slower; there’s no 1-cycle penalty non-indexed addressing (but not any slower; there's no 1-cycle penalty
for using ESP as a base register) on the 486. for using ESP as a base register) on the 486.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -10,10 +10,10 @@ Chapter 14\
When you seem to be stumped, stop for a minute and *think.* All the When you seem to be stumped, stop for a minute and *think.* All the
information you need may be right in front of your nose if you just look information you need may be right in front of your nose if you just look
at things a little differently. Here’s a case in point: at things a little differently. Here's a case in point:
When I was in college, I used to stay around campus for the summer. Oh, When I was in college, I used to stay around campus for the summer. Oh,
I’d take a course or two, but mostly it was an excuse to hang out and I'd take a course or two, but mostly it was an excuse to hang out and
have fun. In that spirit, my girlfriend, Adrian (*not* my future wife, have fun. In that spirit, my girlfriend, Adrian (*not* my future wife,
partly for reasons that will soon become apparent), bussed in to spend a partly for reasons that will soon become apparent), bussed in to spend a
week, sharing a less-than-elegant \$150 per month apartment with me and, week, sharing a less-than-elegant \$150 per month apartment with me and,
@ -29,57 +29,57 @@ so we thought)—had never been cleaned. By which I mean that I know for a
certainty that *we* had never cleaned it, but I suspect that it had in certainty that *we* had never cleaned it, but I suspect that it had in
fact not been cleaned since the Late Jurassic, or possibly earlier. Our fact not been cleaned since the Late Jurassic, or possibly earlier. Our
feet tended to stick to it; had the apartment suddenly turned feet tended to stick to it; had the apartment suddenly turned
upside-down, I think we’d all have been hanging from the ceiling. upside-down, I think we'd all have been hanging from the ceiling.
One day, my roommate and I returned from a pick-up basketball game. One day, my roommate and I returned from a pick-up basketball game.
Adrian, having been left to her own devices for a couple of hours, had Adrian, having been left to her own devices for a couple of hours, had
apparently kept herself busy. “Notice anything?” she asked, with an edge apparently kept herself busy. "Notice anything?" she asked, with an edge
to her voice that suggested we had damned well better. to her voice that suggested we had damned well better.
“Uh, you cooked dinner?” I guessed. “Washed the dishes? Had your hair "Uh, you cooked dinner?" I guessed. "Washed the dishes? Had your hair
done?” My roommate was equally without a clue. done?" My roommate was equally without a clue.
She stamped her foot (really; the only time I’ve ever seen it happen), She stamped her foot (really; the only time I've ever seen it happen),
and said, “No, you jerks! The kitchen floor! Look at the floor! I and said, "No, you jerks! The kitchen floor! Look at the floor! I
cleaned it!” cleaned it!"
The floor really did look amazing. It was actually all white; the black The floor really did look amazing. It was actually all white; the black
lines had been grooves filled with dirt. We assured her that it looked lines had been grooves filled with dirt. We assured her that it looked
terrific, it just wasn’t that obvious until you knew to look for it; terrific, it just wasn't that obvious until you knew to look for it;
anyone would tell you that it wasn’t the kind of thing that jumped out anyone would tell you that it wasn't the kind of thing that jumped out
at you, but it really was great, no kidding. We had almost smoothed at you, but it really was great, no kidding. We had almost smoothed
things over, when a friend walked in, looked around with a start, and things over, when a friend walked in, looked around with a start, and
said, “Hey! Did you guys put in a new floor?” said, "Hey! Did you guys put in a new floor?"
As I said, sometimes everything you need to know is right in front of As I said, sometimes everything you need to know is right in front of
your nose. Which brings us to Boyer-Moore string searching. your nose. Which brings us to Boyer-Moore string searching.
### String Searching Refresher {#Heading3} ### String Searching Refresher {#Heading3}
I’ve discussed string searching earlier in this book, in Chapters 5 and I've discussed string searching earlier in this book, in Chapters 5 and
9. You may want to refer back to these chapters for some background on 9. You may want to refer back to these chapters for some background on
string searching in general. I’m also going to use some of the code from string searching in general. I'm also going to use some of the code from
that chapter as part of this chapter’s test suite. For further that chapter as part of this chapter's test suite. For further
information, you may want to refer to the discussion of string searching information, you may want to refer to the discussion of string searching
in the excellent *Algorithms in C,* by Robert Sedgewick in the excellent *Algorithms in C,* by Robert Sedgewick
(Addison-Wesley), which served as the primary reference for this (Addison-Wesley), which served as the primary reference for this
chapter. (If you look at Sedgewick, be aware that in the Boyer-Moore chapter. (If you look at Sedgewick, be aware that in the Boyer-Moore
listing on page 288, there is a mistake: “j \> 0” in the **for** loop listing on page 288, there is a mistake: "j \> 0" in the **for** loop
should be “j \>= 0,” unless I’m missing something.) should be "j \>= 0," unless I'm missing something.)
String searching is the simple matter of finding the first occurrence of String searching is the simple matter of finding the first occurrence of
a particular sequence of bytes (the pattern) within another sequence of a particular sequence of bytes (the pattern) within another sequence of
bytes (the buffer). The obvious, brute-force approach is to try every bytes (the buffer). The obvious, brute-force approach is to try every
possible match location, starting at the beginning of the buffer and possible match location, starting at the beginning of the buffer and
advancing one position after each mismatch, until either a match is advancing one position after each mismatch, until either a match is
found or the buffer is exhausted. There’s even a nifty string found or the buffer is exhausted. There's even a nifty string
instruction, **REPZ CMPS,** that’s perfect for comparing the pattern to instruction, **REPZ CMPS,** that's perfect for comparing the pattern to
the contents of the buffer at each location. What could be simpler? the contents of the buffer at each location. What could be simpler?
We have some important information that we’re not yet using, though. 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 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 that the buffer contains text, in which case there will be dozens of
different characters; and although the distribution of characters won’t different characters; and although the distribution of characters won't
usually be even, neither will any one character constitute half the usually be even, neither will any one character constitute half the
buffer, or anything close. A reasonable conclusion is that the first buffer, or anything close. A reasonable conclusion is that the first
character of the pattern will rarely match the first character of the character of the pattern will rarely match the first character of the
@ -89,37 +89,37 @@ potential match locations with single repetitions of **SCASB.** Only
when that first character does (infrequently) match must we drop back to when that first character does (infrequently) match must we drop back to
the slower **REPZ CMPS** approach. the slower **REPZ CMPS** approach.
It’s important to understand that we’re assuming that the buffer is 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 typical text. That's what I meant at the outset, when I said that the
information you need may be under your nose. information you need may be under your nose.
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 the letter ‘B,' then the **REPNZ SCASB/REPZ CMPS** approach will be much
slower than the brute-force **REPZ CMPS** approach when searching for slower than the brute-force **REPZ CMPS** approach when searching for
the pattern “AB,” because **REPNZ SCASB** would match at every buffer the pattern "AB," because **REPNZ SCASB** would match at every buffer
location. You could construct a horrendous worst-case scenario for location. You could construct a horrendous worst-case scenario for
almost any good optimization; the key is understanding the usual almost any good optimization; the key is understanding the usual
conditions under which your code will work. conditions under which your code will work.
As discussed in Chapter 9, we also know that certain characters have As discussed in Chapter 9, we also know that certain characters have
lower probabilities of matching than others. In a normal buffer, ‘T’ lower probabilities of matching than others. In a normal buffer, ‘T'
will match far more often than ‘X.’ Therefore, if we use **REPNZ SCASB** 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 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 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 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 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, 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 the least-common **REPNZ SCASB** approach will take about as long as N
repetitions of **REPNZ SCASB.** repetitions of **REPNZ SCASB.**
At this point, we’re pretty much searching at the speed of **REPNZ At this point, we're pretty much searching at the speed of **REPNZ
SCASB.** On the x86, there simply is no faster way to test each SCASB.** On the x86, there simply is no faster way to test each
character in turn. In order to get any faster, we’d have to check fewer character in turn. In order to get any faster, we'd have to check fewer
characters—but we can’t do that and still be sure of finding all characters—but we can't do that and still be sure of finding all
matches. Can we? matches. Can we?
Actually, yes, we can. Actually, yes, we can.

View file

@ -5,7 +5,7 @@
### The Boyer-Moore Algorithm {#Heading4} ### The Boyer-Moore Algorithm {#Heading4}
All our *a priori* knowledge of string searching is stated above, but All our *a priori* knowledge of string searching is stated above, but
there’s another sort of knowledge—knowledge that’s generated there's another sort of knowledge—knowledge that's generated
dynamically. As we search through the buffer, we acquire information dynamically. As we search through the buffer, we acquire information
each time we check for a match. One sort of information that we acquire each time we check for a match. One sort of information that we acquire
is based on partial matches; we can often skip ahead after partial is based on partial matches; we can often skip ahead after partial
@ -14,23 +14,23 @@ already implicitly done a comparison of the partially matched buffer
characters with all possible pattern start locations that overlap those characters with all possible pattern start locations that overlap those
partially-matched bytes. partially-matched bytes.
If that makes your head hurt, it should—and don’t worry. This line of 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 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 half the basis of the Boyer-Moore algorithm, is what gives Boyer-Moore
its reputation for inscrutability. That reputation is well deserved for its reputation for inscrutability. That reputation is well deserved for
this aspect (which I will not discuss further in this book), but there’s this aspect (which I will not discuss further in this book), but there's
another part of Boyer-Moore that’s easily understood, easily another part of Boyer-Moore that's easily understood, easily
implemented, and highly effective. implemented, and highly effective.
Consider this: We’re searching for the pattern “ABC,” beginning the Consider this: We're searching for the pattern "ABC," beginning the
search at the start (offset 0) of a buffer containing “ABZABC.” We match 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 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 ‘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 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! 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 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 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. 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 We can just skip straight from offset 0 to offset 3 and continue, saving
ourselves two comparisons. ourselves two comparisons.
@ -41,8 +41,8 @@ Look at it differently, though: What if we compare the pattern starting
with the last (rightmost) byte, rather than the first (leftmost) byte? 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 In other words, what if we compare from high memory toward low, in the
direction in which string instructions go after the **STD** instruction? direction in which string instructions go after the **STD** instruction?
After all, we’re comparing one set of bytes (the pattern) to another set 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 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 what order we compare them, so long as all the bytes in one set are
compared to the corresponding bytes in the other set. compared to the corresponding bytes in the other set.
@ -51,25 +51,25 @@ compared to the corresponding bytes in the other set.
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
We learn nothing new from a mismatch on the leftmost character, except 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 that the pattern can't match starting at that location. A mismatch on
the rightmost character, however, tells us about the possibilities of the rightmost character, however, tells us about the possibilities of
the pattern matching starting at every buffer location from which the the pattern matching starting at every buffer location from which the
pattern spans the mismatch location. If the mismatched character in 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 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 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 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 *might* have matched, but have just been shown not to, because they
overlap the mismatched character that doesn’t belong in the pattern. In 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 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 This is how we can outperform even **REPNZ SCASB; REPNZ SCASB** has to
check every byte in the buffer, but Boyer-Moore doesn’t. check every byte in the buffer, but Boyer-Moore doesn't.
Figure 14.1 illustrates the operation of a Boyer-Moore search when the Figure 14.1 illustrates the operation of a Boyer-Moore search when the
rightcharacter of the search pattern (which is the first character rightcharacter of the search pattern (which is the first character
that’s compared at each location because we’re comparing backwards) that's compared at each location because we're comparing backwards)
mismatches with a buffer character that appears nowhere in the pattern. mismatches with a buffer character that appears nowhere in the pattern.
Figure 14.2 illustrates the operation of a partial match when the 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 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 case, we can only skip ahead past the mismatch location, resulting in an
advance of fewer bytes than the pattern length, and potentially as advance of fewer bytes than the pattern length, and potentially as
little as the same single byte distance by which the standard search little as the same single byte distance by which the standard search
@ -79,7 +79,7 @@ approach advances.
**Figure 14.1**  *Mismatch on first character checked.* **Figure 14.1**  *Mismatch on first character checked.*
What if the mismatch occurs with a buffer character that *does* occur in What if the mismatch occurs with a buffer character that *does* occur in
the pattern? Then we can’t skip past the mismatch location, but we can the pattern? Then we can't skip past the mismatch location, but we can
skip to whatever location aligns the rightmost occurrence of that skip to whatever location aligns the rightmost occurrence of that
character in the pattern with the mismatch location, as shown in Figure character in the pattern with the mismatch location, as shown in Figure
14.3. 14.3.
@ -91,9 +91,9 @@ a mismatch, we see what we can learn from the buffer character that
failed to match the pattern. Imagine that we move the pattern to the failed to match the pattern. Imagine that we move the pattern to the
right across the mismatch location until we find a start location that right across the mismatch location until we find a start location that
the mismatch does not eliminate as a possible match for the pattern. If the mismatch does not eliminate as a possible match for the pattern. If
the mismatch character doesn’t appear in the pattern, the pattern can the mismatch character doesn't appear in the pattern, the pattern can
move clear past the mismatch location. Otherwise, the pattern moves move clear past the mismatch location. Otherwise, the pattern moves
until a matching pattern byte lies atop the mismatch. That’s all there until a matching pattern byte lies atop the mismatch. That's all there
is to it! is to it!
![](images/14-02.jpg)\ ![](images/14-02.jpg)\

View file

@ -10,7 +10,7 @@ modified version of the text of this chapter) shows that this
implementation is generally much slower than **REPNZ SCASB,** although implementation is generally much slower than **REPNZ SCASB,** although
it does come close when searching for long patterns. Listing 14.1 is it does come close when searching for long patterns. Listing 14.1 is
designed primarily to make later assembly implemenmore comprehensible, designed primarily to make later assembly implemenmore comprehensible,
rather than faster; Sedge’s implementation uses arrays rather than rather than faster; Sedge's implementation uses arrays rather than
pointers, is a great deal more compact and very clever, and may be pointers, is a great deal more compact and very clever, and may be
somewhat faster. Regardless, the far superior performance of **REPNZ somewhat faster. Regardless, the far superior performance of **REPNZ
SCASB** clearly indicates that assembly language is in order at this SCASB** clearly indicates that assembly language is in order at this
@ -18,17 +18,17 @@ point.
* * * * * * * * * *
“g;” "g;"
“Yogi” "Yogi"
“igoY” "igoY"
“Adrian” "Adrian"
“Conclusion” "Conclusion"
“You don’t know what you know” "You don't know what you know"
* * * * * * * * * *
@ -140,14 +140,14 @@ Table 14.1 Comparison of searching techniques.
* * * * * * * * * *
The entry “Standard Boyer-Moore in ASM” in Table 14.1 refers to The entry "Standard Boyer-Moore in ASM" in Table 14.1 refers to
straight-forward hand optimization of Listing 14.1, code that is not straight-forward hand optimization of Listing 14.1, code that is not
included in this chapter for the perfectly good reason that it is slower included in this chapter for the perfectly good reason that it is slower
in most cases than **REPNZ SCASB.** I say this casually now, but not so in most cases than **REPNZ SCASB.** I say this casually now, but not so
yesterday, when I had all but concluded that Boyer-Moore was simply yesterday, when I had all but concluded that Boyer-Moore was simply
inferior on the x86, due to two architectural quirks: the string inferior on the x86, due to two architectural quirks: the string
instructions and slow branch. I had even coined a neat phrase for it: instructions and slow branch. I had even coined a neat phrase for it:
Architecture is destiny. Has a nice ring, doesn’t it? Architecture is destiny. Has a nice ring, doesn't it?
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](14-02.html) [Table of Contents](index.html) [Next](14-04.html) [Previous](14-02.html) [Table of Contents](index.html) [Next](14-04.html)

View file

@ -30,13 +30,13 @@
/* Create the table of distances by which to skip ahead on /* Create the table of distances by which to skip ahead on
mismatches for every possible byte value */ mismatches for every possible byte value */
/* Initialize all skips to the pattern length; this is the skip /* Initialize all skips to the pattern length; this is the skip
distance for bytes that don’t appear in the pattern */ distance for bytes that don't appear in the pattern */
for (i = 0; i < 256; i++) SkipTable[i] = PatternLength; for (i = 0; i < 256; i++) SkipTable[i] = PatternLength;
/*Set the skip values for the bytes that do appear in the pattern /*Set the skip values for the bytes that do appear in the pattern
to the distance from the byte location to the end of the to the distance from the byte location to the end of the
pattern. When there are multiple instances of the same byte, pattern. When there are multiple instances of the same byte,
the rightmost instance’s skip value is used. Note that the the rightmost instance's skip value is used. Note that the
rightmost byte of the pattern isn’t entered in the skip table; rightmost byte of the pattern isn't entered in the skip table;
if we get that value for a mismatch, we know for sure that the if we get that value for a mismatch, we know for sure that the
right end of the pattern has already passed the mismatch right end of the pattern has already passed the mismatch
location, so this is not a relevant byte for skipping purposes */ location, so this is not a relevant byte for skipping purposes */
@ -61,27 +61,27 @@
/* Compare the pattern and the buffer location, searching from /* Compare the pattern and the buffer location, searching from
high memory toward low (right to left) */ high memory toward low (right to left) */
while (*WorkingPatternPtr— == *WorkingBufferPtr—) { while (*WorkingPatternPtr— == *WorkingBufferPtr—) {
/* If we’ve matched the entire pattern, it’s a match */ /* If we've matched the entire pattern, it's a match */
if (-CompCount == 0) if (-CompCount == 0)
/* Return a pointer to the start of the match location */ /* Return a pointer to the start of the match location */
return(BufferPtr - PatternLength + 1); return(BufferPtr - PatternLength + 1);
} }
/* It’s a mismatch; let’s see what we can learn from it */ /* It's a mismatch; let's see what we can learn from it */
WorkingBufferPtr++; /* point back to the mismatch location */ WorkingBufferPtr++; /* point back to the mismatch location */
/* # of bytes that did match */ /* # of bytes that did match */
DistanceMatched = BufferPtr - WorkingBufferPtr; DistanceMatched = BufferPtr - WorkingBufferPtr;
/*If, based on the mismatch character, we can’t even skip ahead /*If, based on the mismatch character, we can't even skip ahead
as far as where we started this particular comparison, then as far as where we started this particular comparison, then
just advance by 1 to the next potential match; otherwise, just advance by 1 to the next potential match; otherwise,
skip ahead from the mismatch location by the skip distance skip ahead from the mismatch location by the skip distance
for the mismatch character */ for the mismatch character */
if (SkipTable[*WorkingBufferPtr] <= DistanceMatched) if (SkipTable[*WorkingBufferPtr] <= DistanceMatched)
Skip = 1; /* skip doesn’t do any good, advance by 1 */ Skip = 1; /* skip doesn't do any good, advance by 1 */
else else
/* Use skip value, accounting for distance covered by the /* Use skip value, accounting for distance covered by the
partial match */ partial match */
Skip = SkipTable[*WorkingBufferPtr] - DistanceMatched; Skip = SkipTable[*WorkingBufferPtr] - DistanceMatched;
/* If skipping ahead would exhaust the buffer, we’re done /* If skipping ahead would exhaust the buffer, we're done
without a match */ without a match */
if (Skip >= BufferLength) return(NULL); if (Skip >= BufferLength) return(NULL);
/* Skip ahead and perform the next comparison */ /* Skip ahead and perform the next comparison */
@ -113,48 +113,48 @@
int Handle; int Handle;
unsigned int WorkingLength; unsigned int WorkingLength;
printf(“File to search:”); printf("File to search:");
gets(Filename); gets(Filename);
printf(“Pattern for which to search:”); printf("Pattern for which to search:");
gets(Pattern); gets(Pattern);
if ( (Handle = open(Filename, O_RDONLY | O_BINARY)) == -1 ) { if ( (Handle = open(Filename, O_RDONLY | O_BINARY)) == -1 ) {
printf(“Can’t open file: %s\n”, Filename); exit(1); printf("Can't open file: %s\n", Filename); exit(1);
} }
/* Get memory in which to buffer the data */ /* Get memory in which to buffer the data */
if ( (TestBuffer=(unsigned char *)malloc(BUFFER_SIZE+1)) == NULL) { if ( (TestBuffer=(unsigned char *)malloc(BUFFER_SIZE+1)) == NULL) {
printf(“Can’t get enough memory\n”); exit(1); printf("Can't get enough memory\n"); exit(1);
} }
/* Process a BUFFER_SIZE chunk */ /* Process a BUFFER_SIZE chunk */
if ( (int)(WorkingLength = if ( (int)(WorkingLength =
read(Handle, TestBuffer, BUFFER_SIZE)) == -1 ) { read(Handle, TestBuffer, BUFFER_SIZE)) == -1 ) {
printf(“Error reading file %s\n”, Filename); exit(1); printf("Error reading file %s\n", Filename); exit(1);
} }
TestBuffer[WorkingLength] = 0; /* 0-terminate buffer for printf */ TestBuffer[WorkingLength] = 0; /* 0-terminate buffer for printf */
/* Search for the pattern and report the results */ /* Search for the pattern and report the results */
if ((MatchPtr = FindString(TestBuffer, WorkingLength, Pattern, if ((MatchPtr = FindString(TestBuffer, WorkingLength, Pattern,
(unsigned int) strlen(Pattern))) == NULL) { (unsigned int) strlen(Pattern))) == NULL) {
/* Pattern wasn’t found */ /* Pattern wasn't found */
printf(“\“%s\” not found\n”, Pattern); printf("\"%s\" not found\n", Pattern);
} else { } else {
/* Pattern was found. Zero-terminate TempBuffer; strncpy /* Pattern was found. Zero-terminate TempBuffer; strncpy
won’t do it if DISPLAY_LENGTH characters are copied */ won't do it if DISPLAY_LENGTH characters are copied */
TempBuffer[DISPLAY_LENGTH] = 0; TempBuffer[DISPLAY_LENGTH] = 0;
printf(“\“%s\” found. Next %d characters at match:\n\”%s\“\n”, printf("\"%s\" found. Next %d characters at match:\n\"%s\"\n",
Pattern, DISPLAY_LENGTH, Pattern, DISPLAY_LENGTH,
strncpy(TempBuffer, MatchPtr, DISPLAY_LENGTH)); strncpy(TempBuffer, MatchPtr, DISPLAY_LENGTH));
} }
exit(0); exit(0);
} }
Well, architecture carries a lot of weight, but it sure as heck isn’t Well, architecture carries a lot of weight, but it sure as heck isn't
destiny. I had simply fallen into the trap of figuring that the destiny. I had simply fallen into the trap of figuring that the
algorithm was so clever that I didn’t have to do any thinking myself. algorithm was so clever that I didn't have to do any thinking myself.
The path leading to **REPNZ SCASB** from the original brute-force The path leading to **REPNZ SCASB** from the original brute-force
approach of **REPZ CMPSB** at every location had been based on my approach of **REPZ CMPSB** at every location had been based on my
observation that the first character comparison at each buffer location observation that the first character comparison at each buffer location
usually fails. Why not apply the same concept to Boyer-Moore? Listing usually fails. Why not apply the same concept to Boyer-Moore? Listing
14.3 is just like the standard implementation—except that it’s optimized 14.3 is just like the standard implementation—except that it's optimized
to handle a first-comparison mismatch as quickly as possible in the loop to handle a first-comparison mismatch as quickly as possible in the loop
at **QuickSearchLoop**, much as **REPNZ SCASB** optimizes at **QuickSearchLoop**, much as **REPNZ SCASB** optimizes
first-comparison mismatches for the brute-force approach. The results in first-comparison mismatches for the brute-force approach. The results in

View file

@ -28,14 +28,14 @@
public _FindString public _FindString
_FindString proc near _FindString proc near
cld cld
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;point to our stack frame mov bp,sp ;point to our stack frame
push si ;preserve caller’s register variables push si ;preserve caller's register variables
push di push di
sub sp,256*2 ;allocate space for SkipTable sub sp,256*2 ;allocate space for SkipTable
; Create the table of distances by which to skip ahead on mismatches ; Create the table of distances by which to skip ahead on mismatches
; for every possible byte value. First, initialize all skips to the ; for every possible byte value. First, initialize all skips to the
; pattern length; this is the skip distance for bytes that don’t ; pattern length; this is the skip distance for bytes that don't
; appear in the pattern. ; appear in the pattern.
mov ax,[bp+PatternLength] mov ax,[bp+PatternLength]
and ax,ax ;return an instant match if the pattern is and ax,ax ;return an instant match if the pattern is
@ -57,8 +57,8 @@
; Set the skip values for the bytes that do appear in the pattern to ; Set the skip values for the bytes that do appear in the pattern to
; the distance from the byte location to the end of the pattern. ; the distance from the byte location to the end of the pattern.
; When there are multiple instances of the same byte, the rightmost ; When there are multiple instances of the same byte, the rightmost
; instance’s skip value is used. Note that the rightmost byte of the ; instance's skip value is used. Note that the rightmost byte of the
; pattern isn’t entered in the skip table; if we get that value for ; pattern isn't entered in the skip table; if we get that value for
; a mismatch, we know for sure that the right end of the pattern has ; a mismatch, we know for sure that the right end of the pattern has
; already passed the mismatch location, so this is not a relevant byte ; already passed the mismatch location, so this is not a relevant byte
; for skipping purposes. ; for skipping purposes.
@ -85,7 +85,7 @@
mov cx,[bp+BufferLength] ;# of match locations to check mov cx,[bp+BufferLength] ;# of match locations to check
SearchLoop: SearchLoop:
mov si,sp ;point SI to SkipTable mov si,sp ;point SI to SkipTable
; Skip through until there’s a match for the rightmost pattern byte. ; Skip through until there's a match for the rightmost pattern byte.
QuickSearchLoop: QuickSearchLoop:
mov bl,[di] ;rightmost buffer byte at this location mov bl,[di] ;rightmost buffer byte at this location
cmp dl,bl ;does it match the rightmost pattern byte? cmp dl,bl ;does it match the rightmost pattern byte?
@ -114,14 +114,14 @@
mov si,[bp+PatternPtr] ;point to next-to-rightmost bytes mov si,[bp+PatternPtr] ;point to next-to-rightmost bytes
dec di ; of buffer location and pattern dec di ; of buffer location and pattern
repz cmpsb ;compare the rest of the pattern repz cmpsb ;compare the rest of the pattern
jz Match ;that’s it; we’ve found a match jz Match ;that's it; we've found a match
; It’s a mismatch; let’s see what we can learn from it. ; It's a mismatch; let's see what we can learn from it.
inc di ;compensate for 1-byte overrun of REPZ CMPSB; inc di ;compensate for 1-byte overrun of REPZ CMPSB;
; point to mismatch location in buffer ; point to mismatch location in buffer
; # of bytes that did match. ; # of bytes that did match.
mov si,[bp+BufferPtr] mov si,[bp+BufferPtr]
sub si,di sub si,di
; If, based on the mismatch character, we can’t even skip ahead as far ; If, based on the mismatch character, we can't even skip ahead as far
; as where we started this particular comparison, then just advance by ; as where we started this particular comparison, then just advance by
; 1 to the next potential match; otherwise, skip ahead from this ; 1 to the next potential match; otherwise, skip ahead from this
; comparison location by the skip distance for the mismatch character, ; comparison location by the skip distance for the mismatch character,
@ -131,14 +131,14 @@
add bx,bx ;prepare for word look-up add bx,bx ;prepare for word look-up
add bx,sp ;SP points to SkipTable add bx,sp ;SP points to SkipTable
mov cx,[bx] ;get the skip value for this mismatch mov cx,[bx] ;get the skip value for this mismatch
mov ax,1 ;assume we’ll just advance to the next mov ax,1 ;assume we'll just advance to the next
; potential match location ; potential match location
sub cx,si ;is the skip far enough to be worth taking? sub cx,si ;is the skip far enough to be worth taking?
jna MoveAhead ;no, go with the default advance of 1 jna MoveAhead ;no, go with the default advance of 1
mov ax,cx ;yes; this is the distance to skip ahead from mov ax,cx ;yes; this is the distance to skip ahead from
; the last potential match location checked ; the last potential match location checked
MoveAhead: MoveAhead:
; Skip ahead and perform the next comparison, if there’s any buffer ; Skip ahead and perform the next comparison, if there's any buffer
; left to check. ; left to check.
mov di,[bp+BufferPtr] mov di,[bp+BufferPtr]
add di,ax ;BufferPtr += Skip; add di,ax ;BufferPtr += Skip;
@ -158,9 +158,9 @@
Done: Done:
cld ;restore default direction flag cld ;restore default direction flag
add sp,256*2 ;deallocate space for SkipTable add sp,256*2 ;deallocate space for SkipTable
pop di ;restore caller’s register variables pop di ;restore caller's register variables
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
_FindString endp _FindString endp
end end

View file

@ -9,12 +9,12 @@ good as or better than—sometimes *much* better than—brute-force
searching. (For short patterns, you might want to use **REPNZ SCASB,** searching. (For short patterns, you might want to use **REPNZ SCASB,**
thereby getting the best of both worlds.) thereby getting the best of both worlds.)
Know your data and use your smarts. Don’t stop thinking just because Know your data and use your smarts. Don't stop thinking just because
you’re implementing a big-name algorithm; you know more than it does. you're implementing a big-name algorithm; you know more than it does.
### Further Optimization of Boyer-Moore {#Heading6} ### Further Optimization of Boyer-Moore {#Heading6}
We can do substantially better yet than Listing 14.3 if we’re willing to We can do substantially better yet than Listing 14.3 if we're willing to
accept tighter limits on the data. Limiting the length of the accept tighter limits on the data. Limiting the length of the
searched-for pattern to a maximum of 255 bytes allows us to use the searched-for pattern to a maximum of 255 bytes allows us to use the
**XLAT** instruction and generally tighten the critical loop. (Be aware, **XLAT** instruction and generally tighten the critical loop. (Be aware,
@ -35,7 +35,7 @@ about 60 percent faster than Listing 14.3.
; Requires that the pattern be no longer than 255 bytes, and that ; Requires that the pattern be no longer than 255 bytes, and that
; there be a match for the pattern somewhere in the buffer (ie., a ; there be a match for the pattern somewhere in the buffer (ie., a
; copy of the pattern should be placed as a sentinel at the end of ; copy of the pattern should be placed as a sentinel at the end of
; the buffer if the pattern isn’t already known to be in the buffer). ; the buffer if the pattern isn't already known to be in the buffer).
; Tested with TASM. ; Tested with TASM.
; C near-callable as: ; C near-callable as:
; unsigned char * FindString(unsigned char * BufferPtr, ; unsigned char * FindString(unsigned char * BufferPtr,
@ -58,14 +58,14 @@ about 60 percent faster than Listing 14.3.
public _FindString public _FindString
_FindString proc near _FindString proc near
cld cld
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;point to our stack frame mov bp,sp ;point to our stack frame
push si ;preserve caller’s register variables push si ;preserve caller's register variables
push di push di
sub sp,256 ;allocate space for SkipTable sub sp,256 ;allocate space for SkipTable
; Create the table of distances by which to skip ahead on mismatches ; Create the table of distances by which to skip ahead on mismatches
; for every possible byte value. First, initialize all skips to the ; for every possible byte value. First, initialize all skips to the
; pattern length; this is the skip distance for bytes that don’t ; pattern length; this is the skip distance for bytes that don't
; appear in the pattern. ; appear in the pattern.
mov di,ds mov di,ds
mov es,di ;ES=DS=SS mov es,di ;ES=DS=SS
@ -106,7 +106,7 @@ about 60 percent faster than Listing 14.3.
mov bx,sp ;point to SkipTable for XLAT mov bx,sp ;point to SkipTable for XLAT
SearchLoop: SearchLoop:
sub ah,ah ;used to convert AL to a word sub ah,ah ;used to convert AL to a word
; Skip through until there’s a match for the first pattern byte. ; Skip through until there's a match for the first pattern byte.
QuickSearchLoop: QuickSearchLoop:
; See if we have a match at the first buffer location. ; See if we have a match at the first buffer location.
REPT 8 ;unroll loop 8 times to reduce branching REPT 8 ;unroll loop 8 times to reduce branching
@ -132,21 +132,21 @@ about 60 percent faster than Listing 14.3.
dec di ;point to next destination byte to compare (SI dec di ;point to next destination byte to compare (SI
; points to next-to-rightmost source byte) ; points to next-to-rightmost source byte)
repz cmpsb ;compare the rest of the pattern repz cmpsb ;compare the rest of the pattern
jz Match ;that’s it; we’ve found a match jz Match ;that's it; we've found a match
; It’s a mismatch; let’s see what we can learn from it. ; It's a mismatch; let's see what we can learn from it.
inc di ;compensate for 1-byte overrun of REPZ CMPSB; inc di ;compensate for 1-byte overrun of REPZ CMPSB;
; point to mismatch location in buffer ; point to mismatch location in buffer
; # of bytes that did match. ; # of bytes that did match.
mov si,[bp+BufferPtr] mov si,[bp+BufferPtr]
sub si,di sub si,di
; If, based on the mismatch character, we can’t even skip ahead as far ; If, based on the mismatch character, we can't even skip ahead as far
; as where we started this particular comparison, then just advance by ; as where we started this particular comparison, then just advance by
; 1 to the next potential match; otherwise, skip ahead from this ; 1 to the next potential match; otherwise, skip ahead from this
; comparison location by the skip distance for the mismatch character, ; comparison location by the skip distance for the mismatch character,
; less the distance covered by the partial match. ; less the distance covered by the partial match.
mov al,[di] ;get the value of the mismatch byte in buffer mov al,[di] ;get the value of the mismatch byte in buffer
xlat ;get the skip value for this mismatch xlat ;get the skip value for this mismatch
mov cx,1 ;assume we’ll just advance to the next mov cx,1 ;assume we'll just advance to the next
; potential match location ; potential match location
sub ax,si ;is the skip far enough to be worth taking? sub ax,si ;is the skip far enough to be worth taking?
jna MoveAhead ;no, go with the default advance of 1 jna MoveAhead ;no, go with the default advance of 1
@ -167,9 +167,9 @@ about 60 percent faster than Listing 14.3.
Done: Done:
cld ;restore default direction flag cld ;restore default direction flag
add sp,256 ;deallocate space for SkipTable add sp,256 ;deallocate space for SkipTable
pop di ;restore caller’s register variables pop di ;restore caller's register variables
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
_FindString endp _FindString endp
end end
@ -182,15 +182,15 @@ it as a parameter.
### Know What You Know {#Heading7} ### Know What You Know {#Heading7}
Here we’ve turned up our nose at a repeated string instruction, we’ve Here we've turned up our nose at a repeated string instruction, we've
gone against the grain by comparing backward, and yet we’ve speeded up gone against the grain by comparing backward, and yet we've speeded up
our code quite a bit. All this without any restrictions or special our code quite a bit. All this without any restrictions or special
requirements (excluding Listing 14.4)—and without any new information. requirements (excluding Listing 14.4)—and without any new information.
Everything we needed was sitting there all along; we just needed to Everything we needed was sitting there all along; we just needed to
think to look at it. think to look at it.
As Yogi Berra might put it, “You don’t know what you know until you know As Yogi Berra might put it, "You don't know what you know until you know
it.” it."
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](14-05.html) [Table of Contents](index.html) [Next](15-01.html) [Previous](14-05.html) [Table of Contents](index.html) [Next](15-01.html)

View file

@ -11,9 +11,9 @@ Chapter 15\
After 21 years, this story still makes me wince. Oh, the humiliations I After 21 years, this story still makes me wince. Oh, the humiliations I
suffer for your enlightenment.... suffer for your enlightenment....
It wasn’t until ninth grade that I had my first real girlfriend. Okay, It wasn't until ninth grade that I had my first real girlfriend. Okay,
maybe I was a little socially challenged as a kid, but hey, show me a maybe I was a little socially challenged as a kid, but hey, show me a
good programmer who wasn’t; it goes with the territory. Her name was good programmer who wasn't; it goes with the territory. Her name was
Jeannie Schweigert, and she was about four feet tall, pretty enough, and Jeannie Schweigert, and she was about four feet tall, pretty enough, and
female—and willing to go out with me, which made her approximately as female—and willing to go out with me, which made her approximately as
attractive as Cheryl Tiegs, in my book. attractive as Cheryl Tiegs, in my book.
@ -21,13 +21,13 @@ attractive as Cheryl Tiegs, in my book.
Jeannie and I hung out together at school, and went to basketball games Jeannie and I hung out together at school, and went to basketball games
and a few parties together, but somehow the two of us were never alone. and a few parties together, but somehow the two of us were never alone.
Being 14, neither of us could drive, so her parents tended to end up Being 14, neither of us could drive, so her parents tended to end up
chauffeuring us. That’s a next-to-ideal arrangement, I now realize, chauffeuring us. That's a next-to-ideal arrangement, I now realize,
having a daughter of my own (ideal being exiling all males between the having a daughter of my own (ideal being exiling all males between the
ages of 12 and 18 to Tasmania), but at the time, it drove me nuts. You ages of 12 and 18 to Tasmania), but at the time, it drove me nuts. You
see...ahem...I had never actually kissed Jeannie—or anyone, for that see...ahem...I had never actually kissed Jeannie—or anyone, for that
matter, unless you count maiden aunts and the like—and I was dying to. matter, unless you count maiden aunts and the like—and I was dying to.
At the same time, I was terrified at the prospect. What if I turned out At the same time, I was terrified at the prospect. What if I turned out
to be no good at it? It wasn’t as if I could go to Kisses ‘R’ Us and to be no good at it? It wasn't as if I could go to Kisses ‘R' Us and
take lessons. take lessons.
My long-awaited opportunity finally came after a basketball game. For a My long-awaited opportunity finally came after a basketball game. For a
@ -46,27 +46,27 @@ try, though, I did manage to get the hang of this kissing business, and
eventually even went on to have a child. (Not with Jeannie, I might add; eventually even went on to have a child. (Not with Jeannie, I might add;
the mind boggles at the mess I could have made of *that* with her.) As the mind boggles at the mess I could have made of *that* with her.) As
it turns out, none of that stuff is particularly difficult; in fact, it turns out, none of that stuff is particularly difficult; in fact,
it’s kind of enjoyable, wink, wink, say no more. it's kind of enjoyable, wink, wink, say no more.
When you’re dealing with something new, a little knowledge goes a long When you're dealing with something new, a little knowledge goes a long
way. When it comes to kissing, we have to fumble along the learning way. When it comes to kissing, we have to fumble along the learning
curve on our own, but there are all sorts of resources to help speed up curve on our own, but there are all sorts of resources to help speed up
the learning process when it comes to programming. The basic mechanisms the learning process when it comes to programming. The basic mechanisms
of programming—searches, sorts, parsing, and the like—are of programming—searches, sorts, parsing, and the like—are
well-understood and superbly well-documented. Treat yourself to a book well-understood and superbly well-documented. Treat yourself to a book
like *Algorithms,* by Robert Sedgewick (Addison Wesley), or Knuth’s *The like *Algorithms,* by Robert Sedgewick (Addison Wesley), or Knuth's *The
Art of Computer Programming* series (also from Addison Wesley; and where Art of Computer Programming* series (also from Addison Wesley; and where
was Knuth with *The Art of Kissing* when I needed him?), or practically was Knuth with *The Art of Kissing* when I needed him?), or practically
anything by Jon Bentley, and when you tackle a new area, give yourself a anything by Jon Bentley, and when you tackle a new area, give yourself a
head start. There’s still plenty of room for inventiveness and head start. There's still plenty of room for inventiveness and
creativity on your part, but why not apply that energy on top of the creativity on your part, but why not apply that energy on top of the
knowledge that’s already been gained, instead of reinventing the wheel? knowledge that's already been gained, instead of reinventing the wheel?
I know, reinventing the wheel is just the kind of challenge programmers I know, reinventing the wheel is just the kind of challenge programmers
love—but can you really afford to waste the time? And do you honestly love—but can you really afford to waste the time? And do you honestly
think that you’re so smart that you can out-think Knuth, who’s spent a think that you're so smart that you can out-think Knuth, who's spent a
lifetime at this stuff and happens to be a genius? lifetime at this stuff and happens to be a genius?
Maybe you can—but I sure can’t. For example, consider the evolution of Maybe you can—but I sure can't. For example, consider the evolution of
my understanding of linked lists. my understanding of linked lists.
### Linked Lists {#Heading3} ### Linked Lists {#Heading3}
@ -85,12 +85,12 @@ nodes; and an inability to backtrack, unless doubly-linked lists are
used. Unfortunately, doubly linked lists need more memory, as well as used. Unfortunately, doubly linked lists need more memory, as well as
processing time to maintain the backward links. processing time to maintain the backward links.
Linked lists aren’t very good for most types of sorts. Insertion and Linked lists aren't very good for most types of sorts. Insertion and
bubble sorts work fine, but more sophisticated sorts depend on efficient bubble sorts work fine, but more sophisticated sorts depend on efficient
random access, which linked lists don’t provide. Likewise, you wouldn’t random access, which linked lists don't provide. Likewise, you wouldn't
want to do a binary search on a linked list. On the other hand, linked want to do a binary search on a linked list. On the other hand, linked
lists are ideal for applications where nothing more than sequential lists are ideal for applications where nothing more than sequential
access is needed to data that’s always sorted or nearly sorted. access is needed to data that's always sorted or nearly sorted.
Consider a polygon fill function, for example. Polygon edges are added Consider a polygon fill function, for example. Polygon edges are added
to the active edge list in x-sorted order, and tend to stay pretty to the active edge list in x-sorted order, and tend to stay pretty
@ -100,15 +100,15 @@ best. Moreover, linked lists are straightforward to implement, and with
linked lists an arbitrary number of polygon edges can be handled with no linked lists an arbitrary number of polygon edges can be handled with no
fuss. All in all, linked lists work beautifully for filling polygons. fuss. All in all, linked lists work beautifully for filling polygons.
For an example of the use of linked lists in polygon filling, see my For an example of the use of linked lists in polygon filling, see my
column in the May 1991 issue of *Dr. Dobb’s Journal.* Be warned, though, column in the May 1991 issue of *Dr. Dobb's Journal.* Be warned, though,
that none of the following optimizations are to be found in that column. that none of the following optimizations are to be found in that column.
You see, that column was my first heavy-duty use of linked lists, and You see, that column was my first heavy-duty use of linked lists, and
they seemed so simple that I didn’t even open Sedgewick or Knuth. For they seemed so simple that I didn't even open Sedgewick or Knuth. For
hashing or Boyer-Moore searching, sure, I’d have done my homework first; hashing or Boyer-Moore searching, sure, I'd have done my homework first;
but linked lists seemed too obvious to bother. I was much more concerned but linked lists seemed too obvious to bother. I was much more concerned
with the polygon-related aspects of the implementation, and, in truth, I with the polygon-related aspects of the implementation, and, in truth, I
gave the linked list implementation not a moment’s thought before I gave the linked list implementation not a moment's thought before I
began coding. Heck, I had handled *much* tougher programming problems in began coding. Heck, I had handled *much* tougher programming problems in
the past; surely it would be faster to figure this one out on my own the past; surely it would be faster to figure this one out on my own
than to look it up. than to look it up.
@ -125,18 +125,18 @@ perspective, however, there are serious flaws with this model.
The fundamental problem is that the model of Figure 15.1 unnecessarily The fundamental problem is that the model of Figure 15.1 unnecessarily
complicates link manipulation. In order to delete a node, for example, complicates link manipulation. In order to delete a node, for example,
you must change the preceding node’s **NextNode** pointer to point to you must change the preceding node's **NextNode** pointer to point to
the following node, as shown in Listing 15.1. (Listing 15.2 is the the following node, as shown in Listing 15.1. (Listing 15.2 is the
header file LLIST.H, which is **\#include**d by all the linked list header file LLIST.H, which is **\#include**d by all the linked list
listings in this chapter.) Easy enough—unless the preceding node happens listings in this chapter.) Easy enough—unless the preceding node happens
to be the head pointer, which doesn’t *have* a **NextNode** field, to be the head pointer, which doesn't *have* a **NextNode** field,
because it’s not a node, so Listing 15.1 won’t work. Cumbersome special because it's not a node, so Listing 15.1 won't work. Cumbersome special
code and extra information (a pointer to the head of the list) are code and extra information (a pointer to the head of the list) are
required to handle the head-pointer case, as shown in Listing 15.3. required to handle the head-pointer case, as shown in Listing 15.3.
(I’ll grant you that if you make the next-node pointer the first field (I'll grant you that if you make the next-node pointer the first field
in the **LinkNode** structure, at offset 0, then you could successfully in the **LinkNode** structure, at offset 0, then you could successfully
point to the head pointer and pretend it was a **LinkNode** point to the head pointer and pretend it was a **LinkNode**
structure—but that’s an ugly and potentially dangerous trick, and we’ll structure—but that's an ugly and potentially dangerous trick, and we'll
see a better approach next.) see a better approach next.)
![](images/15-01.jpg)\ ![](images/15-01.jpg)\

View file

@ -9,7 +9,7 @@
the head-of-list pointer is required. Returns the same pointer the head-of-list pointer is required. Returns the same pointer
that was passed in. */ that was passed in. */
#include “llist.h” #include "llist.h"
struct LinkNode *DeleteNodeAfter(struct LinkNode *NodeToDeleteAfter) struct LinkNode *DeleteNodeAfter(struct LinkNode *NodeToDeleteAfter)
{ {
NodeToDeleteAfter->NextNode = NodeToDeleteAfter->NextNode =
@ -41,7 +41,7 @@
indicated node. List is headed by a head-of-list pointer; if the indicated node. List is headed by a head-of-list pointer; if the
pointer to the node to delete after points to the head-of-list pointer to the node to delete after points to the head-of-list
pointer, special handling is performed. */ pointer, special handling is performed. */
#include “llist.h” #include "llist.h"
struct LinkNode *DeleteNodeAfter(struct LinkNode **HeadOfListPtr, struct LinkNode *DeleteNodeAfter(struct LinkNode **HeadOfListPtr,
struct LinkNode *NodeToDeleteAfter) struct LinkNode *NodeToDeleteAfter)
{ {
@ -56,7 +56,7 @@
return(NodeToDeleteAfter); return(NodeToDeleteAfter);
} }
However, it is true that if you’re going to store a variety of types of However, it is true that if you're going to store a variety of types of
structures in your linked lists, you should start each node with the structures in your linked lists, you should start each node with the
**LinkNode** field. That way, the link pointer is in the same place in **LinkNode** field. That way, the link pointer is in the same place in
*every* structure, and the same linked list code can handle all of the *every* structure, and the same linked list code can handle all of the
@ -65,18 +65,18 @@ This is a less than elegant approach, but it works. C++ can handle data
mixing more cleanly than C, via derivation from a base link-node class. mixing more cleanly than C, via derivation from a base link-node class.
Note that Listings 15.1 and 15.3 have to specify the linked-list delete Note that Listings 15.1 and 15.3 have to specify the linked-list delete
operation as “delete the *next* node,” rather than “delete this node,” operation as "delete the *next* node," rather than "delete this node,"
because in order to relink it’s necessary to access the **NextNode** because in order to relink it's necessary to access the **NextNode**
field of the node preceding the node to be deleted, and it’s impossible field of the node preceding the node to be deleted, and it's impossible
to backtrack in a singly linked list. For this reason, singly-linked to backtrack in a singly linked list. For this reason, singly-linked
list operations tend to work with the structure preceding the one of list operations tend to work with the structure preceding the one of
interest—and that makes the problem of having to special-case the head interest—and that makes the problem of having to special-case the head
pointer all the more acute. pointer all the more acute.
Similar problems with the head pointer crop up when you’re inserting Similar problems with the head pointer crop up when you're inserting
nodes, and in fact in all link manipulation code. It’s easy to end up nodes, and in fact in all link manipulation code. It's easy to end up
working with either pointers to pointers or lots of special-case code, working with either pointers to pointers or lots of special-case code,
and while those approaches work, they’re inelegant and inefficient. and while those approaches work, they're inelegant and inefficient.
### Dummies and Sentinels {#Heading4} ### Dummies and Sentinels {#Heading4}
@ -84,13 +84,13 @@ A far better approach is to use a *dummy node* for the head of the list,
as shown in Figure 15.2. I invented this one for myself the next time I as shown in Figure 15.2. I invented this one for myself the next time I
encountered linked lists, while designing a seed fill function for encountered linked lists, while designing a seed fill function for
MetaWindows, back during my tenure at Metagraphics Corp. But I could MetaWindows, back during my tenure at Metagraphics Corp. But I could
have learned it by spending five minutes with Sedgewick’s book. have learned it by spending five minutes with Sedgewick's book.
![](images/15-02.jpg)\ ![](images/15-02.jpg)\
**Figure 15.2**  *Using a dummy head and tail node with a linked list.* **Figure 15.2**  *Using a dummy head and tail node with a linked list.*
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 Likewise, there should be a separate node for the tail of the list, so
@ -120,7 +120,7 @@ value has to perform two tests in the inner loop, as shown in Listing
NULL pointer if no such value was found. Assumes the list is NULL pointer if no such value was found. Assumes the list is
terminated with a tail node pointing to itself as the next node. */ terminated with a tail node pointing to itself as the next node. */
#include <stdio.h> #include <stdio.h>
#include “llist.h” #include "llist.h"
struct LinkNode *FindNodeBeforeValueNotLess( struct LinkNode *FindNodeBeforeValueNotLess(
struct LinkNode *HeadOfListNode, int SearchValue) struct LinkNode *HeadOfListNode, int SearchValue)
{ {
@ -140,10 +140,10 @@ value has to perform two tests in the inner loop, as shown in Listing
Suppose, however, that we make the tail node a *sentinel* by giving it a Suppose, however, that we make the tail node a *sentinel* by giving it a
value that is guaranteed to terminate the search, as shown in Figure value that is guaranteed to terminate the search, as shown in Figure
15.4. The list in Figure 15.4 has a sentinel with a value field of 15.4. The list in Figure 15.4 has a sentinel with a value field of
32,767; since we’re working with integers, that’s the highest possible 32,767; since we're working with integers, that's the highest possible
search value, and is guaranteed to satisfy any search that comes down search value, and is guaranteed to satisfy any search that comes down
the pike. The success or failure of the search can then be determined the pike. The success or failure of the search can then be determined
outside the loop, if necessary, by checking for the tail node’s special outside the loop, if necessary, by checking for the tail node's special
pointer—but the inside of the loop is streamlined to just one test, as pointer—but the inside of the loop is streamlined to just one test, as
shown in Listing 15.5. Not all linked lists lend themselves to shown in Listing 15.5. Not all linked lists lend themselves to
sentinels, but the performance benefits are considerable for those lend sentinels, but the performance benefits are considerable for those lend

View file

@ -12,7 +12,7 @@
containing the largest possible Value field setting and pointing containing the largest possible Value field setting and pointing
to itself as the next node. */ to itself as the next node. */
#include <stdio.h> #include <stdio.h>
#include “llist.h” #include "llist.h"
struct LinkNode *FindNodeBeforeValueNotLess( struct LinkNode *FindNodeBeforeValueNotLess(
struct LinkNode *HeadOfListNode, int SearchValue) struct LinkNode *HeadOfListNode, int SearchValue)
{ {
@ -35,19 +35,19 @@ One minor but elegant refinement yet remains: Use a single node as both
the head *and* the tail of the list. We can do this by connecting the the head *and* the tail of the list. We can do this by connecting the
last node back to the first through the head/tail node in a circular last node back to the first through the head/tail node in a circular
fashion, as shown in Figure 15.5. This head/tail node can also, of fashion, as shown in Figure 15.5. This head/tail node can also, of
course, be a sentinel; when it’s necessary to check for the end of the course, be a sentinel; when it's necessary to check for the end of the
list explicitly, that can be done by comparing the current node pointer list explicitly, that can be done by comparing the current node pointer
to the head pointer. If they’re equal, you’re at the head/tail node. to the head pointer. If they're equal, you're at the head/tail node.
Why am I so fond of this circular list architecture? For one thing, it Why am I so fond of this circular list architecture? For one thing, it
saves a node, and most of my linked list programming has been done in saves a node, and most of my linked list programming has been done in
severely memory-constrained environments. Mostly, though, it’s just so severely memory-constrained environments. Mostly, though, it's just so
*neat;* with this setup, there’s not a single node or inner-loop *neat;* with this setup, there's not a single node or inner-loop
instruction wasted. Perfect economy of programming, if you ask me. instruction wasted. Perfect economy of programming, if you ask me.
I must admit that I racked my brains for quite a while to come up with I must admit that I racked my brains for quite a while to come up with
the circular list, simple as it may seem. Shortly after coming up with the circular list, simple as it may seem. Shortly after coming up with
it, I happened to look in Sedgewick’s book, only to find my nifty it, I happened to look in Sedgewick's book, only to find my nifty
optimization described plain as day; and a little while after *that,* I optimization described plain as day; and a little while after *that,* I
came across a thread in the algorithms/computer.sci topic on BIX that came across a thread in the algorithms/computer.sci topic on BIX that
described it in considerable detail. Folks, the information is out described it in considerable detail. Folks, the information is out
@ -65,7 +65,7 @@ illustrates the use of the linked-list functions in Listings 15.1 and
Contrast Figure 15.5 with Figure 15.1, and Listings 15.1, 15.5, 15.6, Contrast Figure 15.5 with Figure 15.1, and Listings 15.1, 15.5, 15.6,
and 15.7 with Listings 15.3 and 15.4. Yes, linked lists are simple, but and 15.7 with Listings 15.3 and 15.4. Yes, linked lists are simple, but
not so simple that a little knowledge doesn’t make a substantial not so simple that a little knowledge doesn't make a substantial
difference. Make it a habit to read Knuth or Sedgewick or the like difference. Make it a habit to read Knuth or Sedgewick or the like
before you write a single line of code. before you write a single line of code.
@ -82,7 +82,7 @@ before you write a single line of code.
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include “llist.h” #include "llist.h"
/* Initializes an empty linked list of LinkNode structures, /* Initializes an empty linked list of LinkNode structures,
consisting of a single head/tail/sentinel node, and returns a consisting of a single head/tail/sentinel node, and returns a
pointer to the list. Returns NULL for failure. */ pointer to the list. Returns NULL for failure. */
@ -94,7 +94,7 @@ before you write a single line of code.
return(NULL); return(NULL);
Sentinel->NextNode = Sentinel; Sentinel->NextNode = Sentinel;
Sentinel->Value = SENTINEL; Sentinel->Value = SENTINEL;
strcpy(Sentinel->Text, “*** sentinel ***”); strcpy(Sentinel->Text, "*** sentinel ***");
return(Sentinel); return(Sentinel);
} }

View file

@ -72,7 +72,7 @@
#include <conio.h> #include <conio.h>
#include <ctype.h> #include <ctype.h>
#include <string.h> #include <string.h>
#include “llist.h” #include "llist.h"
void main() void main()
{ int Done = 0, Char, TempValue; { int Done = 0, Char, TempValue;
@ -80,60 +80,60 @@
char TempBuffer[MAX_TEXT_LENGTH+3]; char TempBuffer[MAX_TEXT_LENGTH+3];
if ((ListPtr = InitLinkedList()) == NULL) { if ((ListPtr = InitLinkedList()) == NULL) {
printf(“Out of memory\n”); printf("Out of memory\n");
exit(1); exit(1);
} }
while (!Done) { while (!Done) {
printf(“\nA=add; D=delete; F=find; L=list all; Q=quit\n>”); printf("\nA=add; D=delete; F=find; L=list all; Q=quit\n>");
Char = toupper(getche()); Char = toupper(getche());
printf(“\n”); printf("\n");
switch (Char) { switch (Char) {
case 'A': /* add a node */ case 'A': /* add a node */
if ((TempPtr = malloc(sizeof(struct LinkNode))) == NULL) if ((TempPtr = malloc(sizeof(struct LinkNode))) == NULL)
{ {
printf(“Out of memory\n ); printf("Out of memory\n );
exit(1); exit(1);
} }
printf(“Node value: ”); printf("Node value: ");
scanf(“%d”, &TempPtr->Value); scanf("%d", &TempPtr->Value);
if ((FindNodeBeforeValue(ListPtr,TempPtr->Value))!=NULL) if ((FindNodeBeforeValue(ListPtr,TempPtr->Value))!=NULL)
{ printf(“*** value already in list; try again ***\n”); { printf("*** value already in list; try again ***\n");
free(TempPtr); free(TempPtr);
} else {printf(“Node text: ”); } else {printf("Node text: ");
TempBuffer[0] = MAX_TEXT_LENGTH; TempBuffer[0] = MAX_TEXT_LENGTH;
cgets(TempBuffer); cgets(TempBuffer);
strcpy(TempPtr->Text, &TempBuffer[2]); strcpy(TempPtr->Text, &TempBuffer[2]);
InsertNodeSorted(ListPtr, TempPtr); InsertNodeSorted(ListPtr, TempPtr);
printf(“\n”); printf("\n");
} }
break; break;
case 'D': /* delete a node */ case 'D': /* delete a node */
printf(“Value field of node to delete: ”); printf("Value field of node to delete: ");
scanf(“%d”, &TempValue); scanf("%d", &TempValue);
if ((TempPtr = FindNodeBeforeValue(ListPtr, TempValue)) if ((TempPtr = FindNodeBeforeValue(ListPtr, TempValue))
!= NULL) { != NULL) {
TempPtr2 = TempPtr->NextNode; /* -> node to delete */ TempPtr2 = TempPtr->NextNode; /* -> node to delete */
DeleteNodeAfter(TempPtr); /* delete it */ DeleteNodeAfter(TempPtr); /* delete it */
free(TempPtr2); /* free its memory */ free(TempPtr2); /* free its memory */
} else { } else {
printf(“*** no such value field in list ***\n”) printf("*** no such value field in list ***\n")
break; break;
case 'F': /* find a node */ case 'F': /* find a node */
printf(“Value field of node to find: ”); printf("Value field of node to find: ");
scanf(“%d”, &TempValue); scanf("%d", &TempValue);
if ((TempPtr = FindNodeBeforeValue(ListPtr, TempValue)) if ((TempPtr = FindNodeBeforeValue(ListPtr, TempValue))
!= NULL) != NULL)
printf(“Value: %d\nText: %s\n”, printf("Value: %d\nText: %s\n",
TempPtr->NextNode->Value, TempPtr->NextNode->Text); TempPtr->NextNode->Value, TempPtr->NextNode->Text);
else else
printf(“*** no such value field in list ***\n”); printf("*** no such value field in list ***\n");
break; break;
case 'L': /* list all nodes */ case 'L': /* list all nodes */
TempPtr = ListPtr->NextNode; /* point to first node */ TempPtr = ListPtr->NextNode; /* point to first node */
if (TempPtr == ListPtr) { /* empty if at sentinel */ if (TempPtr == ListPtr) { /* empty if at sentinel */
printf(“*** List is empty ***\n”); printf("*** List is empty ***\n");
} else { } else {
do {printf(“Value: %d\n Text: %s\n”, TempPtr->Value, do {printf("Value: %d\n Text: %s\n", TempPtr->Value,
TempPtr->Text); TempPtr->Text);
TempPtr = TempPtr->NextNode; TempPtr = TempPtr->NextNode;
} while (TempPtr != ListPtr); } while (TempPtr != ListPtr);
@ -150,8 +150,8 @@
### Hi/Lo in 24 Bytes {#Heading6} ### Hi/Lo in 24 Bytes {#Heading6}
In one of my *PC TECHNIQUES* “Pushing the Envelope” columns, I passed In one of my *PC TECHNIQUES* "Pushing the Envelope" columns, I passed
along one of David Stafford’s fiendish programming puzzles: Write a along one of David Stafford's fiendish programming puzzles: Write a
C-callable function to find the greatest or smallest unsigned **int**. C-callable function to find the greatest or smallest unsigned **int**.
Not a big deal—except that David had *already* done it in 24 bytes, so Not a big deal—except that David had *already* done it in 24 bytes, so
the challenge was to do it in 24 bytes or less. the challenge was to do it in 24 bytes or less.
@ -160,16 +160,16 @@ Such routines soon began coming at me from all angles. However (and I
hate to say this because some of my correspondents were *very* pleased hate to say this because some of my correspondents were *very* pleased
with the thought that they had bested David), no one has yet met the with the thought that they had bested David), no one has yet met the
challenge—because most of you folks missed a key point. When David said, challenge—because most of you folks missed a key point. When David said,
“Write a function to find the greatest or smallest unsigned **int** in "Write a function to find the greatest or smallest unsigned **int** in
24 bytes or less,” he meant, “Write the **hi** and the **lo** functions 24 bytes or less," he meant, "Write the **hi** and the **lo** functions
in 24 bytes or less—*combined*.” in 24 bytes or less—*combined*."
Oh. Oh.
Yes, a 24-byte hi/lo function is possible, anatomically improbable as it Yes, a 24-byte hi/lo function is possible, anatomically improbable as it
might seem. Which I guess goes to show that when one of David’s puzzles might seem. Which I guess goes to show that when one of David's puzzles
seems less than impossible, odds are you’re missing something. Listing seems less than impossible, odds are you're missing something. Listing
15.9 is David’s 24-byte solution, from which a lot may be learned if one 15.9 is David's 24-byte solution, from which a lot may be learned if one
reads closely enough. reads closely enough.
**LISTING 15.9 L15-9.ASM** **LISTING 15.9 L15-9.ASM**
@ -203,9 +203,9 @@ reads closely enough.
ret ret
Before I end this chapter, let me say that I get a lot of feedback from Before I end this chapter, let me say that I get a lot of feedback from
my readers, and it’s much appreciated. Keep those cards, letters, and my readers, and it's much appreciated. Keep those cards, letters, and
email messages coming. And if any of you know Jeannie Schweigert, have email messages coming. And if any of you know Jeannie Schweigert, have
her drop me a line and let me know how she’s doing these days.... her drop me a line and let me know how she's doing these days....
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](15-03.html) [Table of Contents](index.html) [Next](16-01.html) [Previous](15-03.html) [Table of Contents](index.html) [Next](16-01.html)

View file

@ -3,7 +3,7 @@
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
Chapter 16\ Chapter 16\
There Ain’t No Such Thing as the Fastest Code {#Heading1} There Ain't No Such Thing as the Fastest Code {#Heading1}
---------------------------------------------- ----------------------------------------------
### Lessons Learned in the Pursuit of the Ultimate Word Counter {#Heading2} ### Lessons Learned in the Pursuit of the Ultimate Word Counter {#Heading2}
@ -34,15 +34,15 @@ importance to buyers:
**10.**  Windows development cycle automation **10.**  Windows development cycle automation
Is something missing here? You bet your maximum *gluteus* something’s Is something missing here? You bet your maximum *gluteus* something's
missing—nowhere on that list is there so much as one word about how fast missing—nowhere on that list is there so much as one word about how fast
the compiled code runs! I’m not saying that performance is everything, the compiled code runs! I'm not saying that performance is everything,
but optimization isn’t even down there at number 10, below online help! but optimization isn't even down there at number 10, below online help!
Ye gods and little fishes! We are talking here about people who would Ye gods and little fishes! We are talking here about people who would
take a bus from LA to New York instead of a plane because it had a take a bus from LA to New York instead of a plane because it had a
cleaner bathroom; who would choose a painting from a Holiday Inn over a cleaner bathroom; who would choose a painting from a Holiday Inn over a
Matisse because it had a fancier frame; who would buy a Yugo instead Matisse because it had a fancier frame; who would buy a Yugo instead
of—well, hell, anything—because it had a nice owner’s manual and of—well, hell, anything—because it had a nice owner's manual and
particularly attractive keys. We are talking about people who are particularly attractive keys. We are talking about people who are
focusing on means, and have forgotten about ends. We are talking about focusing on means, and have forgotten about ends. We are talking about
people with no programming souls. people with no programming souls.
@ -53,7 +53,7 @@ What are we to make of this? At the very least, we can safely guess that
very few corporate buyers ever enter optimization contests. Most of my very few corporate buyers ever enter optimization contests. Most of my
readers do, however; in fact, far more than I thought ever would, but readers do, however; in fact, far more than I thought ever would, but
that gladdens me to no end. I issued my first optimization challenge in that gladdens me to no end. I issued my first optimization challenge in
a “Pushing the Envelope” column in *PC TECHNIQUES* back in 1991, and was a "Pushing the Envelope" column in *PC TECHNIQUES* back in 1991, and was
deluged by respondents who, one might also gather, do not live by *PC deluged by respondents who, one might also gather, do not live by *PC
Week*. Week*.
@ -64,11 +64,11 @@ optimization issues along the way. David did all his coding in Pascal,
pointing out that while an assembly language version would probably be pointing out that while an assembly language version would probably be
faster, his Pascal utility worked properly and was fast enough for him. faster, his Pascal utility worked properly and was fast enough for him.
It wasn’t, however, fast enough for me. The logical starting place for It wasn't, however, fast enough for me. The logical starting place for
speeding up word counting would be David’s original Pascal code, but I’m speeding up word counting would be David's original Pascal code, but I'm
much more comfortable with C, so Listing 16.1 is a loose approximation much more comfortable with C, so Listing 16.1 is a loose approximation
of David’s word count program, translated to C. I left out a few of David's word count program, translated to C. I left out a few
details, such as handling comment blocks, partly because I don’t use details, such as handling comment blocks, partly because I don't use
such blocks myself, and partly so we can focus on optimizing the core such blocks myself, and partly so we can focus on optimizing the core
word-counting code. As Table 16.1 indicates, Listing 16.1 counts the word-counting code. As Table 16.1 indicates, Listing 16.1 counts the
words in a 104,448-word file in 4.6 seconds. The file was stored on a words in a 104,448-word file in 4.6 seconds. The file was stored on a
@ -128,22 +128,22 @@ Table 16.1 Word count timings.
char *Buffer, CharFlag = 0, PredCharFlag, *BufferPtr, Ch; char *Buffer, CharFlag = 0, PredCharFlag, *BufferPtr, Ch;
if (argc != 2) { if (argc != 2) {
printf(“usage: wc <filename>\n”); printf("usage: wc <filename>\n");
exit(1); exit(1);
} }
if ((Buffer = malloc(BUFFER_SIZE)) == NULL) { if ((Buffer = malloc(BUFFER_SIZE)) == NULL) {
printf(“Can’t allocate adequate memory\n”); printf("Can't allocate adequate memory\n");
exit(1); exit(1);
} }
if ((Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1) { if ((Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1) {
printf(“Can’t open file %s\n”, argv[1]); printf("Can't open file %s\n", argv[1]);
exit(1); exit(1);
} }
if ((FileSize = filelength(Handle)) == -1) { if ((FileSize = filelength(Handle)) == -1) {
printf(“Error sizing file %s\n”, argv[1]); printf("Error sizing file %s\n", argv[1]);
exit(1); exit(1);
} }
@ -152,7 +152,7 @@ Table 16.1 Word count timings.
/* Get the next chunk */ /* Get the next chunk */
FileSize -= (BlockSize = min(FileSize, BUFFER_SIZE)); FileSize -= (BlockSize = min(FileSize, BUFFER_SIZE));
if (read(Handle, Buffer, BlockSize) == -1) { if (read(Handle, Buffer, BlockSize) == -1) {
printf(“Error reading file %s\n”, argv[1]); printf("Error reading file %s\n", argv[1]);
exit(1); exit(1);
} }
/* Count words in the chunk */ /* Count words in the chunk */
@ -162,10 +162,10 @@ Table 16.1 Word count timings.
Ch = *BufferPtr++ & 0x7F; /* strip high bit, which some Ch = *BufferPtr++ & 0x7F; /* strip high bit, which some
word processors set as an word processors set as an
internal flag */ internal flag */
CharFlag = ((Ch >= ‘a’) && (Ch <= ‘z’)) || CharFlag = ((Ch >= ‘a') && (Ch <= ‘z')) ||
((Ch >= ‘A’) && (Ch <= ‘Z’)) || ((Ch >= ‘A') && (Ch <= ‘Z')) ||
((Ch >= ‘0’) && (Ch <= ‘9’)) || ((Ch >= ‘0') && (Ch <= ‘9')) ||
(Ch == ‘\’’); (Ch == ‘\'');
if ((!CharFlag) && PredCharFlag) { if ((!CharFlag) && PredCharFlag) {
WordCo u nt++; WordCo u nt++;
} }
@ -176,7 +176,7 @@ Table 16.1 Word count timings.
if (CharFlag) { if (CharFlag) {
WordCount++; WordCount++;
} }
printf(“\nTotal words in file: %lu\n”, WordCount); printf("\nTotal words in file: %lu\n", WordCount);
return(0); return(0);
} }

View file

@ -36,22 +36,22 @@ generates.
char *Buffer, CharFlag = 0; char *Buffer, CharFlag = 0;
if (argc != 2) { if (argc != 2) {
printf(“usage: wc <filename>\n”); printf("usage: wc <filename>\n");
exit(1); exit(1);
} }
if ((Buffer = malloc(BUFFER_SIZE)) == NULL) { if ((Buffer = malloc(BUFFER_SIZE)) == NULL) {
printf(“Can’t allocate adequate memory\n”); printf("Can't allocate adequate memory\n");
exit(1); exit(1);
} }
if ((Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1) { if ((Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1) {
printf(“Can’t open file %s\n”, argv[1]); printf("Can't open file %s\n", argv[1]);
exit(1); exit(1);
} }
if ((FileSize = filelength(Handle)) == -1) { if ((FileSize = filelength(Handle)) == -1) {
printf(“Error sizing file %s\n”, argv[1]); printf("Error sizing file %s\n", argv[1]);
exit(1); exit(1);
} }
@ -59,7 +59,7 @@ generates.
while (FileSize > 0) { while (FileSize > 0) {
FileSize -= (BlockSize = min(FileSize, BUFFER_SIZE)); FileSize -= (BlockSize = min(FileSize, BUFFER_SIZE));
if (read(Handle, Buffer, BlockSize) == -1) { if (read(Handle, Buffer, BlockSize) == -1) {
printf(“Error reading file %s\n”, argv[1]); printf("Error reading file %s\n", argv[1]);
exit(1); exit(1);
} }
ScanBuffer(Buffer, BlockSize, &CharFlag, &WordCount); ScanBuffer(Buffer, BlockSize, &CharFlag, &WordCount);
@ -69,7 +69,7 @@ generates.
if (CharFlag) { if (CharFlag) {
WordCount++; WordCount++;
} }
printf(“\nTotal words in file: %lu\n”, WordCount); printf("\nTotal words in file: %lu\n", WordCount);
return(0); return(0);
} }
@ -98,9 +98,9 @@ generates.
.code .code
public _ScanBuffer public _ScanBuffer
_ScanBuffer proc near _ScanBuffer proc near
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;set up local stack frame mov bp,sp ;set up local stack frame
push si ;preserve caller’s register vars push si ;preserve caller's register vars
push di push di
mov si,[bp+Buffer] ;point to buffer to scan mov si,[bp+Buffer] ;point to buffer to scan
@ -116,19 +116,19 @@ generates.
and al,7fh ;strip high bit for word processors and al,7fh ;strip high bit for word processors
; that set it as an internal flag ; that set it as an internal flag
mov bl,1 ;assume this is a char; CharFlag = 1; mov bl,1 ;assume this is a char; CharFlag = 1;
cmp al,‘a’ ;it is a char if between a and z cmp al,‘a' ;it is a char if between a and z
jb CheckAZ jb CheckAZ
cmp al,‘z’ cmp al,‘z'
jna IsAChar jna IsAChar
CheckAZ: CheckAZ:
cmp al,‘A’ ;it is a char if between A and Z cmp al,‘A' ;it is a char if between A and Z
jb Check09 jb Check09
cmp al,‘Z’ cmp al,‘Z'
jna IsAChar jna IsAChar
Check09: Check09:
cmp al,‘0’ ;it is a char if between 0 and 9 cmp al,‘0' ;it is a char if between 0 and 9
jb CheckApostrophe jb CheckApostrophe
cmp al,‘9’ cmp al,‘9'
jna IsAChar jna IsAChar
CheckApostrophe: CheckApostrophe:
cmp al,27h ;it is a char if an apostrophe cmp al,27h ;it is a char if an apostrophe
@ -149,9 +149,9 @@ generates.
mov [bx],cx ;set new word count mov [bx],cx ;set new word count
mov [bx+2],dx mov [bx+2],dx
pop di ;restore caller’s register vars pop di ;restore caller's register vars
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
_ScanBuffer endp _ScanBuffer endp
end end
@ -160,10 +160,10 @@ generates.
We could rearrange the tests in light of the nature of the data being We could rearrange the tests in light of the nature of the data being
scanned; for example, we could perform the tests more efficiently by scanned; for example, we could perform the tests more efficiently by
taking advantage of the knowledge that if a byte is less than ‘0,’ it’s taking advantage of the knowledge that if a byte is less than ‘0,' it's
either an apostrophe or not a character at all. However, that sort of either an apostrophe or not a character at all. However, that sort of
fine-tuning is typically good for speedups of only 10 to 20 percent, and fine-tuning is typically good for speedups of only 10 to 20 percent, and
I’ve intentionally refrained from implementing this in Listing 16.3 to I've intentionally refrained from implementing this in Listing 16.3 to
avoid pointing you down the wrong path; what we need is a different tack avoid pointing you down the wrong path; what we need is a different tack
altogether. Ponder this. What we *really* want to know is nothing more altogether. Ponder this. What we *really* want to know is nothing more
than whether a byte is a character, not what sort of character it is. than whether a byte is a character, not what sort of character it is.

View file

@ -45,9 +45,9 @@
.code .code
public _ScanBuffer public _ScanBuffer
_ScanBuffer proc near _ScanBuffer proc near
push bp ;preserve caller’s stack frame push bp ;preserve caller's stack frame
mov bp,sp ;set up local stack frame mov bp,sp ;set up local stack frame
push si ;preserve caller’s register vars push si ;preserve caller's register vars
push di push di
mov si,[bp+Buffer] ;point to buffer to scan mov si,[bp+Buffer] ;point to buffer to scan
@ -62,10 +62,10 @@
and al,al ;ZF=0 if last byte was a char, and al,al ;ZF=0 if last byte was a char,
; ZF=1 if not ; ZF=1 if not
lodsb ;get the next byte lodsb ;get the next byte
;***doesn’t change flags*** ;***doesn't change flags***
xlat ;look up its char/not status xlat ;look up its char/not status
;***doesn’t change flags*** ;***doesn't change flags***
jz ScanLoopBottom ;don’t count a word if last byte was jz ScanLoopBottom ;don't count a word if last byte was
; not a character ; not a character
and al,al ;last byte was a character; is the and al,al ;last byte was a character; is the
; current byte a character? ; current byte a character?
@ -80,9 +80,9 @@
mov [bx],di ;set new word count mov [bx],di ;set new word count
mov [bx+2],dx mov [bx+2],dx
pop di ;restore caller’s register vars pop di ;restore caller's register vars
pop si pop si
pop bp ;restore caller’s stack frame pop bp ;restore caller's stack frame
ret ret
align 2 align 2
@ -104,7 +104,7 @@ the byte in a table, all with just two instruction bytes.
on an 8088, where **LODSB** and **XLAT** have a greater advantage over on an 8088, where **LODSB** and **XLAT** have a greater advantage over
conventional instructions. On the 486 and Pentium, however, **LODSB** conventional instructions. On the 486 and Pentium, however, **LODSB**
and **XLAT** lose much of their appeal, and should be replaced with and **XLAT** lose much of their appeal, and should be replaced with
**MOV** instructions.) Better yet, **LODSB** and **XLAT** don’t alter **MOV** instructions.) Better yet, **LODSB** and **XLAT** don't alter
the flags, so the Zero flag status set before **LODSB** is still around the flags, so the Zero flag status set before **LODSB** is still around
to be tested after **XLAT** . to be tested after **XLAT** .
@ -113,20 +113,20 @@ 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 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 the code that increments the word count, to avoid an extra branch back
into the loop; this replaces the more intuitive approach of jumping 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. 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 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 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 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 how understanding the nature of the data you're processing allows you to
optimize in ways the compiler can’t. *Know your data!* optimize in ways the compiler can't. *Know your data!*
So, gosh, Listing 16.4 is the best word-counting code in the universe, 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 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 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 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 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 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 scanning the buffer (as opposed to reading the file), there's potential
for those further optimizations to improve performance significantly. for those further optimizations to improve performance significantly.
(However, it is true that when access is performed to a hard rather than (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.) RAM disk, disk access jumps to about half of overall execution time.)
@ -149,14 +149,14 @@ No listings were to be longer than 200 lines. No complete programs were
to be accepted; submissions had to be plug-compatible with Listing 16.4. to be accepted; submissions had to be plug-compatible with Listing 16.4.
(This was to encourage people not to waste time optimizing outside the (This was to encourage people not to waste time optimizing outside the
inner loop.) Finally, the code had to produce the same results as inner loop.) Finally, the code had to produce the same results as
Listing 16.4; I didn’t want to see functions that approximated the word Listing 16.4; I didn't want to see functions that approximated the word
count by dividing the number of characters by six instead of counting count by dividing the number of characters by six instead of counting
actual words! actual words!
So how did the entrants in this particular challenge stack up? More than So how did the entrants in this particular challenge stack up? More than
one claimed a speed-up over my assembly word-counting code of more than one claimed a speed-up over my assembly word-counting code of more than
three times. On top of the three-times speedup over the original C code three times. On top of the three-times speedup over the original C code
that I had already realized, we’re almost up to an order of magnitude that I had already realized, we're almost up to an order of magnitude
faster. You are, of course, entitled to your own opinion, but *I* faster. You are, of course, entitled to your own opinion, but *I*
consider an order of magnitude to be significant. consider an order of magnitude to be significant.

View file

@ -2,33 +2,33 @@
[Previous](16-03.html) [Table of Contents](index.html) [Next](16-05.html) [Previous](16-03.html) [Table of Contents](index.html) [Next](16-05.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
Truth to tell, I didn’t expect a three-times speedup; around two times Truth to tell, I didn't expect a three-times speedup; around two times
was what I had in mind. Which just goes to show that any code can be was what I had in mind. Which just goes to show that any code can be
made faster than you’d expect, if you think about it long enough and made faster than you'd expect, if you think about it long enough and
from many different perspectives. (The most potent word-counting from many different perspectives. (The most potent word-counting
technique seems to be a 64K lookup table that allows handling two bytes technique seems to be a 64K lookup table that allows handling two bytes
simultaneously. This is not the sort of technique one comes up with by simultaneously. This is not the sort of technique one comes up with by
brute-force optimization.) Thinking (or, worse yet, boasting) that your brute-force optimization.) Thinking (or, worse yet, boasting) that your
code is the fastest possible is rollescating on a tightrope in a code is the fastest possible is rollescating on a tightrope in a
hurricane; you’re due for a fall, if you catch my drift. Case in point: hurricane; you're due for a fall, if you catch my drift. Case in point:
Terje Mathisen’s word-counting program. Terje Mathisen's word-counting program.
#### Blinding Yourself to a Better Approach {#Heading6} #### Blinding Yourself to a Better Approach {#Heading6}
Not so long ago, Terje Mathisen, who I introduced earlier in this book, Not so long ago, Terje Mathisen, who I introduced earlier in this book,
wrote a very fast word-counting program, and posted it on Bix. When I wrote a very fast word-counting program, and posted it on Bix. When I
say it was fast, I mean *fast;* this code was optimized like nobody’s say it was fast, I mean *fast;* this code was optimized like nobody's
business. We’re talking top-quality code here. business. We're talking top-quality code here.
When the topic of optimizing came up in one of the Bix conferences, When the topic of optimizing came up in one of the Bix conferences,
Terje’s program was mentioned, and he posted the following message: “I Terje's program was mentioned, and he posted the following message: "I
challenge BIXens (and especially **mabrash!**) to speed it up challenge BIXens (and especially **mabrash!**) to speed it up
significantly. I would consider 5 percent a good result.” The clear significantly. I would consider 5 percent a good result." The clear
implication was, “That code is as fast as it can possibly be.” implication was, "That code is as fast as it can possibly be."
Naturally, it wasn’t; there ain’t no such thing as the fastest code Naturally, it wasn't; there ain't no such thing as the fastest code
(TANSTATFC? I agree, it doesn’t have the ring of TANSTAAFL). I pored (TANSTATFC? I agree, it doesn't have the ring of TANSTAAFL). I pored
over Terje’s 386 native-mode code, and found the critical inner loop, over Terje's 386 native-mode code, and found the critical inner loop,
which was indeed as tight as one could imagine, consisting of just a few which was indeed as tight as one could imagine, consisting of just a few
386 native-mode instructions. However, one of the instructions was this: 386 native-mode instructions. However, one of the instructions was this:
@ -49,19 +49,19 @@ enough, by good fortune, to speed up the whole program by 5 percent.
------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(Granted, **CMP [*mem*],*reg*** is 1 cycle slower than **CMP (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 *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 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?) curious, both forms take 2 cycles on the 486; quite a lot faster, eh?)
#### Watch Out for Luggable Assumptions! {#Heading7} #### Watch Out for Luggable Assumptions! {#Heading7}
The first lesson to be learned here is not to lug assumptions that may 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 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 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, 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 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 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; 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 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 are what enable us to deal with the complexities of assembly language
without overloading after about 20 instructions, but they can be a major without overloading after about 20 instructions, but they can be a major
@ -73,10 +73,10 @@ counting is not the key to happiness, riches, and wondrous performance.
After getting my 5 percent speedup, I mentioned to Terje the possibility 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 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 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, 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 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 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 the old version. I don't have exact numbers, but Terje's preliminary
estimate was 80 percent faster, and word counting—*including* disk cache 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 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 allowing for the speed of the 486, those are very impressive numbers
@ -87,20 +87,20 @@ barrier that Terje faced was that he *thought* he had the fastest code
possible. Once he opened up the possibility that there were faster possible. Once he opened up the possibility that there were faster
approaches, and looked beyond the specific approach that he had so 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* 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 faster. Consider the incongruity of Terje's willingness to consider a 5
percent speedup significant in light of his later near-doubling of percent speedup significant in light of his later near-doubling of
performance. performance.
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 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 counts characters, words, and lines, can handle multiple files, and lets
you specify the characters that separate words, should you so desire. 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 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 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 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 the *fastest* word-counting code, though, because I would of course
never be so foolish as to call *anything* the fastest. never be so foolish as to call *anything* the fastest.
@ -109,13 +109,13 @@ never be so foolish as to call *anything* the fastest.
As it happened, the challenge I issued to my *PC TECHNIQUES* readers was As it happened, the challenge I issued to my *PC TECHNIQUES* readers was
a smashing success, with dozens of good entries. I certainly enjoyed it, a smashing success, with dozens of good entries. I certainly enjoyed it,
even though I did have to look at a *lot* of tricky assembly code that I even though I did have to look at a *lot* of tricky assembly code that I
didn’t write—hard work under the best of circumstances. It was worth the didn't write—hard work under the best of circumstances. It was worth the
trouble, though. The winning entry was an astonishing example of what trouble, though. The winning entry was an astonishing example of what
assembly language can do in the right hands; on my 386, it was *four assembly language can do in the right hands; on my 386, it was *four
times* faster at word counting than the nice, tight assembly code I times* faster at word counting than the nice, tight assembly code I
provided as a starting point—and about 13 times faster than the original provided as a starting point—and about 13 times faster than the original
C implementation. Attention, high-level language chauvinists: Is the C implementation. Attention, high-level language chauvinists: Is the
speedup getting significant yet? Okay, maybe word counting isn’t the speedup getting significant yet? Okay, maybe word counting isn't the
most critical application, but how would you like to have that kind of most critical application, but how would you like to have that kind of
improvement in your compression software, or in your real-time games—or improvement in your compression software, or in your real-time games—or
in Windows graphics? in Windows graphics?
@ -128,7 +128,7 @@ more than that he is obviously an extremely good assembly language
programmer, was a close third, as shown in Table 16.2, which precedes programmer, was a close third, as shown in Table 16.2, which precedes
Listing 16.5. Those three were out ahead of the pack; the fourth-place Listing 16.5. Those three were out ahead of the pack; the fourth-place
entry, good as it was (twice as fast as my original code), was twice as entry, good as it was (twice as fast as my original code), was twice as
slow as David’s winning entry, so you can see that David, Dave, and Mick slow as David's winning entry, so you can see that David, Dave, and Mick
attained a rarefied level of optimization indeed. attained a rarefied level of optimization indeed.
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -104,7 +104,7 @@ Table 16.2 The top four word-counting entries.
in-a-word/not-in-a-word status. The count register is masked to in-a-word/not-in-a-word status. The count register is masked to
remove the high bit and the count of words remains in the count remove the high bit and the count of words remains in the count
register. register.
Sound complicated? You’re right! But it’s fast! Sound complicated? You're right! But it's fast!
The beauty of this method is that no jumps are required, the The beauty of this method is that no jumps are required, the
operations are fast, it requires only one table and the process can operations are fast, it requires only one table and the process can
@ -153,7 +153,7 @@ Table 16.2 The top four word-counting entries.
mov di,[bp+CharFlag] mov di,[bp+CharFlag]
mov bh,[di] ;bh = old CharFlag mov bh,[di] ;bh = old CharFlag
mov bl,[si] ;bl = character mov bl,[si] ;bl = character
add bh,‘A’-1 ;make bh into character add bh,‘A'-1 ;make bh into character
add bx,bx ;prepare to index add bx,bx ;prepare to index
mov al,es:[bx] mov al,es:[bx]
cbw ;get hi bit in ah (then bh) cbw ;get hi bit in ah (then bh)

View file

@ -5,17 +5,17 @@
### Levels of Optimization {#Heading9} ### Levels of Optimization {#Heading9}
Three levels of optimization were evident in the word-counting entries I Three levels of optimization were evident in the word-counting entries I
received in response to my challenge. I’d briefly describe them as received in response to my challenge. I'd briefly describe them as
“fine-tuning,” “new perspective,” and “table-driven state machine.” The "fine-tuning," "new perspective," and "table-driven state machine." The
latter categories produce faster code, but, by the same token, they are latter categories produce faster code, but, by the same token, they are
harder to design, harder to implement, and more difficult to understand, harder to design, harder to implement, and more difficult to understand,
so they’re suitable for only the most demanding applications. (Heck, I so they're suitable for only the most demanding applications. (Heck, I
don’t even guarantee that David Stafford’s entry works perfectly, don't even guarantee that David Stafford's entry works perfectly,
although, knowing him, it probably does; the more complex and cryptic although, knowing him, it probably does; the more complex and cryptic
the code, the greater the chance for obscure bugs.) the code, the greater the chance for obscure bugs.)
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 {#Heading10} #### Optimization Level 1: Good Code {#Heading10}
@ -28,7 +28,7 @@ but that framework is implemented more efficiently.
One obvious level 1 optimization is using a **word** rather than One obvious level 1 optimization is using a **word** rather than
**dword** counter. **ScanBuffer** can never be called upon to handle **dword** counter. **ScanBuffer** can never be called upon to handle
more than 64K bytes at a time, so no more than 32K words can ever be more than 64K bytes at a time, so no more than 32K words can ever be
found. Given that, it’s a logical step to use **INC** rather than found. Given that, it's a logical step to use **INC** rather than
**ADD/ADC** to keep count, adding the tally into the full 32-bit count **ADD/ADC** to keep count, adding the tally into the full 32-bit count
only upon exiting the function. Another useful optimization is aligning only upon exiting the function. Another useful optimization is aligning
loop tops and other branch destinations to **word** , or better yet loop tops and other branch destinations to **word** , or better yet
@ -44,7 +44,7 @@ the point of unrolling a loop is to reduce the number of times you have
to check for the end of the buffer! The trick to this is to set CX to to check for the end of the buffer! The trick to this is to set CX to
the number of repetitions of the *unrolled* loop and count down only the number of repetitions of the *unrolled* loop and count down only
once each time through the unrolled loop. In order to handle repetition once each time through the unrolled loop. In order to handle repetition
counts that aren’t exact multiples of the unrolling factor, you must counts that aren't exact multiples of the unrolling factor, you must
enter the loop by branching into the middle of it to perform whatever enter the loop by branching into the middle of it to perform whatever
fraction of the number of unrolled repetitions is required to make the fraction of the number of unrolled repetitions is required to make the
whole thing come out right. Listing 16.5 (QSCAN3.ASM) illustrates this whole thing come out right. Listing 16.5 (QSCAN3.ASM) illustrates this
@ -63,7 +63,7 @@ eliminate it entirely. The most straightforward way to reduce such
branching is to employ two loops. One loop is used to look for the end branching is to employ two loops. One loop is used to look for the end
of a word when the last byte was a non-separator, and one loop is used of a word when the last byte was a non-separator, and one loop is used
to look for the start of a word when the last byte was a separator. This to look for the start of a word when the last byte was a separator. This
way, it’s no longer necessary to maintain a flag to indicate the state way, it's no longer necessary to maintain a flag to indicate the state
of the last byte; that state is implied by whichever loop is currently of the last byte; that state is implied by whichever loop is currently
executing. This considerably simplifies and streamlines the inner loop executing. This considerably simplifies and streamlines the inner loop
code. code.
@ -71,11 +71,11 @@ code.
Listing 16.6, contributed by Willem Clements, of Granada, Spain, Listing 16.6, contributed by Willem Clements, of Granada, Spain,
illustrates a variety of level 1 optimizations: the two-loop approach, illustrates a variety of level 1 optimizations: the two-loop approach,
the use of a 16- rather than 32-bit counter, and the use of **LODSW** . the use of a 16- rather than 32-bit counter, and the use of **LODSW** .
Together, these optimizations made Willem’s code nearly twice as fast as Together, these optimizations made Willem's code nearly twice as fast as
mine in Listing 16.4. A few details could stand improvement; for mine in Listing 16.4. A few details could stand improvement; for
example, **AND AX,AX** is a shorter way to test for zero than **CMP example, **AND AX,AX** is a shorter way to test for zero than **CMP
AX,0** , and **ALIGN 2** could be used. Nonetheless, this is good code, AX,0** , and **ALIGN 2** could be used. Nonetheless, this is good code,
and it’s also fairly compact and reasonably easy to understand. In and it's also fairly compact and reasonably easy to understand. In
short, this is an excellent example of how an hour or so of short, this is an excellent example of how an hour or so of
hand-optimization might accomplish significantly improved performance at hand-optimization might accomplish significantly improved performance at
a reasonable cost in complexity and time. This level of optimization is a reasonable cost in complexity and time. This level of optimization is

View file

@ -119,17 +119,17 @@ implemented code that sprang from that new perspective.
You can see one example of this in Listing 16.6, where Willem uses **CMP You can see one example of this in Listing 16.6, where Willem uses **CMP
AX,0101H** to check two bytes at once. While you might think of this as AX,0101H** to check two bytes at once. While you might think of this as
nothing more than a doubling up of tests, it’s a little more than that, nothing more than a doubling up of tests, it's a little more than that,
especially when taken together with the use of two loops. This is a especially when taken together with the use of two loops. This is a
break with the serial nature of the C code, a recognition that word break with the serial nature of the C code, a recognition that word
counting is really nothing more than a state machine that transitions counting is really nothing more than a state machine that transitions
from the “in word” state to the “not in word” state and back, counting a from the "in word" state to the "not in word" state and back, counting a
word on one but not both of those transitions. Willem says, in effect, word on one but not both of those transitions. Willem says, in effect,
“We’re in a word; if the next two bytes are non-separators, then we’re "We're in a word; if the next two bytes are non-separators, then we're
still in a word, else we’re not in a word, so count and change to the still in a word, else we're not in a word, so count and change to the
appropriate state.” That’s really quite different from saying, as I appropriate state." That's really quite different from saying, as I
originally did, “If the last byte was a non-separator, then if the originally did, "If the last byte was a non-separator, then if the
current byte is a separator, then count a word.” Willem has moved away current byte is a separator, then count a word." Willem has moved away
from the all-in-one approach, splitting the code up into state-specific from the all-in-one approach, splitting the code up into state-specific
chunks that are more efficient because each does only the work required chunks that are more efficient because each does only the work required
in a particular state. in a particular state.
@ -140,27 +140,27 @@ the word), rather than waiting for a separator following a non-separator
(at the end of the word). My friend Dan Illowsky describes the thought (at the end of the word). My friend Dan Illowsky describes the thought
process leading to this approach thusly: process leading to this approach thusly:
*“I try to code as closely as possible to the real world nature of those *"I try to code as closely as possible to the real world nature of those
things my program models. It seems somehow wrong to me to count the end things my program models. It seems somehow wrong to me to count the end
of a word as you do when you look for a transition from a word to a of a word as you do when you look for a transition from a word to a
non-word. A word is not a transition, it is the presence of a group of non-word. A word is not a transition, it is the presence of a group of
characters. Thought of this way, the code would have counted the word characters. Thought of this way, the code would have counted the word
when it first detected the group. Had you done this, your main program when it first detected the group. Had you done this, your main program
would not have needed to look for the possible last transition or deal would not have needed to look for the possible last transition or deal
with the semantics of the value in **CharValue**.”* with the semantics of the value in **CharValue**."*
John Richardson, of New York, contributed a good example of the benefits John Richardson, of New York, contributed a good example of the benefits
of a different perspective (in this case, a hardware perspective). John of a different perspective (in this case, a hardware perspective). John
eliminated all branches used for detecting word edges; the inner loop of eliminated all branches used for detecting word edges; the inner loop of
his code is shown in Listing 16.7. As John explains it: his code is shown in Listing 16.7. As John explains it:
*“My next shot was to get rid of all the branches in the loop. To do *"My next shot was to get rid of all the branches in the loop. To do
that, I reached back to my college hardware courses. I noticed that we that, I reached back to my college hardware courses. I noticed that we
were really looking at an edge triggered device we want to count each were really looking at an edge triggered device we want to count each
time the I’m a character state goes from one to zero. Remembering that time the I'm a character state goes from one to zero. Remembering that
XOR on two single-bit values will always return whether the bits are XOR on two single-bit values will always return whether the bits are
different or the same, I implemented a transition counter. The counter different or the same, I implemented a transition counter. The counter
triggers every time a word begins or ends.”* triggers every time a word begins or ends."*
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](16-06.html) [Table of Contents](index.html) [Next](16-08.html) [Previous](16-06.html) [Table of Contents](index.html) [Next](16-08.html)

View file

@ -6,13 +6,13 @@
ScanLoop: ScanLoop:
lodsw ;get the next 2 bytes (AL = first, AH = 2nd) lodsw ;get the next 2 bytes (AL = first, AH = 2nd)
xlat ;look up first’s char/not status xlat ;look up first's char/not status
xor dl,al ;see if there’s a new char/not status xor dl,al ;see if there's a new char/not status
add di,dx ;we add 1 for each char/not transition add di,dx ;we add 1 for each char/not transition
mov dl,al mov dl,al
mov al,ah ;look at the second byte mov al,ah ;look at the second byte
xlat ;look up its char/not status xlat ;look up its char/not status
xor dl,al ;see if there’s a new char/not status xor dl,al ;see if there's a new char/not status
add di,dx ;we add 1 for each char/not transition add di,dx ;we add 1 for each char/not transition
mov dl,al mov dl,al
dec dx dec dx
@ -20,10 +20,10 @@
John later divides the transition count by two to get the word count. John later divides the transition count by two to get the word count.
(Food for thought: It’s also possible to use **CMP** and **ADC** to (Food for thought: It's also possible to use **CMP** and **ADC** to
detect words without branching.) detect words without branching.)
John’s approach makes it clear that word-counting is nothing more than a John's approach makes it clear that word-counting is nothing more than a
fairly simple state machine. The interesting part, of course, is fairly simple state machine. The interesting part, of course, is
building the fastest state machine. building the fastest state machine.
@ -40,21 +40,21 @@ In the case of word counting, level 3 means building a table-driven
state machine dedicated to processing a buffer of bytes into a count of state machine dedicated to processing a buffer of bytes into a count of
words with a minimum of branching. This level of optimization strips words with a minimum of branching. This level of optimization strips
away many of the abstractions we usually use in coding, such as loops, away many of the abstractions we usually use in coding, such as loops,
tests, and named variables—look back to Listing 16.5, and you’ll see tests, and named variables—look back to Listing 16.5, and you'll see
what I mean. Only a few people reached this level, and I don’t think any what I mean. Only a few people reached this level, and I don't think any
of them did it without long, hard thinking; David Stafford’s final entry of them did it without long, hard thinking; David Stafford's final entry
(that is, the one I present as Listing 16.5) was *at least* the fifth (that is, the one I present as Listing 16.5) was *at least* the fifth
entry he sent me. entry he sent me.
The key concept at level 3 is the use of a massive (64K) lookup table The key concept at level 3 is the use of a massive (64K) lookup table
that processes byte sequences directly into word-count actions. With that processes byte sequences directly into word-count actions. With
such a table, it’s possible to look up the appropriate action for two such a table, it's possible to look up the appropriate action for two
bytes simultaneously in just a few instructions; next, I’m going to look bytes simultaneously in just a few instructions; next, I'm going to look
at the inspired and highly unusual way that David’s code, shown in at the inspired and highly unusual way that David's code, shown in
Listing 16.5, does exactly that. (Before assembling Listing 16.5, you Listing 16.5, does exactly that. (Before assembling Listing 16.5, you
must run the C code in Listing 16.8, to generate an include file must run the C code in Listing 16.8, to generate an include file
defining the 64K lookup table. When you assemble Listing 16.5, TASM will defining the 64K lookup table. When you assemble Listing 16.5, TASM will
report a “location counter overflow” warning; ignore it.) report a "location counter overflow" warning; ignore it.)
**LISTING 16.8 MAKETAB.C** **LISTING 16.8 MAKETAB.C**
@ -63,7 +63,7 @@ report a “location counter overflow” warning; ignore it.)
#include <stdio.h> #include <stdio.h>
#include <ctype.h> #include <ctype.h>
#define ChType( c ) (((c) & 0x7f) == ‘\’’ || isalnum((c) & 0x7f)) #define ChType( c ) (((c) & 0x7f) == ‘\'' || isalnum((c) & 0x7f))
int NoCarry[ 4 ] = { 0, 0x80, 1, 0x80 }; int NoCarry[ 4 ] = { 0, 0x80, 1, 0x80 };
int Carry[ 4 ] = { 1, 0x81, 1, 0x80 }; int Carry[ 4 ] = { 1, 0x81, 1, 0x80 };
@ -71,9 +71,9 @@ report a “location counter overflow” warning; ignore it.)
void main( void ) void main( void )
{ {
int ahChar, alChar, i; int ahChar, alChar, i;
FILE *t = fopen( “QSCAN3.INC”, “wt” ); FILE *t = fopen( "QSCAN3.INC", "wt" );
printf( “Building table. Please wait...” ); printf( "Building table. Please wait..." );
for( ahChar = 0; ahChar < 128; ahChar++ ) for( ahChar = 0; ahChar < 128; ahChar++ )
{ {
@ -81,10 +81,10 @@ report a “location counter overflow” warning; ignore it.)
{ {
i = ChType( alChar ) * 2 + ChType( ahChar ); i = ChType( alChar ) * 2 + ChType( ahChar );
if( alChar % 8 == 0 ) fprintf( t, “\ndb %02Xh”, NoCarry[ i ] ); if( alChar % 8 == 0 ) fprintf( t, "\ndb %02Xh", NoCarry[ i ] );
else fprintf( t, “,%02Xh”, NoCarry[ i ] ); else fprintf( t, ",%02Xh", NoCarry[ i ] );
fprintf( t, “,%02Xh”, Carry[ i ] ); fprintf( t, ",%02Xh", Carry[ i ] );
} }
} }
@ -92,7 +92,7 @@ report a “location counter overflow” warning; ignore it.)
} }
David’s approach is simplicity itself, although his implementation David's approach is simplicity itself, although his implementation
arguably is not. Consider any three sequential bytes in the buffer. arguably is not. Consider any three sequential bytes in the buffer.
Those three bytes define two potential places where a word might be Those three bytes define two potential places where a word might be
counted, as shown in Figure 16.1. Given the separator/non-separator counted, as shown in Figure 16.1. Given the separator/non-separator
@ -102,11 +102,11 @@ there is a non-separator followed by a separator. Note that a maximum of
one word can be counted per three-byte sequence. one word can be counted per three-byte sequence.
The trick, then, is to identify the separator/not statuses of each set The trick, then, is to identify the separator/not statuses of each set
of three bytes and turn them into a 1 (count word) or 0 (don’t count of three bytes and turn them into a 1 (count word) or 0 (don't count
word), as quickly as possible. Assuming that the separator/not status word), as quickly as possible. Assuming that the separator/not status
for the first byte is in the Carry flag, this is easily accomplished by for the first byte is in the Carry flag, this is easily accomplished by
a lookup in a 64K table, based on the Carry flag and the other two a lookup in a 64K table, based on the Carry flag and the other two
bytes, as shown in Figure 16.2. (Remember that we’re counting 7-bit bytes, as shown in Figure 16.2. (Remember that we're counting 7-bit
ASCII here, so the high bit is ignored.) Thus, David is able to add the ASCII here, so the high bit is ignored.) Thus, David is able to add the
word/not status for each pair of bytes to the main word count simply by word/not status for each pair of bytes to the main word count simply by
getting the two bytes, working in the carry status from the last byte, getting the two bytes, working in the carry status from the last byte,
@ -122,7 +122,7 @@ One detail remains to be attended to: setting the Carry flag for next
time if the last byte was a non-separator. David does this in a bizarre time if the last byte was a non-separator. David does this in a bizarre
and incredibly effective way: He presets the high bit of the count, and and incredibly effective way: He presets the high bit of the count, and
sets the high bit in the lookup table for those entries looked up by sets the high bit in the lookup table for those entries looked up by
non-separators. When a non-separator’s lookup entry is added to the non-separators. When a non-separator's lookup entry is added to the
count, it will produce a carry, as desired. The high bit of the count is count, it will produce a carry, as desired. The high bit of the count is
masked off before being added to the total count, so David is masked off before being added to the total count, so David is
essentially using different parts of the count variables for different essentially using different parts of the count variables for different
@ -131,13 +131,13 @@ purposes (counting, and setting the Carry flag).
![](images/16-02.jpg)\ ![](images/16-02.jpg)\
**Figure 16.2**  *Looking up a word count status.* **Figure 16.2**  *Looking up a word count status.*
There are a number of other interesting details in David’s code, There are a number of other interesting details in David's code,
including the unrolling of the loop 64 times, so that 256 bytes in a row including the unrolling of the loop 64 times, so that 256 bytes in a row
are processed without a single branch. Unfortunately, I lack the space are processed without a single branch. Unfortunately, I lack the space
to discuss Listing 16.5 any further. Perhaps that’s not so unfortunate, to discuss Listing 16.5 any further. Perhaps that's not so unfortunate,
after all; I’d hate to deny you the pleasure of discovering the wonders after all; I'd hate to deny you the pleasure of discovering the wonders
of this rather remarkable code yourself. I will say one more thing, of this rather remarkable code yourself. I will say one more thing,
though. The cycle count for David’s inner loop is 6.5 cycles per byte though. The cycle count for David's inner loop is 6.5 cycles per byte
processed, and the actual measured time for his routine, overhead and processed, and the actual measured time for his routine, overhead and
all, is 7.9 cycles/byte. The original C code clocked in at around 100 all, is 7.9 cycles/byte. The original C code clocked in at around 100
cycles/byte. cycles/byte.
@ -146,24 +146,24 @@ Enough said, I trust.
#### Enough Word Counting Already! {#Heading13} #### Enough Word Counting Already! {#Heading13}
Before I finish up this chapter, I’d like to mention that Terje Before I finish up this chapter, I'd like to mention that Terje
Mathisen’s WC word-counting program, which I’ve mentioned previously and Mathisen's WC word-counting program, which I've mentioned previously and
which is available, with source, on Bix, is in the ballpark with David’s which is available, with source, on Bix, is in the ballpark with David's
code for performance. What’s more, Terje’s program handles 8-bit ASCII, code for performance. What's more, Terje's program handles 8-bit ASCII,
counts lines as well as words, and supports user-definable separator counts lines as well as words, and supports user-definable separator
sets. It’s wonderful code, well worth a look; it also happens to be a sets. It's wonderful code, well worth a look; it also happens to be a
great word-counting utility. By the way, Terje builds his 64K table on great word-counting utility. By the way, Terje builds his 64K table on
the fly, at program initialization; this allows for customized tables, the fly, at program initialization; this allows for customized tables,
shrinks the size of the EXE, and, according to Terje’s calculations, shrinks the size of the EXE, and, according to Terje's calculations,
takes less time than loading the table off disk as part of the EXE. takes less time than loading the table off disk as part of the EXE.
So, has David written the fastest possible word-counting code? Well, So, has David written the fastest possible word-counting code? Well,
maybe—but I have a letter from Terry Holmes, of San Rafael, California, maybe—but I have a letter from Terry Holmes, of San Rafael, California,
that calculates the theoretical maximum performance of native 386 that calculates the theoretical maximum performance of native 386
word-counting code at 5.5 cycles/byte, which would be significantly word-counting code at 5.5 cycles/byte, which would be significantly
faster than David’s code. Terry, alas, didn’t bother to implement his faster than David's code. Terry, alas, didn't bother to implement his
design, but maybe I’ll take a shot at it someday. It’d be fun, for design, but maybe I'll take a shot at it someday. It'd be fun, for
sure—but jeez, I’ve got *real* work to do! sure—but jeez, I've got *real* work to do!
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
[Previous](16-07.html) [Table of Contents](index.html) [Next](17-01.html) [Previous](16-07.html) [Table of Contents](index.html) [Next](17-01.html)

View file

@ -8,52 +8,52 @@ Chapter 17\
### The Triumph of Algorithmic Optimization in a Cellular Automata Game {#Heading2} ### The Triumph of Algorithmic Optimization in a Cellular Automata Game {#Heading2}
I’ve spent a lot of my life discussing assembly language optimization, I've spent a lot of my life discussing assembly language optimization,
which I consider to be an important and underappreciated topic. However, which I consider to be an important and underappreciated topic. However,
I’d like to take this opportunity to point out that there is much, much I'd like to take this opportunity to point out that there is much, much
more to optimization than assembly language. Assembly is essential for more to optimization than assembly language. Assembly is essential for
absolute maximum performance, but it’s not the only ingredient; absolute maximum performance, but it's not the only ingredient;
necessary but not sufficient, if you catch my drift—and not even necessary but not sufficient, if you catch my drift—and not even
necessary, if you’re looking for improved but not maximum performance. necessary, if you're looking for improved but not maximum performance.
You’ve heard it a thousand times: Optimize your algorithm first. Devise You've heard it a thousand times: Optimize your algorithm first. Devise
new approaches. Or, as Knuth said, *Premature optimization is the root new approaches. Or, as Knuth said, *Premature optimization is the root
of all evil.* of all evil.*
This is, of course, old hat, stuff you know like the back of your hand. This is, of course, old hat, stuff you know like the back of your hand.
Or is it? As Jeff Duntemann pointed out to me the other day, performance Or is it? As Jeff Duntemann pointed out to me the other day, performance
programmers are made, not born. While I’m merrily gallivanting around in programmers are made, not born. While I'm merrily gallivanting around in
this book optimizing 486 pipelining and turning simple tasks into this book optimizing 486 pipelining and turning simple tasks into
horribly complicated and terrifyingly fast state machines, many of you horribly complicated and terrifyingly fast state machines, many of you
are still developing your basic optimization skills. I don’t want to are still developing your basic optimization skills. I don't want to
shortchange those of you in the latter category, so in this chapter, shortchange those of you in the latter category, so in this chapter,
we’ll discuss some high-level language optimizations that can be applied we'll discuss some high-level language optimizations that can be applied
by mere mortals within a reasonable period of time. We’re going to by mere mortals within a reasonable period of time. We're going to
examine a complete optimization process, from start to finish, and what examine a complete optimization process, from start to finish, and what
we will find is that it’s possible to get a 50-times speed-up without we will find is that it's possible to get a 50-times speed-up without
using *one byte of assembly!* It’s all a matter of perspective—how you using *one byte of assembly!* It's all a matter of perspective—how you
look at your code and data. look at your code and data.
### Conway’s Game {#Heading3} ### Conway's Game {#Heading3}
The program that we’re going to optimize is Conway’s famous Game of The program that we're going to optimize is Conway's famous Game of
Life, long-ago favorite of the hackers at MIT’s AI Lab. If you’ve never Life, long-ago favorite of the hackers at MIT's AI Lab. If you've never
seen it, let me assure you: Life is *neat,* and more than a little seen it, let me assure you: Life is *neat,* and more than a little
hypnotic. Fractals have been the hot graphics topic in recent years, but hypnotic. Fractals have been the hot graphics topic in recent years, but
for eye-catching dazzle, Life is hard to beat. for eye-catching dazzle, Life is hard to beat.
Of course, eye-catching dazzle requires real-time performance—lots of Of course, eye-catching dazzle requires real-time performance—lots of
pixels help too—and there’s the rub. When there are, say, 40,000 cells pixels help too—and there's the rub. When there are, say, 40,000 cells
to process and display, a simple, straightforward implementation just to process and display, a simple, straightforward implementation just
doesn’t cut it, even on a 33 MHz 486. Happily, though, there are many, doesn't cut it, even on a 33 MHz 486. Happily, though, there are many,
many ways to speed up Life, and they illustrate a variety of important many ways to speed up Life, and they illustrate a variety of important
optimization principles, as this chapter will show. optimization principles, as this chapter will show.
First, I’ll describe the ground rules of Life, implement a very First, I'll describe the ground rules of Life, implement a very
straightforward version in C++, and then speed that version up by about straightforward version in C++, and then speed that version up by about
eight times without using any drastically different approaches or any eight times without using any drastically different approaches or any
assembly. This may be a little tame for some of you, but be patient; for assembly. This may be a little tame for some of you, but be patient; for
after that, we’ll haul out the big guns and move into the 30 to 40 times after that, we'll haul out the big guns and move into the 30 to 40 times
speed-up range. Then in the next chapter, I’ll show you how several speed-up range. Then in the next chapter, I'll show you how several
programmers *really* floored it in taking me up on my second programmers *really* floored it in taking me up on my second
Optimization Challenge, which involved the Game of Life. Optimization Challenge, which involved the Game of Life.
@ -68,25 +68,25 @@ according to the following rules:
- If a cell is on and has either two or three neighbors that are on in - If a cell is on and has either two or three neighbors that are on in
the current generation, it stays on; otherwise, the cell turns off. the current generation, it stays on; otherwise, the cell turns off.
- If a cell is off and has exactly three “on” neighbors in the current - If a cell is off and has exactly three "on" neighbors in the current
generation, it turns on; otherwise, it stays off. That’s all the generation, it turns on; otherwise, it stays off. That's all the
rules there are—but they give rise to an astonishing variety of rules there are—but they give rise to an astonishing variety of
forms, including patterns that spin, march across the screen, and forms, including patterns that spin, march across the screen, and
explode. explode.
It’s only a little more complicated to implement the Game of Life than It's only a little more complicated to implement the Game of Life than
it is to describe it. Listing 17.1, together with the display functions it is to describe it. Listing 17.1, together with the display functions
in Listing 17.2, is a C++ implementation of the Game of Life, and it’s in Listing 17.2, is a C++ implementation of the Game of Life, and it's
very straightforward. A cellmap is an object that’s accessible through very straightforward. A cellmap is an object that's accessible through
member functions to set, clear, and test cell states, and through a member functions to set, clear, and test cell states, and through a
member function to calculate the next generation. Calculating the next member function to calculate the next generation. Calculating the next
generation involves nothing more than using the other member functions generation involves nothing more than using the other member functions
to set each cell to the appropriate state, given the number of to set each cell to the appropriate state, given the number of
neighboring on-cells and the cell’s current state. The only complication neighboring on-cells and the cell's current state. The only complication
is that it’s necessary to place the next generation’s cells in another is that it's necessary to place the next generation's cells in another
cellmap, and then copy the final result back to the original cellmap. cellmap, and then copy the final result back to the original cellmap.
This keeps us from corrupting the current generation’s cellmap before This keeps us from corrupting the current generation's cellmap before
we’re done using it to calculate the next generation. we're done using it to calculate the next generation.
All in all, Listing 17.1 is a clean, compact, and elegant implementation All in all, Listing 17.1 is a clean, compact, and elegant implementation
of the Game of Life. Were it not that the code is as slow as molasses, of the Game of Life. Were it not that the code is as slow as molasses,

View file

@ -65,12 +65,12 @@
cellmap next_map(cellmap_height, cellmap_width); cellmap next_map(cellmap_height, cellmap_width);
// Get the seed; seed randomly if 0 entered // Get the seed; seed randomly if 0 entered
cout << “Seed (0 for random seed): ”; cout << "Seed (0 for random seed): ";
cin >> seed; cin >> seed;
if (seed == 0) seed = (unsigned) time(NULL); if (seed == 0) seed = (unsigned) time(NULL);
// Randomly initialize the initial cell map // Randomly initialize the initial cell map
cout << “Initializing...”; cout << "Initializing...";
srand(seed); srand(seed);
init_length = (cellmap_height * cellmap_width) / 2; init_length = (cellmap_height * cellmap_width) / 2;
do { do {
@ -84,11 +84,11 @@
// Keep recalculating and redisplaying generations until a key // Keep recalculating and redisplaying generations until a key
// is pressed // is pressed
show_text(0, MSG_LINE, “Generation: ”); show_text(0, MSG_LINE, "Generation: ");
start_bios_time = _bios_timeofday(_TIME_GETCLOCK, &bios_time); start_bios_time = _bios_timeofday(_TIME_GETCLOCK, &bios_time);
do { do {
generation++; generation++;
sprintf(gen_text, “%10lu”, generation); sprintf(gen_text, "%10lu", generation);
show_text(1, GENERATION_LINE, gen_text); show_text(1, GENERATION_LINE, gen_text);
// Recalculate and draw the next generation // Recalculate and draw the next generation
current_map.next_generation(next_map); current_map.next_generation(next_map);
@ -104,8 +104,8 @@
} while (!kbhit()); } while (!kbhit());
getch(); // clear keypress getch(); // clear keypress
exit_display_mode(); exit_display_mode();
cout << “Total generations: ” << generation << “\nSeed: ” << cout << "Total generations: " << generation << "\nSeed: " <<
seed << “\n”; seed << "\n";
} }
/* cellmap constructor. */ /* cellmap constructor. */
@ -125,7 +125,7 @@
delete[] cells; delete[] cells;
} }
/* Copies one cellmap’s cells to another cellmap. Both cellmaps are /* Copies one cellmap's cells to another cellmap. Both cellmaps are
assumed to be the same size. */ assumed to be the same size. */
void cellmap::copy_cells(cellmap &sourcemap) void cellmap::copy_cells(cellmap &sourcemap)
{ {

View file

@ -16,12 +16,12 @@ optimizing to do.
The first rule of optimization is: Only optimize where it matters. Use a The first rule of optimization is: Only optimize where it matters. Use a
profiler, or risk making a fool of yourself. Consider Listings 17.1 and profiler, or risk making a fool of yourself. Consider Listings 17.1 and
17.2. Where do you think the potential for significant speed-up lies? 17.2. Where do you think the potential for significant speed-up lies?
I’ll tell you one place where I thought there was considerable I'll tell you one place where I thought there was considerable
potential—in **draw\_pixel()**. As a programmer of high-speed graphics, potential—in **draw\_pixel()**. As a programmer of high-speed graphics,
I figured any drawing function that was not only written in C/C++ but I figured any drawing function that was not only written in C/C++ but
also recalculated the target address from scratch for each pixel would also recalculated the target address from scratch for each pixel would
be among the first optimization targets. I also expected to get major be among the first optimization targets. I also expected to get major
gains out of going to a Ping-Pong arrangement so that I didn’t have to gains out of going to a Ping-Pong arrangement so that I didn't have to
copy the new cellmap back to **current\_map** after calculating the next copy the new cellmap back to **current\_map** after calculating the next
generation. generation.
@ -111,34 +111,34 @@ 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* the time taken by **draw\_pixel(),** **copy\_cells(),** and *everything*
other than calculating the next generation is nothing more than noise. other than calculating the next generation is nothing more than noise.
We could optimize these routines right down to executing We could optimize these routines right down to executing
*instantaneously,* and you know what? It wouldn’t make the slightest *instantaneously,* and you know what? It wouldn't make the slightest
perceptible difference in how fast the program runs. Given the present perceptible difference in how fast the program runs. Given the present
state of our Game of Life implementation, the only areas worth looking state of our Game of Life implementation, the only areas worth looking
at for possible optimizations are **cell\_state()** and at for possible optimizations are **cell\_state()** and
**next\_generation().** **next\_generation().**
------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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 {#Heading6} ### The Hazards and Advantages of Abstraction {#Heading6}
How can we speed up **cell\_state()** and **next\_generation()**? I’ll 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 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 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 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 to do is figure out *why* **cell\_state()** is taking all the time, then
address that aspect of the program directly. address that aspect of the program directly.
Once you know where you need to optimize, the one word to keep in mind 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.* isn't assembly, it's...plastics. No, actually, it's *abstraction.*
Well-written C and especially C++ programs are highly abstract models. Well-written C and especially C++ programs are highly abstract models.
For example, Listing 17.1 essentially creates a new programming language For example, Listing 17.1 essentially creates a new programming language
in which cells are tangible things, with built-in manipulation in which cells are tangible things, with built-in manipulation
instructions. Given the cellmap member functions, you don’t even need to 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 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 saves programming time and bugs, and frees you to work on the
application’s needs, rather than implementation details. application's needs, rather than implementation details.
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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.*
@ -148,10 +148,10 @@ 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 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 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 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 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 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 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 abstraction that's killing us is that of looking at the eight neighbors
with eight completely independent operations, requiring eight calls to with eight completely independent operations, requiring eight calls to
**cell\_state()** and eight calculations of cell address and cell mask. **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 In fact, given the nature of cell storage, the eight neighbors are in a

View file

@ -2,43 +2,43 @@
[Previous](17-03.html) [Table of Contents](index.html) [Next](17-05.html) [Previous](17-03.html) [Table of Contents](index.html) [Next](17-05.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
There’s a kicker here, though, and that’s the counting of neighbors for There's a kicker here, though, and that's the counting of neighbors for
cells at the edge of the cellmap. When cellmap wrapping is enabled (so cells at the edge of the cellmap. When cellmap wrapping is enabled (so
that the cellmap becomes essentially a toroid, with each edge joined that the cellmap becomes essentially a toroid, with each edge joined
seamlessly to the opposite edge, as opposed to having a border of seamlessly to the opposite edge, as opposed to having a border of
off-cells), neighbors that reside on the other edge of the cellmap can’t off-cells), neighbors that reside on the other edge of the cellmap can't
be accessed by the standard fixed offset, as shown in Figure 17.1. So, be accessed by the standard fixed offset, as shown in Figure 17.1. So,
in general, we could improve performance by hard-wiring our in general, we could improve performance by hard-wiring our
neighbor-counting for the bit-per-cell cellmap format, but it seems we’d neighbor-counting for the bit-per-cell cellmap format, but it seems we'd
need a lot of conditional code to handle wrapping, and that would slow need a lot of conditional code to handle wrapping, and that would slow
things back down again. things back down again.
![](images/17-01.jpg)\ ![](images/17-01.jpg)\
**Figure 17.1**  *Edge-wrapping complications.* **Figure 17.1**  *Edge-wrapping complications.*
When a problem doesn’t lend itself well to optimization, make it a When a problem doesn't lend itself well to optimization, make it a
practice to see if you can change the problem definition to one that practice to see if you can change the problem definition to one that
allows for greater efficiency. In this case, we’ll change the problem by allows for greater efficiency. In this case, we'll change the problem by
putting padding bytes around the edge of the cellmap, and duplicating putting padding bytes around the edge of the cellmap, and duplicating
each edge of the cellmap in the padding bytes at the opposite side, as each edge of the cellmap in the padding bytes at the opposite side, as
shown in Figure 17.2. That way, a hard-wired neighbor count will find shown in Figure 17.2. That way, a hard-wired neighbor count will find
exactly what it should—the opposite edge—without any special code at exactly what it should—the opposite edge—without any special code at
all. all.
But doesn’t that extra copying of the edges take time? Sure, but only a But doesn't that extra copying of the edges take time? Sure, but only a
little; we can build it into the cellmap copying function, and then little; we can build it into the cellmap copying function, and then
frankly we won’t even notice it. Avoiding tens or hundreds of thousands frankly we won't even notice it. Avoiding tens or hundreds of thousands
of calls to **cell\_state(),** on the other hand, will be *very* of calls to **cell\_state(),** on the other hand, will be *very*
noticeable. Listing 17.3 shows the alterations to Listing 17.1 required noticeable. Listing 17.3 shows the alterations to Listing 17.1 required
to implement a hard-wired neighbor-counting function. This is a minor to implement a hard-wired neighbor-counting function. This is a minor
change, in truth, implemented in about half an hour and not making the change, in truth, implemented in about half an hour and not making the
code significantly larger—but Listing 17.3 is 3.6 times faster than code significantly larger—but Listing 17.3 is 3.6 times faster than
Listing 17.1, as shown in Table 17.1. We’re up to about 10 generations Listing 17.1, as shown in Table 17.1. We're up to about 10 generations
per second on a 486; not where we want to be, but it is a vast per second on a 486; not where we want to be, but it is a vast
improvement. improvement.
![](images/17-02.jpg)\ ![](images/17-02.jpg)\
**Figure 17.2**  *The “padding cells” solution.* **Figure 17.2**  *The "padding cells" solution.*
**LISTING 17.3 L17-3.CPP** **LISTING 17.3 L17-3.CPP**
@ -79,7 +79,7 @@ improvement.
memset(cells, 0, length_in_bytes); // clear all cells, to start memset(cells, 0, length_in_bytes); // clear all cells, to start
} }
/* Copies one cellmap’s cells to another cellmap. If wrapping is /* Copies one cellmap's cells to another cellmap. If wrapping is
enabled, copies edge (wrap) bytes into opposite padding bytes in enabled, copies edge (wrap) bytes into opposite padding bytes in
source first, so that the padding bytes off each edge have the source first, so that the padding bytes off each edge have the
same values as would be found by wrapping around to the opposite same values as would be found by wrapping around to the opposite

View file

@ -7,7 +7,7 @@ the member functions to compensate for the padding. Also note that the
width now has to be a multiple of eight, to facilitate the process of width now has to be a multiple of eight, to facilitate the process of
copying the edges to the opposite padding bytes. We have decreased the copying the edges to the opposite padding bytes. We have decreased the
generality of our Game of Life implementation in exchange for better generality of our Game of Life implementation in exchange for better
performance. That’s a very common trade-off, as common as trading memory performance. That's a very common trade-off, as common as trading memory
for performance. As a rule, the more general a program is, the slower it for performance. As a rule, the more general a program is, the slower it
is. A corollary is that often (not always, but often), the more heavily is. A corollary is that often (not always, but often), the more heavily
optimized a program is, the more complex and the more difficult to optimized a program is, the more complex and the more difficult to
@ -15,17 +15,17 @@ implement it is. You can often improve performance a good deal by
implementing only the level of generality you need, but at the same time implementing only the level of generality you need, but at the same time
decreased generality makes it more difficult to change or port the decreased generality makes it more difficult to change or port the
program at some later date. A Game of Life implementation, such as program at some later date. A Game of Life implementation, such as
Listing 17.1, that’s built on **set\_cell()**, **clear\_cell()**, and Listing 17.1, that's built on **set\_cell()**, **clear\_cell()**, and
**get\_cell()** is completely general; you can change the cell storage **get\_cell()** is completely general; you can change the cell storage
format simply by changing the constructor and those three functions. format simply by changing the constructor and those three functions.
Listing 17.3 is harder to change because **count\_neighbors()** would Listing 17.3 is harder to change because **count\_neighbors()** would
also have to be altered, and it’s more complex than any of the other also have to be altered, and it's more complex than any of the other
functions. functions.
So, in Listing 17.3, we’ve gotten under the hood and changed the cellmap So, in Listing 17.3, we've gotten under the hood and changed the cellmap
format a little, and gotten impressive results. But now format a little, and gotten impressive results. But now
**count\_neighbors()** is hard-wired for optimized counting, and it’s **count\_neighbors()** is hard-wired for optimized counting, and it's
still taking up more than half the time. Maybe now it’s time to go to still taking up more than half the time. Maybe now it's time to go to
assembly? assembly?
Not hardly. Not hardly.
@ -37,7 +37,7 @@ then see if we can find an alternative approach that better fits the
application. It would actually have made much more sense if we had application. It would actually have made much more sense if we had
looked for a new approach as our first optimization step, but I decided looked for a new approach as our first optimization step, but I decided
it would be better to cover straightforward C++ optimizations at this it would be better to cover straightforward C++ optimizations at this
point, and the mind-bending stuff a little later. Right now, let’s look point, and the mind-bending stuff a little later. Right now, let's look
at some C++ optimizations; Listing 17.4 is a C++-optimized version of at some C++ optimizations; Listing 17.4 is a C++-optimized version of
Listing 17.3. Listing 17.3.
@ -138,21 +138,21 @@ eliminating all remaining function calls and from-scratch address/mask
calculations. calculations.
The net effect of these optimizations is that Listing 17.4 is more than 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 twice as fast as Listing 17.3; we've achieved the desired 18 generations
per second, albeit only on a 486, and only at 96x96. (The **\#define** per second, albeit only on a 486, and only at 96x96. (The **\#define**
that enables code limiting the speed to 18 Hz, which seemed ridiculous that enables code limiting the speed to 18 Hz, which seemed ridiculous
in Listing 17.1, is actually useful for keeping the generations from 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 iterating too quickly when Listing 17.4 is running on a 486, especially
with a small cellmap like 48x48.) We’ve sped things up by about eight with a small cellmap like 48x48.) We've sped things up by about eight
times so far; we need to increase our speed another ten times to reach times so far; we need to increase our speed another ten times to reach
our goal of 200x200 at 18 generations per second on a 20 MHz 386. our goal of 200x200 at 18 generations per second on a 20 MHz 386.
It’s undoubtedly possible to improve the performance of Listing 17.4 It's undoubtedly possible to improve the performance of Listing 17.4
further by fine-tuning the code, but no tremendous improvement is further by fine-tuning the code, but no tremendous improvement is
possible that way. possible that way.
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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.* ![](images/i.jpg) *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.*
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------

View file

@ -2,10 +2,10 @@
[Previous](17-05.html) [Table of Contents](index.html) [Next](17-07.html) [Previous](17-05.html) [Table of Contents](index.html) [Next](17-07.html)
------------------------ --------------------------------- -------------------- ------------------------ --------------------------------- --------------------
We’re still not ready for assembly, though; what we need is a new We're still not ready for assembly, though; what we need is a new
perspective that lends itself to vastly better performance in C++. The perspective that lends itself to vastly better performance in C++. The
Life program in the next section is *three to seven times* faster than Life program in the next section is *three to seven times* faster than
Listing 17.4—and it’s still in C++. Listing 17.4—and it's still in C++.
How is this possible? Here are some hints: How is this possible? Here are some hints:
@ -19,10 +19,10 @@ How is this possible? Here are some hints:
In the previous section, we saw how a C++ program could be sped up about In the previous section, we saw how a C++ program could be sped up about
eight times simply by rearranging the data and code in straightforward eight times simply by rearranging the data and code in straightforward
ways. Now we’re going to see how right-brain non-linear optimization can ways. Now we're going to see how right-brain non-linear optimization can
speed things up by another four times—and make the code *simpler.* speed things up by another four times—and make the code *simpler.*
Now *that’s* Zen code optimization. Now *that's* Zen code optimization.
I have two objectives to achieve in the remainder of this chapter. I have two objectives to achieve in the remainder of this chapter.
First, I want to show that optimization consists of many levels, from First, I want to show that optimization consists of many levels, from
@ -43,25 +43,25 @@ optimizations were moving the critical code into a single loop and using
pointers rather than member functions whenever possible. In other words, pointers rather than member functions whenever possible. In other words,
we took what we already knew and made it more efficient. 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 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 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 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 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 "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 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-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 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 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 so that we can count neighbors and check the cell state as quickly as
possible. Large lookup tables, oddly encoded cellmaps, and lots of possible. Large lookup tables, oddly encoded cellmaps, and lots of
bit-twiddling assembly code spring to mind as possible approaches. Can’t bit-twiddling assembly code spring to mind as possible approaches. Can't
you just feel your adrenaline start to pump? you just feel your adrenaline start to pump?
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *Relax. Step back. Try to divine the true nature of the problem. The object is not to count neighbors and check cell states as quickly as possible; that’s just one possible implementation. The object is to determine when a cell’s state must be changed and to change it appropriately, and that’s what we need to do as quickly as possible.* ![](images/i.jpg) *Relax. Step back. Try to divine the true nature of the problem. The object is not to count neighbors and check cell states as quickly as possible; that's just one possible implementation. The object is to determine when a cell's state must be changed and to change it appropriately, and that's what we need to do as quickly as possible.*
------------------- ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
What difference does that new perspective make? Let’s approach it this 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 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 generations, the vast majority of cells are off. In fact, the vast
majority of cells are not only off but are entirely surrounded by majority of cells are not only off but are entirely surrounded by
@ -69,7 +69,7 @@ off-cells. Also, cells change state infrequently; in any given
generation after the first few, most cells remain in the same state as generation after the first few, most cells remain in the same state as
in the previous generation. in the previous generation.
Do you see where I’m heading? Do you hear a whisper of inspiration from Do you see where I'm heading? Do you hear a whisper of inspiration from
your right brain? The original implementation stored cell states as your right brain? The original implementation stored cell states as
1-bits (on), or 0-bits (off). For each generation and for each cell, it 1-bits (on), or 0-bits (off). For each generation and for each cell, it
counted the states of the eight neighbors, for an average of eight counted the states of the eight neighbors, for an average of eight
@ -96,7 +96,7 @@ only one-tenth that of the original approach!
#### Acting on What We Know {#Heading10} #### Acting on What We Know {#Heading10}
Once we’ve changed the cellmap format to store neighbor counts as well Once we've changed the cellmap format to store neighbor counts as well
as states, with a byte for each cell, we can get another performance as states, with a byte for each cell, we can get another performance
boost by again examining what we know about our data. I said earlier boost by again examining what we know about our data. I said earlier
that most cells are off during any given generation. This means that that most cells are off during any given generation. This means that
@ -106,13 +106,13 @@ can skip over scads of unchanged cells at a pop simply by scanning for
non-zero bytes. This is much faster than explicitly testing cell states non-zero bytes. This is much faster than explicitly testing cell states
and neighbor counts, and lends itself beautifully to assembly language and neighbor counts, and lends itself beautifully to assembly language
implementation as **REPZ SCASB** or (with a little cleverness) **REPZ implementation as **REPZ SCASB** or (with a little cleverness) **REPZ
SCASW.** (Unfortunately, there’s no C library function that can scan SCASW.** (Unfortunately, there's no C library function that can scan
memory for the next byte that’s non-zero.) memory for the next byte that's non-zero.)
Listing 17.5 is a Game of Life implementation that uses the Listing 17.5 is a Game of Life implementation that uses the
neighbor-count cell map format and scans for non-zero bytes. On a 20 MHz neighbor-count cell map format and scans for non-zero bytes. On a 20 MHz
386, Listing 17.5 is about 4.5 times faster at calculating generations 386, Listing 17.5 is about 4.5 times faster at calculating generations
(that is, the generation engine is 4.5 times faster; I’m ignoring the (that is, the generation engine is 4.5 times faster; I'm ignoring the
time consumed by drawing and text display) than Listing 17.4, which is time consumed by drawing and text display) than Listing 17.4, which is
no slouch. On a 33 MHz 486, Listing 17.5 is about 3.5 times faster than no slouch. On a 33 MHz 486, Listing 17.5 is about 3.5 times faster than
Listing 17.4. This is true even though Listing 17.5 must be compiled Listing 17.4. This is true even though Listing 17.5 must be compiled

View file

@ -76,11 +76,11 @@
// Keep recalculating and redisplaying generations until any key // Keep recalculating and redisplaying generations until any key
// is pressed // is pressed
show_text(0, MSG_LINE, “Generation: ”); show_text(0, MSG_LINE, "Generation: ");
start_bios_time = _bios_timeofday(_TIME_GETCLOCK, &bios_time); start_bios_time = _bios_timeofday(_TIME_GETCLOCK, &bios_time);
do { do {
generation++; generation++;
sprintf(gen_text, “%10lu”, generation); sprintf(gen_text, "%10lu", generation);
show_text(1, GENERATION_LINE, gen_text); show_text(1, GENERATION_LINE, gen_text);
// Recalculate and draw the next generation // Recalculate and draw the next generation
current_map.next_generation(); current_map.next_generation();
@ -95,8 +95,8 @@
getch(); // clear keypress getch(); // clear keypress
exit_display_mode(); exit_display_mode();
cout << “Total generations: ” << generation << “\nSeed: ” << cout << "Total generations: " << generation << "\nSeed: " <<
seed << “\n”; seed << "\n";
} }
/* cellmap constructor. */ /* cellmap constructor. */
@ -108,7 +108,7 @@
cells = new unsigned char[length_in_bytes]; // cell storage cells = new unsigned char[length_in_bytes]; // cell storage
temp_cells = new unsigned char[length_in_bytes]; // temp cell storage temp_cells = new unsigned char[length_in_bytes]; // temp cell storage
if ( (cells == NULL) || (temp_cells == NULL) ) { if ( (cells == NULL) || (temp_cells == NULL) ) {
printf(“Out of memory\n”); printf("Out of memory\n");
exit(1); exit(1);
} }
memset(cells, 0, length_in_bytes); // clear all cells, to start memset(cells, 0, length_in_bytes); // clear all cells, to start
@ -229,11 +229,11 @@
cell_ptr++; // advance to the next cell cell_ptr++; // advance to the next cell
if (++x >= w) goto RowDone; if (++x >= w) goto RowDone;
} }
// Found a cell that’s either on or has on-neighbors, // Found a cell that's either on or has on-neighbors,
// so see if its state needs to be changed // so see if its state needs to be changed
count = *cell_ptr >> 1; // # of neighboring on-cells count = *cell_ptr >> 1; // # of neighboring on-cells
if (*cell_ptr & 0x01) { if (*cell_ptr & 0x01) {
// Cell is on; turn it off if it doesn’t have // Cell is on; turn it off if it doesn't have
// 2 or 3 neighbors // 2 or 3 neighbors
if ((count != 2) && (count != 3)) { if ((count != 2) && (count != 3)) {
clear_cell(x, y); clear_cell(x, y);
@ -259,14 +259,14 @@
unsigned int x, y, init_length; unsigned int x, y, init_length;
// Get the seed; seed randomly if 0 entered // Get the seed; seed randomly if 0 entered
cout << “Seed (0 for random seed): ”; cout << "Seed (0 for random seed): ";
cin >> seed; cin >> seed;
if (seed == 0) seed = (unsigned) time(NULL); if (seed == 0) seed = (unsigned) time(NULL);
// Randomly initialize the initial cell map to 50% on-pixels // Randomly initialize the initial cell map to 50% on-pixels
// (actually generally fewer, because some coordinates will be // (actually generally fewer, because some coordinates will be
// randomly selected more than once) // randomly selected more than once)
cout << “Initializing...”; cout << "Initializing...";
srand(seed); srand(seed);
init_length = (height * width) / 2; init_length = (height * width) / 2;
do { do {

View file

@ -4,48 +4,48 @@
The large model is actually not necessary for the 96x96 cellmap in The large model is actually not necessary for the 96x96 cellmap in
Listing 17.5. However, I was actually more interested in seeing a fast Listing 17.5. However, I was actually more interested in seeing a fast
200x200 cellmap, and two 200x200 cellmaps can’t fit in a single segment. 200x200 cellmap, and two 200x200 cellmaps can't fit in a single segment.
(This can easily be worked around in assembly language for cellmaps up (This can easily be worked around in assembly language for cellmaps up
to a segment in size; beyond that size, cellmap scanning becomes pretty to a segment in size; beyond that size, cellmap scanning becomes pretty
complex, although it can still be efficiently implemented with some complex, although it can still be efficiently implemented with some
clever programming.) clever programming.)
Anyway, using the large model helps illustrate that it’s the data Anyway, using the large model helps illustrate that it's the data
representation and the data processing approach you choose that matter representation and the data processing approach you choose that matter
most. Optimization details like memory models and segments and in-line most. Optimization details like memory models and segments and in-line
functions and assembly language are important but secondary. Let your functions and assembly language are important but secondary. Let your
mind roam creatively before you start coding. Otherwise, you may find 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 you're writing well-tuned slow code, which is by no means the same thing
as fast code. as fast code.
Take a close look at Listing 17.5. You will see that it’s quite a bit 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 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 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 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 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* complex mask and pointer management, and the only thing that *really*
needs to be optimized is scanning for zero bytes. (And, in fact, I 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 haven't optimized even that because it's done in a C++ loop; it should
really be **REPZ SCASB.**) really be **REPZ SCASB.**)
In truth, none of the code in Listing 17.5 is particularly 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 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 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. still in C++; note well that there's not a whit of assembly here.
------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *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).* ![](images/i.jpg) *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 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 assembly code—but that's dwarfed by a 30-times improvement, so
optimization at a conceptual level *must* come first. optimization at a conceptual level *must* come first.
#### The Challenge That Ate My Life {#Heading11} #### The Challenge That Ate My Life {#Heading11}
The most recent optimization challenge I laid my community of readers The most recent optimization challenge I laid my community of readers
was to write the fastest possible Game of Life generation engine. By was to write the fastest possible Game of Life generation engine. By
“engine” I meant that I didn’t care about time spent in input or output, "engine" I meant that I didn't care about time spent in input or output,
only time consumed by the call to **next-generation.** The time spent only time consumed by the call to **next-generation.** The time spent
updating the cellmap was what I wanted people to concentrate on. updating the cellmap was what I wanted people to concentrate on.
@ -70,7 +70,7 @@ Here are the rules I laid down for the challenge:
That was the challenge I put to the readers. Little did I realize the That was the challenge I put to the readers. Little did I realize the
challenge it would lay on *me:* Entries poured in from the four corners challenge it would lay on *me:* Entries poured in from the four corners
of the globe. Some were plain, some were brilliant, some were, well, of the globe. Some were plain, some were brilliant, some were, well,
berserk. Many didn’t even work. But all had to be gone through, examined berserk. Many didn't even work. But all had to be gone through, examined
for adherence to the rules, read, compiled, linked, run, and judged. I for adherence to the rules, read, compiled, linked, run, and judged. I
learned a lot—about a lot of things, not the least of which was the learned a lot—about a lot of things, not the least of which was the
process (or maybe the wisdom) of laying down challenges to readers. process (or maybe the wisdom) of laying down challenges to readers.

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more