Change bold code snippets to inline code blocks

This commit is contained in:
James Gregory 2013-12-31 17:12:29 +11:00
commit 3c76d884ef
185 changed files with 1772 additions and 1772 deletions

View file

@ -73,7 +73,7 @@ value before reading the next byte, we'll minimize memory requirements
and be able to handle any size file at all.
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
until the end of the file is reached. The code is compact, easy to
write, and functions perfectly—with one slight hitch:

View file

@ -143,7 +143,7 @@ that, we have to understand what's wrong with the current design.
#### Know the Territory {#Heading9}
Just why is Listing 1.1 so slow? In a word: overhead. The C library
implements the **read()** function by calling DOS to read the desired
implements the `read()` function by calling DOS to read the desired
number of bytes. (I figured this out by watching the code execute with a
debugger, but you can buy library source code from both Microsoft and
Borland.) That means that Listing 1.1 (and Listing 1.3 as well) executes
@ -158,7 +158,7 @@ if the desired byte has already been read, read it from the disk if not,
store the byte in the specified location, and return. All of that takes
a *long* time—far, far longer than the rest of the main loop in Listing
1.1. In short, Listing 1.1 spends virtually all of its time executing
**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
using a code profiler, but take my word for it: There's a great deal of
@ -174,13 +174,13 @@ bytes internally, doling them out to the application as needed by
reading them from memory rather than calling DOS. Let's try using stream
I/O and see what happens.
Listing 1.4 is similar to Listing 1.1, but uses **fopen()** and
**getc()** (rather than **open()** and **read()**) to access the file
Listing 1.4 is similar to Listing 1.1, but uses `fopen()` and
`getc()` (rather than `open()` and `read()`) to access the file
being checksummed. The results confirm our theories splendidly, and
validate our new design. As shown in Table 1.1, Listing 1.4 runs more
than an order of magnitude faster than even the assembly version of
Listing 1.1, *even though Listing 1.1 and Listing 1.4 look almost the
same*. To the casual observer, **read()** and **getc()** would seem
same*. To the casual observer, `read()` and `getc()` would seem
slightly different but pretty much interchangeable, and yet in this
application the performance difference between the two is about the same
as that between a 4.77 MHz PC and a 16 MHz 386.

View file

@ -93,15 +93,15 @@ approaches.
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
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
Listing 1.4 over Listing 1.1 occurs because **getc()** eads many bytes
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
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
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
itself. Easier, yes, but not faster.
Consider this: Every invocation of **getc()** involves pushing a
Consider this: Every invocation of `getc()` involves pushing a
parameter, executing a call to the C library function, getting the
parameter (in the C library code), looking up information about the
desired stream, unbuffering the next byte from the stream, and returning

View file

@ -87,8 +87,8 @@ 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
same number of instructions, the performance of the entire subroutine
improved by about 10 percent from just this one change. (Incidentally,
that wasn't the end of the optimization; I eliminated the **DEC** and
**JNJ** instructions by expanding the four iterations of the loop—but
that wasn't the end of the optimization; I eliminated the `DEC` and
`JNJ` instructions by expanding the four iterations of the loop—but
that's a tale for another chapter.)
The point is this: To write truly superior assembly programs, you need

View file

@ -109,7 +109,7 @@ precise, the 8253 counts once every 838.1 nanoseconds. (A nanosecond is
one billionth of a second, and is abbreviated ns.)
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
"Zen timer." C-callable versions of the two precision Zen timers are
presented in Chapter K on the companion CD-ROM.

View file

@ -27,8 +27,8 @@ and you'll be on the right road.
#### Starting the Zen Timer {#Heading6}
**ZTimerOn** is called at the start of a segment of code to be timed.
**ZTimerOn** saves the context of the calling code, disables interrupts,
`ZTimerOn` is called at the start of a segment of code to be timed.
`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
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
@ -36,21 +36,21 @@ 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
loaded.)
Two aspects of **ZTimerOn** are worth discussing further. One point of
interest is that **ZTimerOn** disables interrupts. (**ZTimerOff** later
restores interrupts to the state they were in when **ZTimerOn** was
called.) Were interrupts not disabled by **ZTimerOn**, keyboard, mouse,
Two aspects of `ZTimerOn` are worth discussing further. One point of
interest is that `ZTimerOn` disables interrupts. (`ZTimerOff` later
restores interrupts to the state they were in when `ZTimerOn` was
called.) Were interrupts not disabled by `ZTimerOn`, keyboard, mouse,
timer, and other interrupts could occur during the timing interval, and
the time required to service those interrupts would incorrectly and
erratically appear to be part of the execution time of the code being
measured. As a result, code timed with the Zen timer should not expect
any hardware interrupts to occur during the interval between any call to
**ZTimerOn** and the corresponding call to **ZTimerOff**, and should not
`ZTimerOn` and the corresponding call to `ZTimerOff`, and should not
enable interrupts during that time.
### Time and the PC {#Heading7}
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.
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.
@ -89,11 +89,11 @@ connected to the hardware interrupt 0 (IRQ0) line on the system board,
so every 54.925 ms, timer 0 causes hardware interrupt 0 to occur.
The interrupt vector for IRQ0 is set by the BIOS at power-up time to
point to a BIOS routine, **TIMER\_INT,** that maintains a time-of-day
count. **TIMER\_INT** keeps a 16-bit count of IRQ0 interrupts in the
point to a BIOS routine, `TIMER_INT`, that maintains a time-of-day
count. `TIMER_INT` keeps a 16-bit count of IRQ0 interrupts in the
BIOS data area at address 0000:046C (all addresses in this book are
given in segment:offset hexadecimal pairs); this count turns over once
an hour (less a few microseconds), and when it does, **TIMER\_INT**
an hour (less a few microseconds), and when it does, `TIMER_INT`
updates a 16-bit hour count at address 0000:046E in the BIOS data area.
This count is the basis for the current time and date that DOS supports
via functions 2AH (2A hexadecimal) through 2DH and by way of the DATE

View file

@ -26,7 +26,7 @@ see whether an IRQ0 interrupt is pending. (Remember, interrupts are off
while the Zen timer runs, so the timer interrupt cannot be recognized
until the Zen timer stops and enables interrupts.) If an IRQ0 interrupt
is pending, then timer 0 has turned over and generated a timer
interrupt. Recall that **ZTimerOn** initially sets timer 0 to 0, in
interrupt. Recall that `ZTimerOn` initially sets timer 0 to 0, in
order to allow for the longest possible period—about 54 ms—before timer
0 reaches 0 and generates the timer interrupt.
@ -39,7 +39,7 @@ started. In addition, a timer interrupt is generated when timer 0 is
switched from mode 3 to mode 2, advancing the system clock by up to
54.925 ms, although this only happens the first time the Zen timer is
run after a warm or cold boot. Finally, up to 54.925 ms can again be
lost when **ZTimerOff** is called, since that routine again sets the
lost when `ZTimerOff` is called, since that routine again sets the
timer count to zero. Net result: The system clock will run up to 110 ms
(about a ninth of a second) slow each time the Zen timer is used.
@ -70,27 +70,27 @@ is correct.
### Stopping the Zen Timer {#Heading8}
At some point after **ZTimerOn** is called, **ZTimerOff** must always be
called to mark the end of the timing interval. **ZTimerOff** saves the
At some point after `ZTimerOn` is called, `ZTimerOff` must always be
called to mark the end of the timing interval. `ZTimerOff` saves the
context of the calling program, latches and reads the timer 0 count,
converts that count from the countdown value that the timer maintains to
the number of counts elapsed since **ZTimerOn** was called, and stores
the number of counts elapsed since `ZTimerOn` was called, and stores
the result. Immediately after latching the timer 0 count—and before
enabling interrupts—**ZTimerOff** checks the 8259 interrupt controller
enabling interrupts—`ZTimerOff` checks the 8259 interrupt controller
to see if there is a pending timer interrupt, setting a flag to mark
that the timer overflowed if there is indeed a pending timer interrupt.
After that, **ZTimerOff** executes just the overhead code of
**ZTimerOn** and **ZTimerOff** 16 times, and averages and saves the
After that, `ZTimerOff` executes just the overhead code of
`ZTimerOn` and `ZTimerOff` 16 times, and averages and saves the
results in order to determine how many of the counts in the timing
result just obtained were incurred by the overhead of the Zen timer
rather than by the code being timed.
Finally, **ZTimerOff** restores the context of the calling program,
Finally, `ZTimerOff` restores the context of the calling program,
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
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
@ -102,48 +102,48 @@ without breaking stride.
### Reporting Timing Results {#Heading9}
**ZTimerReport** may be called to display timing results at any time
after both **ZTimerOn** and **ZTimerOff** have been called.
**ZTimerReport** first checks to see whether the timer overflowed
(counted down to 0 and turned over) before **ZTimerOff** was called; if
overflow did occur, **ZTimerOff** prints a message to that effect and
returns. Otherwise, **ZTimerReport** subtracts the reference count
`ZTimerReport` may be called to display timing results at any time
after both `ZTimerOn` and `ZTimerOff` have been called.
`ZTimerReport` first checks to see whether the timer overflowed
(counted down to 0 and turned over) before `ZTimerOff` was called; if
overflow did occur, `ZTimerOff` prints a message to that effect and
returns. Otherwise, `ZTimerReport` subtracts the reference count
(representing the overhead of the Zen timer) from the count measured
between the calls to **ZTimerOn** and **ZTimerOff**, converts the result
between the calls to `ZTimerOn` and `ZTimerOff`, converts the result
from timer counts to microseconds, and prints the resulting time in
microseconds to the standard output.
Note that **ZTimerReport** need not be called immediately after
**ZTimerOff**. In fact, after a given call to **ZTimerOff,
ZTimerReport** can be called at any time right up until the next call to
**ZTimerOn**.
Note that `ZTimerReport` need not be called immediately after
`ZTimerOff`. In fact, after a given call to `ZTimerOff,
ZTimerReport` can be called at any time right up until the next call to
`ZTimerOn`.
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
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
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
Debugger or CodeView), placing a breakpoint at the start of
**ZTimerReport**, and directly observing the count in microseconds as
**ZTimerReport** calculates it.
`ZTimerReport`, and directly observing the count in microseconds as
`ZTimerReport` calculates it.
A second approach is modification of **ZTimerReport** to place the
A second approach is modification of `ZTimerReport` to place the
result at some safe location in memory, such as an unused portion of the
BIOS data area.
A third approach is alteration of **ZTimerReport** to print the result
A third approach is alteration of `ZTimerReport` to print the result
over a serial port to a terminal or to another PC acting as a terminal.
Similarly, many debuggers can be run from a remote terminal via a serial
link.
Yet another approach is modification of **ZTimerReport** to send the
Yet another approach is modification of `ZTimerReport` to send the
result to the printer via either DOS function 5 or BIOS interrupt 17H.
A final approach is to modify **ZTimerReport** to print the result to
A final approach is to modify `ZTimerReport` to print the result to
the auxiliary output via DOS function 4, and to then write and load a
special device driver named **AUX**, to which DOS function 4 output
special device driver named `AUX`, to which DOS function 4 output
would automatically be directed. This device driver could send the
result anywhere you might desire. The result might go to the secondary
display adapter, over a serial port, or to the printer, or could simply

View file

@ -13,7 +13,7 @@ pages: 048-050
### Notes on the Zen Timer {#Heading10}
The Zen timer subroutines are designed to be near-called from assembly
language code running in the public segment **Code**. The Zen timer
language code running in the public segment `Code`. The Zen timer
subroutines can, however, be called from any assembly or high-level
language code that generates OBJ files that are compatible with the
Microsoft linker, simply by modifying the segment that the timer code
@ -26,9 +26,9 @@ transparent to the calling code.
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*
the Zen timer routines far, including **ReferenceZTimerOn** and
**ReferenceZTimerOff**. (You'll have to put **FAR PTR** overrides on the
calls from **ZTimerOff** to the latter two routines if you do make them
the Zen timer routines far, including `ReferenceZTimerOn` and
`ReferenceZTimerOff`. (You'll have to put `FAR PTR` overrides on the
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
other routines, they won't reflect the true overhead incurred by
starting and stopping the Zen timer.
@ -76,9 +76,9 @@ computers.
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
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,
and should contain calls to **ZTimerOn** and **ZTimerOff** .
and should contain calls to `ZTimerOn` and `ZTimerOff` .
**LISTING 3.2 PZTEST.ASM**
@ -124,10 +124,10 @@ Code ends
Listing 3.3 shows some sample code to be timed. This listing measures
the time required to execute 1,000 loads of AL from the memory variable
**MemVar** . Note that Listing 3.3 calls **ZTimerOn** to start timing,
performs 1,000 **MOV** instructions in a row, and calls **ZTimerOff** to
`MemVar` . Note that Listing 3.3 calls `ZTimerOn` to start timing,
performs 1,000 `MOV` instructions in a row, and calls `ZTimerOff` to
end timing. When Listing 3.2 is named TESTCODE and included by Listing
3.3, Listing 3.2 calls **ZTimerReport** to display the execution time
3.3, Listing 3.2 calls `ZTimerReport` to display the execution time
after the code in Listing 3.3 has been run.
**LISTING 3.3 LST3-3.ASM**
@ -160,7 +160,7 @@ Skip:
```
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
any needed data right in the code segment and jumping around that data,
each listing becomes self-contained and can be plugged directly into
@ -169,7 +169,7 @@ anything else precisely so that data can be embedded in code fragments
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
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
TESTCODE is timed, and we can keep set-up code and the like out of the
timing interval.

View file

@ -107,7 +107,7 @@ repetitions of a given instruction are timed, there's just too much
noise in the timing process—between dynamic RAM refresh, the prefetch
queue, and the internal state of the processor at the start of
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
execution time for this instruction. (See the MASM or TASM
documentation, or Intel's processor reference manuals, for official
@ -127,8 +127,8 @@ pztime <filename>
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
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,
ZTimerOff**, and **ZTimerReport** in the appropriate places and link
the test-bed program of Listing 3.2, simply insert calls to `ZTimerOn,
ZTimerOff`, and `ZTimerReport` in the appropriate places and link
PZTIMER to your program.
### The Long-Period Zen Timer {#Heading12}

View file

@ -67,10 +67,10 @@ 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.
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**
equate selects between the two modes of operation. If **PS2** is 1 (as
the undocumented timer-stopping feature, as you please. The `PS2`
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
**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
occasionally produce results that are incorrect by 54 ms. The timer-stop
approach avoids synchronization problems, but doesn't work on all

View file

@ -15,25 +15,25 @@ approach could conceivably cause erratic 8253 operation, which could in
turn seriously affect your computer's operation until the next reboot.
In non-8253-compatible systems, I've observed not only wildly incorrect
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
alert for signs of trouble if you do set **PS2** to 0.
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.
Rebooting should clear up any timer-related problems of the sort
described above. (This gives us another reason to reboot at the end of
each code-timing session.) You should *immediately* reboot and set the
**PS2** equate to 1 if you get erratic or obviously incorrect results
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
in this book with **PS2** set first to 1 and then to 0, to make sure
`PS2` equate to 1 if you get erratic or obviously incorrect results
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
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
set **PS2** to 1.
set `PS2` to 1.
While the the non-PS/2 version is more dangerous than the PS/2 version,
it also produces more accurate results when it does work. If you have a
non-PS/2 PC-compatible computer, the choice between the two timing
approaches is yours.
If you do leave the **PS2** equate at 1 in Listing 3.5, you should
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
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
@ -58,7 +58,7 @@ 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.
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
interrupts must be left on in order to time periods longer than 54 ms,
the interrupts generated by keystrokes (including the upstroke of the

View file

@ -124,31 +124,31 @@ the result is 72,544 µs, or about 3.63 µs per load of AL from memory.
This is just slightly longer than the time per load of AL measured by
the precision Zen timer, as we would expect given that interrupts are
left enabled by the long-period Zen timer. The extra fraction of a
microsecond measured per **MOV** reflects the time required to execute
microsecond measured per `MOV` reflects the time required to execute
the BIOS code that handles the 18.2 timer interrupts that occur each
second.
Note that the command can take as much as 10 minutes to finish on a slow
PC if you are using MASM, with most of that time spent assembling
Listing 3.8. Why? Because MASM is notoriously slow at assembling
**REPT** blocks, and the block in Listing 3.8 is repeated 20,000 times.
`REPT` blocks, and the block in Listing 3.8 is repeated 20,000 times.
### Using the Zen Timer from C {#Heading15}
The Zen timer can be used to measure code performance when programming
in C—but not right out of the box. As presented earlier, the timer is
designed to be called from assembly language; some relatively minor
modifications are required before the **ZTimerOn** (start timer),
**ZTimerOff** (stop timer), and **ZTimerReport** (display timing
modifications are required before the `ZTimerOn` (start timer),
`ZTimerOff` (stop timer), and `ZTimerReport` (display timing
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
one, the small code model, first.
Altering the Zen timer for linking to a small code model C program
involves the following steps: **C** hange **ZTimerOn** to
**\_ZTimerOn**, change **ZTimerOff** to **\_ZTimerOff**, change
**ZTimerReport** to **\_ZTimerReport**, and change **Code** to
**\_TEXT** . Figure 3.2 shows the line numbers and new states of all
involves the following steps: `C` hange `ZTimerOn` to
`_ZTimerOn`, change `ZTimerOff` to `_ZTimerOff`, change
`ZTimerReport` to `_ZTimerReport`, and change `Code` to
`_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
code to use C-style external label names and the small model C code
segment. (In C++, use the "C" specifier, as in

View file

@ -10,7 +10,7 @@ chapter: '03'
pages: 070-073
---
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.)
That's all it takes; after doing this, you'll be able to use the Zen
@ -59,8 +59,8 @@ push cs
call near ptr ReferenceZTimerOn
```
(and likewise for **ReferenceZTimerOff** ), which works because
**ReferenceZTimerOn** is in the same segment as the calling code. This
(and likewise for `ReferenceZTimerOff` ), which works because
`ReferenceZTimerOn` is in the same segment as the calling code. This
is normally a great optimization, being both smaller and faster than a
far call. However, it's not so great for the Zen
@ -68,12 +68,12 @@ far call. However, it's not so great for the Zen
timer, because our purpose in calling the reference timing code is to
determine exactly how much time is taken by overhead code—including the
far calls to **ZTimerOn** and **ZTimerOf**f! By converting the far calls
far calls to `ZTimerOn` and `ZTimerOf`f! By converting the far calls
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
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
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

View file

@ -57,13 +57,13 @@ required for every word-sized access to a memory operand. For instance,
mov ax,word ptr [MemVar]
```
takes 4 cycles longer to read the word at address **MemVar** than
takes 4 cycles longer to read the word at address `MemVar` than
```nasm
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
that will become clear once we discuss the prefetch queue and dynamic
RAM refresh cycle-eaters later in this chapter.)
@ -88,7 +88,7 @@ add byte ptr [MemVar],al
String instructions can suffer from the 8-bit bus cycle-eater to a
greater extent than other instructions. Believe it or not, a single
**REP MOVSW** instruction can lose as much as 131,070 word-sized memory
`REP MOVSW` instruction can lose as much as 131,070 word-sized memory
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
does a great deal) can take over one-tenth of a second longer on an 8088
@ -115,9 +115,9 @@ 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
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
word-sized **DEC** performs 4 byte-sized memory accesses (two to read
word-sized `DEC` performs 4 byte-sized memory accesses (two to read
the word-sized operand and two to write the result back to memory),
while each byte-sized **DEC** performs only 2 byte-sized memory accesses
while each byte-sized `DEC` performs only 2 byte-sized memory accesses
in all.
**LISTING 4.1 LST4-1.ASM**
@ -161,8 +161,8 @@ listings in this book. Throughout this book I've modeled the sample code
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
have shown a still greater advantage for byte-sized operands simply by
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
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
listings I used code that both decremented the counter and tested the
result. The difference is that between decrementing a memory location
(simply an instruction) and using a loop counter (a functional

View file

@ -47,7 +47,7 @@ accesses must be. (Remember that the Bus Interface Unit must perform two
byte-sized memory accesses in order to handle a word-sized memory
operand.) However, Listing 4.3 is considerably faster because it expends
only 4 additional cycles to read the second byte of each word, while
Listing 4.4 performs a second **LODSB,** requiring 13 cycles, to read
Listing 4.4 performs a second `LODSB`, requiring 13 cycles, to read
the second byte of each word.
**LISTING 4.3 LST4-3.ASM**
@ -166,7 +166,7 @@ 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
contain Intel's official instruction execution times, but in this
case—and in many others—the specifications are drastically wrong. Why?

View file

@ -12,7 +12,7 @@ pages: 087-090
#### Official Execution Times Are Only Part of the Story {#Heading10}
The sequence of 5 **SHR** instructions in the last example is 10 bytes
The sequence of 5 `SHR` instructions in the last example is 10 bytes
long. That means that it can never execute in less than 24 cycles even
if the 4-byte prefetch queue is full when it starts, since 6 instruction
bytes would still remain to be fetched, at 4 cycles per fetch. If the
@ -90,17 +90,17 @@ that), and some of the other prefetch states may well produce distinctly
different results.
For example, consider the code in Listings 4.5 and 4.6. Listing 4.5
shows our familiar **SHR** case. Here, because the prefetch queue is
shows our familiar `SHR` case. Here, because the prefetch queue is
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
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
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.
(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
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**
@ -138,27 +138,27 @@ that the "true" execution time of **SHR** is 8.64 cycles.
![**Figure 4.3**  *Execution and instruction prefetching sequence for
Listing 4.5.*](images/04-03.jpg)
Now let's examine Listing 4.6. Here each **SHR** follows a **MUL**
instruction. Since **MUL** instructions take so long to execute that the
prefetch queue is always full when they finish, each **SHR** should be
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;
Now let's examine Listing 4.6. Here each `SHR` follows a `MUL`
instruction. Since `MUL` instructions take so long to execute that the
prefetch queue is always full when they finish, each `SHR` should be
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;
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
execution time of 25.14 µs per **SHR/MUL** pair, or *exactly* 120
cycles! According to these results, the "true" execution time of **SHR**
execution time of 25.14 µs per `SHR/MUL` pair, or *exactly* 120
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
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
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
in which the preceding code left the prefetch queue. In Listing 4.5,
each **SHR** after the first few follows a slew of other **SHR**
each `SHR` after the first few follows a slew of other `SHR`
instructions which have sucked the prefetch queue dry, so overall
performance reflects instruction fetch time. By contrast, each **SHR**
in Listing 4.6 follows a **MUL** instruction which leaves the prefetch
performance reflects instruction fetch time. By contrast, each `SHR`
in Listing 4.6 follows a `MUL` instruction which leaves the prefetch
queue full, so overall performance reflects Execution Unit execution
time.

View file

@ -30,9 +30,9 @@ 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
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
**MOV AL,0** instructions, and which executes at the rate of 1.81 µs per
instruction. Now consider Listing 4.8, which replaces the 4-cycle **MOV
AL,0** with the 3-cycle (but still 2-byte) **SUB AL,AL,** Despite its
`MOV AL,0` instructions, and which executes at the rate of 1.81 µs per
instruction. Now consider Listing 4.8, which replaces the 4-cycle `MOV
AL,0` with the 3-cycle (but still 2-byte) `SUB AL,AL,` Despite its
1-cycle-per-instruction advantage, Listing 4.8 runs at exactly the same
speed as Listing 4.7. The reason: Both instructions are 2 bytes long,
and in both cases it is the 8-cycle instruction fetch time, not the 3 or
@ -108,19 +108,19 @@ in this book, and use the Zen timer to measure your code.
Don't think that because overall instruction execution time is
determined by both instruction fetch time and Execution Unit execution
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
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
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
the 2 cycles of Execution Unit execution time.
Alternatively, you could think of each **SHR** in Listing 4.5 as taking
Alternatively, you could think of each `SHR` in Listing 4.5 as taking
8 cycles to fetch, and then executing in effectively 0 cycles while the
next **SHR** is being fetched. Whichever perspective you prefer is fine.
next `SHR` is being fetched. Whichever perspective you prefer is fine.
The important point is that the time during which the execution of one
instruction and the fetching of the next instruction overlap should only
be counted toward the overall execution time of one of the instructions.
@ -150,7 +150,7 @@ While short instructions minimize overall prefetch time, ironically they
actually often suffer more from the prefetch queue bottleneck than do
long instructions. Short instructions generally have such fast execution
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
bytes long, thanks to the prefetch queue bottleneck. Short instructions
are nonetheless generally faster than long instructions, thanks to the

View file

@ -14,7 +14,7 @@ pages: 097-099
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
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
long, there should be plenty of time for the prefetch queue to fill
after each instruction, even after DRAM refresh has taken its slice of
@ -42,15 +42,15 @@ request a memory access from the Bus Interface Unit.)
call ZTimerOff
```
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
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
execute, we can see that no performance is lost to DRAM refresh. Listing
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
memory access.
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
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
prefetch queue should be empty while Listing 4.10 executes, with the
8088 prefetching instruction bytes non-stop. As a result, the time per
instruction of Listing 4.10 should precisely reflect the time required
@ -71,8 +71,8 @@ to fetch the instruction bytes.
```
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
refresh. In fact, each **SHR** in Listing 4.10 executes in 1.81 µs,
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,
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
estimate of the loss to DRAM refresh overhead! In fact, the result
@ -84,7 +84,7 @@ 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
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
is in executing the current **SHR** instruction when the refresh request
is in executing the current `SHR` instruction when the refresh request
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
time—6 out of every 72 cycles—from the 8088.*

View file

@ -79,8 +79,8 @@ computer standards.
That sounds pretty serious, but we did make an unfounded assumption
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
MOVSW** with display memory as both source and destination. The code in
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
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.
@ -119,7 +119,7 @@ long as we had assumed, but a long time nonetheless.
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,
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
on average, 1.79 µs (more than 8 cycles!) are lost to the display
adapter cycle-eater on each access. In other words, the display adapter

View file

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

View file

@ -72,7 +72,7 @@ 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
the command line for a string (also specified on the comline), then
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.)
This is a *very* limited subset of what search utilities such as grep
@ -115,9 +115,9 @@ access overhead).
### Avoiding the String Trap {#Heading4}
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
for the first occurrence of a second string. However, while **strstr()**
closest match to what we need is `strstr()`, which searches one string
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
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.

View file

@ -10,11 +10,11 @@ chapter: '05'
pages: 115-118
---
We could put a zero byte at the end of our buffer to allow **strstr()**
to work, but why bother? The **strstr()** function must spend time
We could put a zero byte at the end of our buffer to allow `strstr()`
to work, but why bother? The `strstr()` function must spend time
either checking for the end of the string being searched or determining
the length of that string—wasted effort given that we already know
exactly how long our search buffer is. Even if a given **strstr()**
exactly how long our search buffer is. Even if a given `strstr()`
implementation is well-written, its performance will suffer, at least
for our application, from unnecessary overhead.
@ -28,26 +28,26 @@ for our application, from unnecessary overhead.
Given that no C/C++ library function meets our needs precisely, an
obvious alternative approach is the brute-force technique that uses
**memcmp()** to compare *every* potential matching location in the
`memcmp()` to compare *every* potential matching location in the
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
in a loop, to perform the comparison in place of **memcmp()**. But
**memcmp()** will almost certainly use the very fast **REPZ CMPS**
in a loop, to perform the comparison in place of `memcmp()`. But
`memcmp()` will almost certainly use the very fast `REPZ CMPS`
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
language implementation of **memcmp()**.
language implementation of `memcmp()`.
![**Figure 5.1**  *The brute-force searching technique.*](images/05-01.jpg)
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
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
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
reject most such locations right off the bat. We'll just check whether
the first character of the potentially matching buffer location matches
@ -60,16 +60,16 @@ Figure 5.2.
### Using memchr() {#Heading6}
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
(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
**memchr()** to scan for potential matches that can then be fully tested
with **memcmp()**, we can build a highly efficient search engine that
`memchr()` to scan for potential matches that can then be fully tested
with `memcmp()`, we can build a highly efficient search engine that
takes good advantage of the information we have about the buffer being
searched and the string we're searching for. Our engine also relies
heavily on repeated string instructions, assuming that the **memchr()**
and **memcmp()** library functions are properly coded.
heavily on repeated string instructions, assuming that the `memchr()`
and `memcmp()` library functions are properly coded.
![**Figure 5.2**  *The faster string-searching technique.*](images/05-02.jpg)
@ -94,7 +94,7 @@ let's make it restartable.
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
(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
is found.
@ -115,7 +115,7 @@ 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
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
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
listing).

View file

@ -13,7 +13,7 @@ pages: 121-122
### Interpreting Where the Cycles Go {#Heading8}
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
lesson as any discussion of optimized assembly code is likely to be.
Take a moment to examine some interesting performance aspects of the C
@ -28,7 +28,7 @@ think.
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
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
the block size shrinks to 64 bytes that execution time becomes
approximately double that of the 16K buffer.
@ -38,23 +38,23 @@ for the best performance, the increment in performance may not be very
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
large chunks, most of the execution time of Listing 5.1 is nonetheless
spent in executing the **read()** function.
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
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
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
in which a single call to **memchr()** (and thus a single **REPNZ
SCASB**) can eliminate an entire block at a time.
in which a single call to `memchr()` (and thus a single `REPNZ
SCASB`) can eliminate an entire block at a time.
All in all, the time required for DOS disk access calls is taking up at
least 80 percent of execution time, and search time is less than 20
percent of overall execution time. In fact, search time is probably a
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
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
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
@ -74,13 +74,13 @@ If, for example, your application will typically search buffers in which
the first character of the search string occurs frequently as might be
the case when searching a text buffer for a string starting with the
space character an assembly implementation might be several times
faster. Why? Because assembly code can switch from **REPNZ SCASB** to
match the first character to **REPZ CMPS** to check the remaining
faster. Why? Because assembly code can switch from `REPNZ SCASB` to
match the first character to `REPZ CMPS` to check the remaining
characters in just a few instructions.
In contrast, Listing 5.1 must return from **memchr()**, set up
parameters, and call **memcmp()** in order to do the same thing.
Likewise, assembly can switch back to **REPNZ SCASB** after a non-match
In contrast, Listing 5.1 must return from `memchr()`, set up
parameters, and call `memcmp()` in order to do the same thing.
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
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

View file

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

View file

@ -60,7 +60,7 @@ programming, and especially so when working in assembly language, where
many instructions have talents above and beyond their obvious abilities.
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
by its cover, either.
@ -84,9 +84,9 @@ of not judging a book by its cover.
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.
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,
but it can't put the result in a third register; **LEA** can. The
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,
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.
By limiting yourself to thinking only in certain well-established ways
about the various instructions, you're putting yourself at a substantial

View file

@ -14,7 +14,7 @@ The two approaches are functionally interchangeable but *not* equivalent
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
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
addition outside the loop, if possible, reducing effective address
calculation time inside the loop, as in the following:
@ -27,13 +27,13 @@ LoopTop:
loop LoopTop
```
Here, **MOV AL,[BX]** is two cycles faster than **MOV AL,[BX+SI]**.
Here, `MOV AL,[BX]` is two cycles faster than `MOV AL,[BX+SI]`.
On a 286 or 386, however, the balance shifts. **MOV AL,[BX+SI]** takes
no longer than **MOV AL,[BX]** on these processors because effective
On a 286 or 386, however, the balance shifts. `MOV AL,[BX+SI]` takes
no longer than `MOV AL,[BX]` on these processors because effective
address calculations generally take no extra time at all. (According to
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
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
@ -59,15 +59,15 @@ instructions, at that.
How?
With **LEA**, the only instruction that performs memory addressing
calculations but doesn't actually address memory. **LEA** accepts a
With `LEA`, the only instruction that performs memory addressing
calculations but doesn't actually address memory. `LEA` accepts a
standard memory addressing operand, but does nothing more than store the
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
**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 store the result in *any* register, not just in one of the
source operands.
@ -102,20 +102,20 @@ or:
lea di,[si+2]
```
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
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
than three memory addressing components are used. **LEA** is 1 cycle
slower than **ADD** on a 486 if the sum of two registers is used to
point to memory, but no slower than **ADD** on a Pentium. On both a 486
and Pentium, **LEA** can also be slowed down by addressing interlocks.
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
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
than three memory addressing components are used. `LEA` is 1 cycle
slower than `ADD` on a 486 if the sum of two registers is used to
point to memory, but no slower than `ADD` on a Pentium. On both a 486
and Pentium, `LEA` can also be slowed down by addressing interlocks.
![**Figure 6.1**  *Operation of ADD Reg,Reg vs. LEA Reg,{Addr}.*](images/06-01.jpg)
#### 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
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.)
@ -129,15 +129,15 @@ that's good for.
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
constant, can be added together, with the result stored in any register.
This makes the 32-bit **LEA** much more generally useful than the
standard 16-bit **LEA** in the role of an **ADD** with an independent
This makes the 32-bit `LEA` much more generally useful than the
standard 16-bit `LEA` in the role of an `ADD` with an independent
destination.
![**Figure 6.2**  *Operation of the 32-bit LEA reg,[Addr].*](images/06-02.jpg)
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
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
@ -162,8 +162,8 @@ when pointing to an entry in a doubly indexed table.
### Multiplication with LEA Using Non-Powers of Two {#Heading6}
Are you impressed yet with all that **LEA** can do on the 386? Believe
it or not, one more feature still awaits us. **LEA** can actually
Are you impressed yet with all that `LEA` can do on the 386? Believe
it or not, one more feature still awaits us. `LEA` can actually
perform a fast multiply of a 32-bit register by some values *other* than
powers of two. You see, the same 32-bit register can be both base and
index on the 386, and can be scaled as the index while being used
@ -174,8 +174,8 @@ EBX by 5 with:
lea ebx,[ebx+ebx*4]
```
Without **LEA** and scaling, multiplication of EBX by 5 would require
either a relatively slow **MUL**, along with a set-up instruction or
Without `LEA` and scaling, multiplication of EBX by 5 would require
either a relatively slow `MUL`, along with a set-up instruction or
two, or three separate instructions along the lines of the following
```nasm
@ -192,11 +192,11 @@ cycles is a pretty neat trick, even though it works only on a 386 or
486.
> ![](images/i.jpg)
> The full list of values that **LEA** can multiply a register by on a 386
> The full list of values that `LEA` can multiply a register by on a 386
> or 486 is: 2, 3, 4, 5, 8, and 9. That list doesn't include every
> multiplier you might want, but it covers some commonly used ones, and
> the performance is hard to beat.
I'd like to extend my thanks to Duane Strong of Metagraphics for his
help in brainstorming uses for the 386 version of **LEA** and for
help in brainstorming uses for the 386 version of `LEA` and for
pointing out the complications of 486 instruction timings.

View file

@ -91,37 +91,37 @@ there? Let me put it this way: If I were, I'd never admit it!
#### When LOOP Is a Bad Idea {#Heading3}
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
to the task of counting down loops that any experienced x86 programmer
instinctively stuffs the loop count in CX and reaches for **LOOP** when
setting up a loop. That's fine—**LOOP** does, of course, work as
instinctively stuffs the loop count in CX and reaches for `LOOP` when
setting up a loop. That's fine—`LOOP` does, of course, work as
advertised—but there is one problem:
> ![](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
> On half of the processors in the x86 family, `LOOP` is slower than
> `DEC CX` followed by `JNZ`. (Granted, `DEC CX/JNZ` isn't precisely
> equivalent to `LOOP`, because `DEC` alters the flags and LOOP
> doesn't, but in most situations they're comparable.)
How can this be? Don't ask me, ask Intel. On the 8088 and 80286,
**LOOP** is indeed faster than **DEC CX/JNZ** by a cycle, and **LOOP**
`LOOP` is indeed faster than `DEC CX/JNZ` by a cycle, and `LOOP`
is generally a little faster still because it's a byte shorter and so
can be fetched faster. On the 386, however, things change; **LOOP** is
two cycles *slower* than **DEC/JNZ,** and the fetch time for one extra
can be fetched faster. On the 386, however, things change; `LOOP` is
two cycles *slower* than `DEC/JNZ` and the fetch time for one extra
byte on even an uncached 386 generally isn't significant. (Remember that
the 386 fetches four instruction bytes at a pop.) **LOOP** is three
cycles slower than **DEC/JNZ** on the 486, and the 486 executes
the 386 fetches four instruction bytes at a pop.) `LOOP` is three
cycles slower than `DEC/JNZ` on the 486, and the 486 executes
instructions in so few cycles that those three cycles mean that
**DEC/JNZ** is nearly *twice* as fast as **LOOP**. Then, too, unlike
**LOOP, DEC** doesn't require that **CX** be used, so the **DEC/JNZ**
`DEC/JNZ` is nearly *twice* as fast as `LOOP`. Then, too, unlike
`LOOP, DEC` doesn't require that `CX` be used, so the `DEC/JNZ`
solution is both faster and more flexible on the 386 and 486, and on the
Pentium as well. (By the way, all this is not just theory; I've timed
the relative performances of **LOOP** and **DEC CX/JNZ** on a cached
the relative performances of `LOOP` and `DEC CX/JNZ` on a cached
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
> 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

@ -10,7 +10,7 @@ chapter: '07'
pages: 139-141
---
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:
```nasm
@ -18,19 +18,19 @@ and cx,ofh ;Isolate the desired field
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
```nasm
and cx,0fh ;Isolate the desired field
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.
### The Lessons of LOOP and JCXZ {#Heading4}
What can we learn from **LOOP** and **JCXZ?** First, that a single
What can we learn from `LOOP` and `JCXZ`? First, that a single
instruction that is intended to do a complex task is not necessarily
faster than several instructions that together do the same thing.
Second, that the relative merits of instructions and optimization rules
@ -39,11 +39,11 @@ vary to a surprisingly large degree across the x86 family.
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
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
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
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
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;
@ -53,8 +53,8 @@ relative performance levels of x86 instructions.
#### Avoiding LOOPS of Any Stripe {#Heading5}
Cycle counting and directly substituting instructions (**DEC CX/JNZ**
for **LOOP,** for example) are techniques that belong at the lowest
Cycle counting and directly substituting instructions (`DEC CX/JNZ`
for `LOOP`, for example) are techniques that belong at the lowest
level of optimization. It's an important level, but it's fairly
mechanical; once you've learned the capabilities and relative
performance levels of the various instructions, you should be able to
@ -74,16 +74,16 @@ characteristics. Your job is to sequence those blocks so that they
perform well. It doesn't matter what the instructions are intended to do
or what their names are; all that matters is what they *do.*
Our discussion of **LOOP** versus **DEC/JNZ** is an excellent example of
Our discussion of `LOOP` versus `DEC/JNZ` is an excellent example of
optimization by cycle counting. It's worth knowing, but once you've
learned it, you just routinely use **DEC/JNZ** at the bottom of loops in
learned it, you just routinely use `DEC/JNZ` at the bottom of loops in
386/486-specific code, and that's that. Besides, you'll save at most a
few cycles each time, and while that helps a little, it's not going to
make all *that* much difference.
Now let's step back for a moment, and with no preconceptions consider
what the x86 instruction set can do for us. The bulk of the time with
both **LOOP** and **DEC/JNZ** is taken up by branching, which just
both `LOOP` and `DEC/JNZ` is taken up by branching, which just
happens to be one of the slowest aspects of every processor in the x86
family, and the rest is taken up by decrementing the count register and
checking whether it's zero. There may be ways to perform those tasks a
@ -101,17 +101,17 @@ Consider Listing 7.1, which searches a buffer until either the specified
byte is found, a zero byte is found, or the specified number of
characters have been checked. Such a function would be useful for
scanning up to a maximum number of characters in a zero-terminated
buffer. Listing 7.1, which uses **LOOP** in the main loop, performs a
buffer. Listing 7.1, which uses `LOOP` in the main loop, performs a
search of the sample string for a period (.') in 170 µs on a 20 MHz
cached 386.
When the **LOOP** in Listing 7.1 is replaced with **DEC CX/JNZ,**
When the `LOOP` in Listing 7.1 is replaced with `DEC CX/JNZ`,
performance improves to 168 µs, less than 2 percent faster than Listing
7.1. Actually, instruction fetching, instruction alignment, cache
characteristics, or something similar is affecting these results; I'd
expect a slightly larger improvement—around 7 percent—but that's the
most that counting cycles could buy us in this case. (All right,
already; **LOOPNZ** could be used at the bottom of the loop, and other
already; `LOOPNZ` could be used at the bottom of the loop, and other
optimizations are surely possible, but all that won't add up to anywhere
near the benefits we're about to see from local optimization, and that's
the whole point.)

View file

@ -108,9 +108,9 @@ SearchMaxLengthendp
### Unrolling Loops {#Heading7}
Listing 7.2 takes a different tack, unrolling the loop so that four
bytes are checked for each **LOOP** performed. The same instructions are
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
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
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

View file

@ -144,8 +144,8 @@ SearchMaxLengthendp
```
How much difference? Listing 7.2 runs in 121 µs—40 percent faster than
Listing 7.1, even though Listing 7.2 still uses **LOOP** rather than
**DEC CX/JNZ.** (The loop in Listing 7.2 could be unrolled further, too;
Listing 7.1, even though Listing 7.2 still uses `LOOP` rather than
`DEC CX/JNZ`. (The loop in Listing 7.2 could be unrolled further, too;
it's just a question of how much more memory you want to trade for
ever-decreasing performance benefits.) That's typical of local
optimization; it won't often yield the order-of-magnitude improvements

View file

@ -80,25 +80,25 @@ BIT_PATTERN=BIT_PATTERN SHL 1
#### NOT Flips Bits—Not Flags {#Heading9}
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
The `NOT` instruction flips all the bits in the operand, from 0 to 1
or from 1 to 0. That's as simple as could be, but `NOT` nonetheless
has a minor but interesting talent: It doesn't affect the flags. That
can be irritating; I once spent a good hour tracking down a bug caused
by my unconscious assumption that **NOT** does set the flags. After all,
by my unconscious assumption that `NOT` does set the flags. After all,
every other arithmetic and logical instruction sets the flags; why not
**NOT**? Probably because **NOT** isn't considered to be an arithmetic
`NOT`? Probably because `NOT` isn't considered to be an arithmetic
or logical instruction at all; rather, it's a data manipulation
instruction, like **MOV** and the various rotates. (These are **RCR,
RCL, ROR,** and **ROL,** which affect only the Carry and Overflow
instruction, like `MOV` and the various rotates. (These are `RCR`,
`RCL`, `ROR`, and `ROL`, which affect only the Carry and Overflow
flags.) NOT is often used for tasks, such as flipping masks, where
there's no reason to test the state of the result, and in that context
it can be handy to keep the flags unmodified for later testing.
> ![](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.
> Besides, if you want to `NOT` an operand and set the flags in the
> process, you can just `XOR` it with -1. Put another way, the only
> functional difference between `NOT AX` and `XOR AX,0FFFFH` is that
> `XOR` modifies the flags and `NOT` doesn't.
The x86 instruction set offers many ways to accomplish almost any task.
Understanding the subtle distinctions between the instructions—whether
@ -109,17 +109,17 @@ you're trying to minimize branching.
#### Incrementing with and without Carry {#Heading10}
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
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
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
obvious, but no less important, is that **ADD** sets the Carry flag
while **INC** leaves the Carry flag untouched.
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
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
the same length as `INC AL`), and is faster on some processors. Less
obvious, but no less important, is that `ADD` sets the Carry flag
while `INC` leaves the Carry flag untouched.
Why is that important? Because it allows **INC** to function as a data
Why is that important? Because it allows `INC` to function as a data
pointer manipulation instruction for multi-word arithmetic. You can use
**INC** to advance the pointers in code like that shown in Listing 7.5
`INC` to advance the pointers in code like that shown in Listing 7.5
without having to do any work to preserve the Carry status from one
addition to the next.
@ -137,7 +137,7 @@ LOOP_TOP:
LOOP LOOP_TOP
```
If **ADD** were used, the Carry flag would have to be saved between
If `ADD` were used, the Carry flag would have to be saved between
additions, with code along the lines shown in Listing 7.6.
**LISTING 7.6 L7-6.ASM**
@ -158,38 +158,38 @@ It's not that the Listing 7.6 approach is necessarily better or worse;
that depends on the processor and the situation. The Listing 7.6
approach is *different,* and if you understand the differences, you'll
be able to choose the best approach for whatever code you happen to
write. (**DEC** has the same property of preserving the Carry flag, by
write. (`DEC` has the same property of preserving the Carry flag, by
the way.)
There are a couple of interesting aspects to the last example. First,
note that **LOOP** doesn't affect any flags at all; this allows the
note that `LOOP` doesn't affect any flags at all; this allows the
Carry flag to remain unchanged from one addition to the next. Not
altering the arithmetic flags is a common characteristic of program
control instructions (as opposed to arithmetic and logical instructions
like **SUB** and **AND,** which do alter the flags).
like `SUB` and `AND`, which do alter the flags).
> ![](images/i.jpg)
> The rule is not that the arithmetic flags change whenever the CPU
> performs a calculation; rather, the flags change whenever you execute an
> arithmetic, logical, or flag control (such as **CLC** to clear the Carry
> arithmetic, logical, or flag control (such as `CLC` to clear the Carry
> flag) instruction.
Not only do **LOOP** and **JCXZ** not alter the flags, but **REP MOVS**,
Not only do `LOOP` and `JCXZ` not alter the flags, but `REP MOVS`,
which counts down CX to 0, doesn't affect the flags either.
The other interesting point about the last example is the use of
**LAHF** and **SAHF,** which transfer the low byte of the FLAGS register
`LAHF` and `SAHF`, which transfer the low byte of the FLAGS register
to and from AH, respectively. These instructions were created to help
provide compatibility with the 8080's (that's *8080*, not *8088*)
**PUSH** **PSW** and **POP PSW** instructions, but turn out to be
`PUSH` `PSW` and `POP PSW` instructions, but turn out to be
compact (one byte) instructions for saving and restoring the arithmetic
flags. A word of caution, however: **SAHF** restores the Carry, Zero,
flags. A word of caution, however: `SAHF` restores the Carry, Zero,
Sign, Auxiliary Carry, and Parity flags—but *not* the Overflow flag,
which resides in the high byte of the FLAGS register. Also, be aware
that **LAHF** and **SAHF** provide a fast way to preserve the flags on
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.
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
```nasm

View file

@ -53,14 +53,14 @@ over C is that it allows the use of all forms of instructions and all
registers in all ways, whereas C compilers tend to use a subset of
registers and instructions in a limited number of ways. Yes and no. It's
true that C compilers typically don't generate instructions such as
**XLAT,** rotates, or the string instructions. On the other hand,
**XLAT** and rotates are useful in a limited set of circumstances, and
`XLAT`, rotates, or the string instructions. On the other hand,
`XLAT` and rotates are useful in a limited set of circumstances, and
string instructions *are* used in the C library functions. In fact, C
library code is likely to be carefully optimized by experts, and may be
much better than equivalent code you'd produce yourself.
Am I saying that C compilers produce better code than you do? No, I'm
saying that they *can,* unless you use assembly language properly.
saying that they *can*, unless you use assembly language properly.
Writing code in assembly language rather than C guarantees nothing.
> ![](images/i.jpg)
@ -83,7 +83,7 @@ to something like Figure 8.1A. You might look at that and tweak it to
the code shown in Figure 8.1B.
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
a string instruction. Unfortunately, the new code isn't going to run
very much faster. Maybe 25 percent faster, maybe a little more. Big
@ -107,13 +107,13 @@ of better assembly language code in the small section of code that most
affects overall performance. For example, consider that the data
searched in the last example is stored in an array of structures, with
each structure in the array containing other information as well. In
this situation, **REP SCASW** couldn't be used because the data searched
this situation, `REP SCASW` couldn't be used because the data searched
through wouldn't be contiguous.
However, if the need for performance in searching the array is urgent
enough, there's no reason why you can't reorganize the data. This might
mean removing the array elements from the structures and storing them in
their own array so that **REP SCASW** *could* be used.
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
@ -145,5 +145,5 @@ That said, let me show some of these precepts in action.
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
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.

View file

@ -136,9 +136,9 @@ unsigned int FindIDAverage(unsigned int SearchedForID,
The main body of Listing 8.1 constructs a linked list of memory blocks
of various sizes and stores an array of structures across those blocks,
as shown in Figure 8.2. The function **FindIDAverage** in Listing 8.1
as shown in Figure 8.2. The function `FindIDAverage` in Listing 8.1
searches through that array for all matches to a specified ID number and
returns the average value of all such matches. **FindIDAverage**
returns the average value of all such matches. `FindIDAverage`
contains two nested loops, the outer one repeating once for each linked
block and the inner one repeating once for each array element in each
block. The inner loop—the critical one—is compact, containing only four
@ -147,11 +147,11 @@ statements, and should lend itself rather well to compiler optimization.
![**Figure 8.2**  *Linked array storage format (version 1).*](images/08-02.jpg)
As it happens, Microsoft C/C++ does optimize the inner loop of
**FindIDAverage** nicely. Listing 8.2 shows the code Microsoft C/C++
`FindIDAverage` nicely. Listing 8.2 shows the code Microsoft C/C++
generates for the inner loop, consisting of a mere seven assembly
language instructions inside the loop. The compiler is smart enough to
convert the loop index variable, which counts up but is used for nothing
but counting loops, into a count-down variable so that the **LOOP**
but counting loops, into a count-down variable so that the `LOOP`
instruction can be used.
**LISTING 8.2 L8-2.COD**

View file

@ -12,7 +12,7 @@ pages: 160-163
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
**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
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
@ -97,8 +97,8 @@ _FindIDAverage ENDP
Listing 8.4 tosses some sophisticated optimization techniques into the
mix. The loop is unrolled eight times, eliminating a good deal of
branching, and **SCASW** is used instead of **CMP [DI],AX.** (Note,
however, that **SCASW** is in fact slower than **CMP [DI],AX** on the
branching, and `SCASW` is used instead of `CMP [DI],AX`. (Note,
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
it's shorter and therefore may prefetch faster.) This advanced tweaking
produces a 39 percent improvement over the original C code—substantial,

View file

@ -170,9 +170,9 @@ _FindIDAverage2 ENDP
end
```
The whole point of this rearrangement is to allow us to use **REP
SCASW** to search through each block, and that's exactly what
**FindIDAverage2** in Listing 8.6 does. The result: Listing 8.6
The whole point of this rearrangement is to allow us to use `REP
SCASW` to search through each block, and that's exactly what
`FindIDAverage2` in Listing 8.6 does. The result: Listing 8.6
calculates the average about *three times* as fast as the original C
implementation and more than twice as fast as Listing 8.4, heavily
optimized as the latter code is.

View file

@ -78,14 +78,14 @@ feedback.
#### 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
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
**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
machine word (32 bits in 386 protected mode, 16 bits otherwise), but it
renders **LEA** useless for multiword operations, which use the Carry
renders `LEA` useless for multiword operations, which use the Carry
flag to tie together partial results. For example, these instructions
```nasm
@ -100,11 +100,11 @@ LEA EAX,[EAX+EBX]
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
uses **LEA** to advance the pointers while adding one 128-bit memory
uses `LEA` to advance the pointers while adding one 128-bit memory
variable to another such variable:
```nasm
@ -120,15 +120,15 @@ ADDLOOP:
LOOP ADDLOOP
```
(Yes, I could use **LODSD** instead of **MOV/LEA**; I'm just
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
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
either **PUSHF/POPF** or **LAHF/SAHF**. (Alternatively, we could use
multiple **INC**s, since **INC** doesn't affect the Carry flag.)
(Yes, I could use `LODSD` instead of `MOV/LEA`; I'm just
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
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
either `PUSHF/POPF` or `LAHF/SAHF`. (Alternatively, we could use
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
substitutions and optimizations that will occur to you over time.
There's no such thing as "best" instructions on the x86; it all depends
@ -180,7 +180,7 @@ ADC CX,CX ;CX=1 if copy length was odd,
REP MOVSB ;copy any odd byte
```
(**ADC CX,CX** can be replaced with **RCL CX,1**; which is faster
(`ADC CX,CX` can be replaced with `RCL CX,1`; which is faster
depends on the processor type.) It might be hard to believe that the
above is faster than this:

View file

@ -11,17 +11,17 @@ pages: 172-175
---
However, it generally is. Sure, if the length is odd, John's approach
incurs a penalty approximately equal to the **REP** startup time for
**MOVSB**. However, if the length is even, John's approach doesn't
incurs a penalty approximately equal to the `REP` startup time for
`MOVSB`. However, if the length is even, John's approach doesn't
branch, saving cycles and not emptying the prefetch queue. If copy
lengths are evenly distributed between even and odd, John's approach is
faster in most x86 systems. (Not on the 486, though.)
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**,
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`,
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
386, but only by the values 2, 3, 4, 5, 8, and 9; several **LEA**s
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
strung together can handle a much wider range of values. For example,
video programmers are undoubtedly familiar with the following code to
multiply AX times 80 (the width in bytes of the bitmap in most PC
@ -38,7 +38,7 @@ SH LAX,1 ;*64
ADD AX,BX ;*80
```
Using **LEA** on the 386, the above could be reduced to
Using `LEA` on the 386, the above could be reduced to
```nasm
LEA EAX,[EAX*2] ;*2
@ -68,12 +68,12 @@ ADD AX,BX ;*80
That brings us to multiplication, one of the slowest of x86 operations
and one that allows for considerable optimization. One way to speed up
multiplication is to use shift and add, **LEA**, or a lookup table to
multiplication is to use shift and add, `LEA`, or a lookup table to
hard-code a multiplication operation for a fixed multiplier, as shown
above. Another is to take advantage of the early-out feature of the 386
(and the 486, but in the interests of brevity I'll just say "386" from
now on) by arranging your operands so that the multiplier (always the
rightmost operand following **MUL** or **IMUL**) is no larger than the
rightmost operand following `MUL` or `IMUL`) is no larger than the
other operand.
> ![](images/i.jpg)
@ -97,17 +97,17 @@ regularly indicate that multiplication takes 3 to 4 cycles longer than
the specs indicate, but the cycle-per-bit advantage of smaller
multipliers holds true nonetheless.)
This highlights another interesting point: **MUL** and **IMUL** on the
This highlights another interesting point: `MUL` and `IMUL` on the
386 are so fast that alternative multiplication approaches, while
generally still faster, are worthwhile only in truly time-critical code.
> ![](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
> 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,
no longer to be avoided in most cases—and you can help that along by
arranging your code to make the smaller operand the multiplier whenever
you know which operand is smaller.
@ -121,33 +121,33 @@ scale value is the logical choice for the multiplier.
#### Optimizing Optimized Searching {#Heading6}
Rob Williams writes with a wonderful optimization to the **REPNZ
SCASB-**based optimized searching routine I discussed in Chapter 5. As a
Rob Williams writes with a wonderful optimization to the `REPNZ
SCASB`-based optimized searching routine I discussed in Chapter 5. As a
quick refresher, I described searching a buffer for a text string as
follows: Scan for the first byte of the text string with **REPNZ
SCASB**, then use **REPZ CMPS** to check for a full match whenever
**REPNZ SCASB** finds a match for the first character, as shown in
follows: Scan for the first byte of the text string with `REPNZ
SCASB`, then use `REPZ CMPS` to check for a full match whenever
`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
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
matches; each remaining potential match can then be checked in its
entirety with **REPZ CMPS**.
entirety with `REPZ CMPS`.
![**Figure 9.1**  *Simple searching method for locating a text string.*](images/09-01.jpg)
Rob's revelation, which he credits without explanation to Edgar Allen
Poe (search nevermore?), was that by far the slowest part of the whole
deal is handling **REPNZ SCASB** matches, which require checking the
remainder of the string with **REPZ CMPS** and restarting **REPNZ
SCASB** if no match is found.
deal is handling `REPNZ SCASB` matches, which require checking the
remainder of the string with `REPZ CMPS` and restarting `REPNZ
SCASB` if no match is found.
> ![](images/i.jpg)
> Rob points out that the number of **REPNZ SCASB** matches can easily be
> Rob points out that the number of `REPNZ SCASB` matches can easily be
> reduced simply by scanning for the character in the searched-for string
> that appears least often in the buffer being searched.
Imagine, if you will, that you're searching for the string "EQUAL." By
my approach, you'd use **REPNZ SCASB** to scan for each occurrence of
my approach, you'd use `REPNZ SCASB` to scan for each occurrence of
"E," which crops up quite often in normal text. Rob points out that it
would make more sense to scan for "Q," then back up one character and
check the whole string when a "Q" is found, as shown in Figure 9.2. "Q"

View file

@ -14,22 +14,22 @@ Listing 9.1 implements the scan-on-first-character approach. Listing 9.2
scans for whatever character the caller specifies. Listing 9.3 is a test
program used to compare the two approaches. How much difference does
Rob's revelation make? Plenty. Even when the entire C function call to
**FindString** is timed—**strlen** calls, parameter pushing, calling,
setup, and all—the version of **FindString** in Listing 9.2, which is
`FindString` is timed—`strlen` calls, parameter pushing, calling,
setup, and all—the version of `FindString` in Listing 9.2, which is
directed by Listing 9.3 to scan for the infrequently-occurring "Q," is
about 40 percent faster on a 20 MHz cached 386 for the test search of
Listing 9.3 than is the version of **FindString** in Listing 9.1, which
Listing 9.3 than is the version of `FindString` in Listing 9.1, which
always scans for the first character, in this case "E." However, when
only the search loops (the code that actually does the searching) in the
two versions of **FindString** are compared, Listing 9.2 is more than
two versions of `FindString` are compared, Listing 9.2 is more than
*twice* as fast as Listing 9.1—a remarkable improvement over code that
already uses **REPNZ SCASB** and **REPZ CMPS**.
already uses `REPNZ SCASB` and `REPZ CMPS`.
What I like so much about Rob's approach is that it demonstrates that
optimization involves much more than instruction selection and cycle
counting. Listings 9.1 and 9.2 use pretty much the same instructions,
and even use the same approach of scanning with **REPNZ SCASB** and
using **REPZ CMPS** to check scanning matches.
and even use the same approach of scanning with `REPNZ SCASB` and
using `REPZ CMPS` to check scanning matches.
> ![](images/i.jpg)
> The difference between Listings 9.1 and 9.2 (which gives you more than a

View file

@ -80,7 +80,7 @@ _sort: pop dx ;get return address (entry point)
#### Full 32-Bit Division {#Heading8}
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
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
@ -118,5 +118,5 @@ can be done easily enough by remembering the signs of the dividend and
divisor, dividing the absolute value of the dividend by the absolute
value of the divisor, and applying the stored signs to set the proper
signs for the quotient and remainder. There may be more clever ways to
produce the same result, by using **IDIV**, for example; if you know of
produce the same result, by using `IDIV`, for example; if you know of
one, drop me a line c/o Coriolis Group Books.

View file

@ -14,19 +14,19 @@ pages: 185-188
Next, we come to an item that cycle counters will love, especially since
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,
take 9 cycles on the 386 when working with a register operand. My
measurements indicate that the 9-cycle execution time almost holds true
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
**RCL DX,2**. Contrast that with **ROR** and **ROL**, which can rotate
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
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
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
cycles, just like **ROR, ROL, SHR,** and **SHL**. At least, that's how
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
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,
though!)
@ -34,12 +34,12 @@ though!)
![**Figure 9.4**  *Performing rotate instructions using the Carry flag.*](images/09-04.jpg)
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
register **RCR** or **RCL**!
register `RCR` or `RCL`!
No great lesson here, just a caution to be leery of multibit **RCR** and
**RCL** when performance matters—and to take cycle-time documentation
No great lesson here, just a caution to be leery of multibit `RCR` and
`RCL` when performance matters—and to take cycle-time documentation
with a grain of salt.
#### Hardwired Far Jumps {#Heading11}
@ -47,8 +47,8 @@ with a grain of salt.
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
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
PTR 1000:5**, but you'd be wrong. That won't even assemble. You might
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
then think to construct in memory a far pointer containing 1000:5, as in
the following:
@ -60,9 +60,9 @@ Ptr dd ?
jmp [Ptr]
```
That will work, but at a price in performance. On an 8088, **JMP DWORD
PTR [*mem*]** (an indirect far jump) takes at least 37 cycles; **JMP
DWORD PTR *label*** (a direct far jump) takes only 15 cycles (plus,
That will work, but at a price in performance. On an 8088, `JMP DWORD
PTR [*mem*]` (an indirect far jump) takes at least 37 cycles; `JMP
DWORD PTR *label*` (a direct far jump) takes only 15 cycles (plus,
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
(31 in protected mode); a direct far jump is documented to take at least
@ -81,7 +81,7 @@ preferable.
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
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
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
@ -132,8 +132,8 @@ mov eax,1
```
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).
Both code fragments are ways to set **EAX** to 1 (although the first
constants are dwords and the `MOV` instruction doesn't sign-extend).
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
of speed for space. Second,
@ -148,15 +148,15 @@ move bx,-1
```
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
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
`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
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
displacements and constants of 386 native mode change the familiar
landscape of 80x86 optimization.
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
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
386 under some circumstances, for reasons that thus far escape me. This
just reinforces the first rule of optimization: Measure your code in
action, and place not your trust in documented cycle times.

View file

@ -102,7 +102,7 @@ les ax,dword ptr [LongVar]
mov dx,es
```
which loads **LongVar** into DX:AX faster than this:
which loads `LongVar` into DX:AX faster than this:
```nasm
mov ax,word ptr [LongVar]

View file

@ -100,7 +100,7 @@ wait states hurt plenty—and the place they hurt most is instruction
fetching.
Consider this: The 286 can store an immediate value to memory, as in
**MOV [WordVar],0**, in just 3 cycles. However, that instruction is 6
`MOV [WordVar],0`, in just 3 cycles. However, that instruction is 6
bytes long. The 286 is capable of fetching 1 word every 2 cycles;
however, the one-wait-state architecture of the AT stretches that to 3
cycles. Consequently, nine cycles are needed to fetch the six
@ -117,7 +117,7 @@ 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.
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
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

View file

@ -84,15 +84,15 @@ different workaround, we'll consider it to be a new cycle-eater.)
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
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
variables. **EVEN** forces the offset of the next byte assembled to be
even by inserting a **NOP** if the current offset is odd; consequently,
to place the directive `EVEN` before each of your word-sized
variables. `EVEN` forces the offset of the next byte assembled to be
even by inserting a `NOP` if the current offset is odd; consequently,
you can ensure that any word-sized variable can be accessed efficiently
by the 286 simply by preceding it with **EVEN**.
by the 286 simply by preceding it with `EVEN`.
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
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
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
@ -123,7 +123,7 @@ Skip:
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
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
twice as fast as the non-word-aligned accesses of Listing 11.2, just as
we predicted.

View file

@ -33,7 +33,7 @@ not explain in any way. After rolling it around in my head for a while,
I took a look at the code under a debugger...and the answer leaped out
at me. *The loop began at an odd address!* That meant that two
instruction fetches were required each time through the loop; one to get
the opcode byte of the **LOOP** instruction, which resided at the end of
the opcode byte of the `LOOP` instruction, which resided at the end of
one word-aligned word, and another to get the displacement byte, which
resided at the start of the next word-aligned word.
@ -51,8 +51,8 @@ LoopTop:
While word-aligning branch destinations can improve branching
performance, it's a nuisance and can increase code size a good deal, so
it's not worth doing in most code. Besides, **EVEN** inserts a **NOP**
instruction if necessary, and the time required to execute a **NOP** can
it's not worth doing in most code. Besides, `EVEN` inserts a `NOP`
instruction if necessary, and the time required to execute a `NOP` can
sometimes cancel the performance advantage of having a word-aligned
branch destination.
@ -103,19 +103,19 @@ make the stack pointer odd by adding an odd value to it or subtracting
an odd value from it, or by loading it with an odd value.) An odd stack
pointer on the 286 or 386 (or a non-doubleword-aligned stack in 32-bit
protected mode on the 386, 486, or Pentium) will significantly reduce
the performance of **PUSH,** **POP,** **CALL**, and **RET**, as well as
**INT** and **IRET**, which are executed to invoke DOS and BIOS
the performance of `PUSH`, `POP`, `CALL`, and `RET`, as well as
`INT` and `IRET`, which are executed to invoke DOS and BIOS
functions, handle keystrokes and incoming serial characters, and manage
the mouse. I know of a Forth programmer who vastly improved the
performance of a complex application on the AT simply by forcing the
Forth interpreter to maintain an even stack pointer at all times.
An interesting corollary to this rule is that you shouldn't **INC SP**
twice to add 2, even though that takes fewer bytes than **ADD SP,2**.
The stack pointer is odd between the first and second **INC**, so any
An interesting corollary to this rule is that you shouldn't `INC SP`
twice to add 2, even though that takes fewer bytes than `ADD SP,2`.
The stack pointer is odd between the first and second `INC`, so any
interrupt occurring between the two instructions will be serviced more
slowly than it normally would. The same goes for decrementing twice; use
**SUB SP,2** instead.
`SUB SP,2` instead.
> ![](images/i.jpg)
> Keep the stack pointer aligned at all times.
@ -163,7 +163,7 @@ computers are actually the same. As you'll recall from my earlier
discussion of the matter in Chapter 4, display adapters offer only a
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
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
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

View file

@ -12,7 +12,7 @@ pages: 220-222
Under ideal conditions, a 286 can access memory much, much faster than
an 8088. A 10 MHz 286 is capable of accessing a word of system memory
every 0.20 ms with **REP MOVSW**, dwarfing the 1 byte every 1.31 µs that
every 0.20 ms with `REP MOVSW`, dwarfing the 1 byte every 1.31 µs that
the 8088 in a PC can manage. However, access to display memory is
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
@ -29,7 +29,7 @@ factors that reduce the speed with which the 286 can access display
memory. Far more cycles are eaten by the inherent memory-access
limitations of display adapters—that is, the limited number of display
memory accesses that display adapters make available to the 286. Look at
it this way: If **REP MOVSW** on a PC can use more than half of all
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
running on a 286 or 386 possibly run when accessing display memory?
@ -97,17 +97,17 @@ programs are still limited to 1 MB of addressable memory on the 286. In
either mode, each segment is still limited to 64K.)
There are also a handful of 286-specific real-mode instructions, and
they can be quite useful. **BOUND** checks array bounds. **ENTER** and
**LEAVE** support compact and speedy stack frame construction and
they can be quite useful. `BOUND` checks array bounds. `ENTER` and
`LEAVE` support compact and speedy stack frame construction and
removal, ideal for interfacing to high-level languages such as C and
Pascal (although these instructions are actually relatively slow on the
386 and its successors, and should be used with caution when performance
matters). **INS** and **OUTS** are new string instructions that support
efficient data transfer between memory and I/O ports. Finally, **PUSHA**
and **POPA** push and pop all eight general-purpose registers.
matters). `INS` and `OUTS` are new string instructions that support
efficient data transfer between memory and I/O ports. Finally, `PUSHA`
and `POPA` push and pop all eight general-purpose registers.
A couple of old instructions gain new features on the 286. For one, the
286 version of **PUSH** is capable of pushing a constant on the stack.
286 version of `PUSH` is capable of pushing a constant on the stack.
For another, the 286 allows all shifts and rotates to be performed for
not just 1 bit or the number of bits specified by CL, but for *any*
constant number of bits.
@ -143,5 +143,5 @@ complex 386-specific instructions are slower than equivalent sequences
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
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
double") instruction, or to negate a 32-bit value with **NEG eax**.
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`.

View file

@ -56,7 +56,7 @@ around the 286 and 386, many of the instruction-specific optimizations
no longer hold, for the execution times of most instructions are quite
different on the 286 and 386 than on the 8088. We have already seen one
such example of the sometimes vast difference between 8088 and 286/386
instruction execution times: **MOV [WordVar],0**, which has an Execution
instruction execution times: `MOV [WordVar],0`, which has an Execution
Unit execution time of 20 cycles on the 8088, has an EU execution time
of just 3 cycles on the 286 and 2 cycles on the 386.
@ -65,17 +65,17 @@ has been improved enormously on the 286 and 386. The key to this
improvement is the near elimination of effective address (EA)
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
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
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
perform the EA calculation, but that's both the worst case and the only
case in which there's any EA overhead at all.
The elimination of EA calculation time means that the EU execution time
of memory-addressing instructions is much closer to the EU execution
time of register-only instructions. For instance, on the 8088 **ADD
[WordVar],100H** is a 31-cycle instruction, while **ADD DX,100H** is a
time of register-only instructions. For instance, on the 8088 `ADD
[WordVar],100H` is a 31-cycle instruction, while `ADD DX,100H` is a
4-cycle instruction—a ratio of nearly 8 to 1. By contrast, on the 286
**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.
It would seem, then, that it's less necessary to use the registers on
@ -84,12 +84,12 @@ reasons we've already seen. The key is this: The 286 can execute
memory-addressing instructions so fast that there's no spare instruction
prefetching time during those instructions, so the prefetch queue runs
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
15-cycle instruction, because 3 cycles are needed to fetch each of the
three instruction words and 6 more cycles are needed to read **WordVar**
three instruction words and 6 more cycles are needed to read `WordVar`
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
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
@ -106,7 +106,7 @@ in 10.05 ms. On a 10 MHz AT clone, Listing 11.4 runs in 0.64 ms, while
Listing 11.5 runs in 1.80 ms. Obviously, the AT is much faster...but the
ratio of Listing 11.5 to Listing 11.4 is virtually identical on both
computers, at 2.78 for the PC and 2.81 for the AT. If anything, the
register-only form of **ADD** has a slightly *larger* advantage on the
register-only form of `ADD` has a slightly *larger* advantage on the
AT than it does on the PC in this case.
Theory confirmed.

View file

@ -71,9 +71,9 @@ The more things change, the more they remain the same....
#### POPF and the 286 {#Heading17}
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
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
@ -85,29 +85,29 @@ absolutely disabled, with no chance of an interrupt sneaking through.
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
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
disk BIOS is reading a sector of the File Allocation Table.
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
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
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
view those extra cycles and bytes as cheap insurance against mysterious
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
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
functionality of the **POPF** instruction without using **POPF**, and
the place to start is by asking exactly what **POPF** does.
functionality of the `POPF` instruction without using `POPF`, and
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
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
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`?
Of course, the 286's designers intended us to use `POPF` for this
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 search for instructions that contain some of the same
functionality as **POPF**, in the hope that one of those instructions
can be used in some way to replace **POPF**.
functionality as `POPF`, in the hope that one of those instructions
can be used in some way to replace `POPF`.

View file

@ -10,26 +10,26 @@ chapter: '11'
pages: 226-231
---
Well, there's only one instruction other than **POPF** that loads the
FLAGS register directly from the stack, and that's **IRET**, which loads
Well, there's only one instruction other than `POPF` that loads the
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
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.
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
**POPF**; the far return address that **IRET** pops into CS:IP lies
`POPF`; the far return address that `IRET` pops into CS:IP lies
between the top of the stack and the word popped into the FLAGS
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
**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
`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
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
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
after the pop.
@ -38,7 +38,7 @@ after the pop.
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
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
pushed on the stack will point to the instruction we want to continue
with. The code works out like this:
@ -66,7 +66,7 @@ popfskip:
The operation of this code is illustrated in Figure 11.6.
The **POPF** workaround can best be implemented as a macro; we can also
The `POPF` workaround can best be implemented as a macro; we can also
emulate a far call by pushing CS and performing a near call, thereby
shrinking the workaround code by 1 byte:
@ -84,7 +84,7 @@ popfskip:
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
emulates **POPF** with just one branch, but wipes out AX:
emulates `POPF` with just one branch, but wipes out AX:
```nasm
EMULATE_POPF_TRASH_AX macro
@ -95,11 +95,11 @@ EMULATE_POPF_TRASH_AX macro
endm
```
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
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
you can spare the register. If you're using 286-specific instructions,
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.)
```nasm
@ -114,15 +114,15 @@ EMULATE_POPFmacro
![**Figure 11.6**  *Workaround code for the POPF bug.*](images/11-06.jpg)
The standard version of **EMULATE\_POPF** is 6 bytes longer than
**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
The standard version of `EMULATE_POPF` is 6 bytes longer than
`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
larger, slower, three-branch macro—given a choice. In noncode, however,
there's no choice here; the safer—if slower—approach is the best.
(Having people associate your programs with crashed computers is *not* a
desirable situation, no matter how unfair the circumstances under which
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
example of the tremendous flexibility of the x86 instruction set.

View file

@ -40,8 +40,8 @@ Substitute "processor" for the various digging implements, and you get
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
encountering a processor where string instructions are often to be
avoided and memory-to-register **MOV**s are frequently as fast as
register-to-register **MOV**s, Dorothy was heard to exclaim (before she
avoided and memory-to-register `MOV`s are frequently as fast as
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
think we're in Kansas anymore, Toto."
@ -53,15 +53,15 @@ RISC elements, and it's those elements that are most responsible for
making 486 optimization unique. Simple, common instructions are executed
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
instruction takes at least 2 cycles. For example, **MOV AL, [TestChar]**
instruction takes at least 2 cycles. For example, `MOV AL, [TestChar]`
takes only 1 cycle on the 486, assuming both instruction and data are in
the cache—3 cycles faster than the 386—but **STOSB** takes 5 cycles, 1
the cache—3 cycles faster than the 386—but `STOSB` takes 5 cycles, 1
cycle *slower* than on the 386. The floating-point execution unit inside
the 486 is also much faster than the 387 math coprocessor, largely
because, being in the same silicon as the CPU (the 486 has a math
coprocessor built in), it is more tightly coupled. The results are
sometimes startling: **FMUL** (floating point multiply) is usually
faster on the 486 than **IMUL** (integer multiply)!
sometimes startling: `FMUL` (floating point multiply) is usually
faster on the 486 than `IMUL` (integer multiply)!
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
@ -116,9 +116,9 @@ CPUs. That is not the sense in which "indexed addressing" is meant here,
however. In real mode, indexed addressing means that two registers,
rather than one or none, are used to point to memory. (In this context,
the use of one register to address memory is "base addressing," no
matter what register is used.) **MOV AX, [BX+DI]** and **MOV CL,
[BP+SI+10]** perform indexed addressing; **MOV AX,[BX]** and **MOV DL,
[SI+1]** do not.
matter what register is used.) `MOV AX, [BX+DI]` and `MOV CL,
[BP+SI+10]` perform indexed addressing; `MOV AX,[BX]` and `MOV DL,
[SI+1]` do not.
> ![](images/i.jpg)
> Therefore, in real mode, the rule is to avoid using two registers to

View file

@ -14,9 +14,9 @@ which calculates the same sum and leaves the registers in the same state
as the first example, but avoids indexed addressing.
In protected mode, the definition of indexed addressing is a tad more
complex. The use of two registers to address memory, as in **MOV EAX,
[EDX+EDI]**, still qualifies for the one-cycle penalty. In addition, the
use of 386/486 scaled addressing, as in **MOV [ECX\*2],EAX**, also
complex. The use of two registers to address memory, as in `MOV EAX,
[EDX+EDI]`, still qualifies for the one-cycle penalty. In addition, the
use of 386/486 scaled addressing, as in `MOV [ECX*2],EAX`, also
constitutes indexed addressing, even if only one register is used to
point to memory.
@ -24,8 +24,8 @@ All this fuss over one cycle! You might well wonder how much difference
one cycle could make. After all, on the 8088, effective address
calculations take a *minimum* of 5 cycles. On the 486, however, 1 cycle
is a big deal because many instructions, including most register-only
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
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
they're not hampered by something like indexed addressing, in which case
they slow to half speed (or worse, as we will see shortly).
@ -64,7 +64,7 @@ MOV AX,[BX]
```
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
rearranging your code so that at least one instruction lies between the
loading of the memory pointer and its use. For example,
@ -102,7 +102,7 @@ it's used.
In 32-bit protected mode, however, the penalty is, in fact, the 1 cycle
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
cycles faster than the preincrement loop, resulting in a 29 percent
improvement in the performance of the entire loop. But wait, there's

View file

@ -26,14 +26,14 @@ Rule \#2A: Rule \#2 sometimes, but not always, applies to the stack
pointer when it is implicitly used to point to memory.
Intel states that the stack pointer is an implied destination register
for **CALL**, **ENTER**, **LEAVE**, **RET**, **PUSH**, and **POP**
for `CALL`, `ENTER`, `LEAVE`, `RET`, `PUSH`, and `POP`
(which alter (E)SP), and that it is the implied base addressing register
for **PUSH**, **POP**, and **RET** (which use (E)SP to address memory).
for `PUSH`, `POP`, and `RET` (which use (E)SP to address memory).
Intel then implies that the aforementioned addressing pipeline penalty
is incurred whenever the stack pointer is used as a destination by one
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
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.
Fortunately, matters are actually not so grim as Intel's documentation
would indicate; my tests indicate that the addressing pipeline penalty
@ -52,7 +52,7 @@ et
to exhibit the addressing pipeline interruption phenomenon (SP is both
destination and addressing register for both instructions, according to
Intel), but this code runs in six cycles per **POP/RET** pair, matching
Intel), but this code runs in six cycles per `POP/RET` pair, matching
the official execution times exactly. Likewise, a sequence like
```nasm
@ -66,7 +66,7 @@ runs in one cycle per instruction, just as it should.
On the other hand, performing arithmetic directly on SP as an *explicit*
destination—for example, to deallocate local variables—and then using
**PUSH**, **POP**, or **RET**, definitely can interrupt the addressing
`PUSH`, `POP`, or `RET`, definitely can interrupt the addressing
pipeline. For example
```nasm
@ -89,12 +89,12 @@ I certainly haven't tried all possible combinations, but the results so
far indicate that the stack pointer incurs the addressing pipeline
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
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
**RET**. Happily, then, we need not worry about the sequence in which we
use **PUSH** and **POP**. However, adding to, moving to, or subtracting
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
`RET`. Happily, then, we need not worry about the sequence in which we
use `PUSH` and `POP`. However, adding to, moving to, or subtracting
from the stack pointer should ideally be done at least two cycles before
**PUSH**, **POP**, **RET**, or any other instruction that uses the stack
`PUSH`, `POP`, `RET`, or any other instruction that uses the stack
pointer to address memory.
#### Problems with Byte Registers {#Heading9}
@ -144,7 +144,7 @@ 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
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
```nasm
@ -154,20 +154,20 @@ mov al,[si]
xlat
```
even though AL must be converted to a word by **XLAT** before it can be
even though AL must be converted to a word by `XLAT` before it can be
added to BX and used to address memory. In fact, none of the penalties
mentioned in this chapter apply to **XLAT**, apparently because **XLAT**
mentioned in this chapter apply to `XLAT`, apparently because `XLAT`
is so slow—4 cycles—that it gives the 486 time to perform addressing
calculations during the course of the instruction.
> ![](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.
> While it's nice that `XLAT` doesn't suffer from the various 486
> addressing penalties, the reason for that is basically that `XLAT` is
> slow, so there's still no compelling reason to use `XLAT` on the 486.
In general, penalties for interrupting the 486's pipeline apply
primarily to the fast core instructions of the 486, most notably
register-only instructions and **MOV**, although arithmetic and logical
register-only instructions and `MOV`, although arithmetic and logical
operations that access memory are also often affected. I don't know all
the performance dependencies, and I don't plan to; figuring all of them
out would be a big, boring job of little value. Basically, on the 486

View file

@ -76,9 +76,9 @@ in this case—one cycle before using a register to address memory
produces no penalty; loading 2 cycles ahead is the only case that
normally incurs a penalty. However, think of Rule \#4 as meaning that
loading a byte register disrupts the memory addressing pipeline as it
starts up. Viewed that way, we can see that **MOV BX,OFFSET MemVar**
starts up. Viewed that way, we can see that `MOV BX,OFFSET MemVar`
interrupts the addressing pipeline, forcing it to start again, and then,
presumably, **MOV CL,AL** interrupts the pipeline again because the
presumably, `MOV CL,AL` interrupts the pipeline again because the
pipeline is now on its first cycle: the one that loading a byte register
can affect.
@ -150,7 +150,7 @@ Note that Listings 12.1 and 12.2 each repeat the timing of the code
under test a second time, to make sure that the instructions are in the
cache on the second pass, the one for which results are displayed. Also
note that the code is less than 8K in size, so that it can all fit in
the 486's 8K internal cache. If I double the **REPT** value in Listing
the 486's 8K internal cache. If I double the `REPT` value in Listing
12.2 to 2,000, making the test code larger than 8K, the execution time
more than doubles to 224 µs, or 3.7 cycles per repetition; the extra
seven-tenths of a cycle comes from fetching non-cached instruction

View file

@ -11,11 +11,11 @@ pages: 252-254
---
"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
written as shown in Listing 13.3. The final breakthrough came when I
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
the first and the fourth line, avoiding all pipeline stalls, for a total
throughput of two cycles/char."
@ -52,12 +52,12 @@ Clever 486 optimization can pay off big. QED.
### BSWAP: More Useful Than You Might Think {#Heading4}
There are only 3 non-system instructions unique to the 486. None is
earthshaking, but they have their uses. Consider **BSWAP. BSWAP** does
earthshaking, but they have their uses. Consider `BSWAP`. `BSWAP` does
just what its name implies, swapping the bytes (not bits) of a 32-bit
register from one end of the register to the other, as shown in Figure
13.2. (**BSWAP** can only work with 32-bit registers; memory locations
13.2. (`BSWAP` can only work with 32-bit registers; memory locations
and 16-bit registers are not valid operands.) The obvious use of
**BSWAP** is to convert data from Intel format (least significant byte
`BSWAP` is to convert data from Intel format (least significant byte
first in memory, also called *little endian*) to Motorola format (most
significant byte first in memory, or *big endian*), like so:
@ -67,16 +67,16 @@ bswap
stosd
```
**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
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,
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.
![**Figure 13.2**  *BSWAP in operation.*](images/13-02.jpg)
As it turns out, though, **BSWAP** is also useful in an unexpected way,
As it turns out, though, `BSWAP` is also useful in an unexpected way,
having to do with making efficient use of the upper half of 32-bit
registers. As any assembly language programmer knows, the x86 register
set is too small; or, to phrase that another way, it sure would be nice
@ -118,8 +118,8 @@ Not necessarily. Shifts and rotates are among the worst performing
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
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
or from memory takes only one cycle. Basically, using **ROR** to access
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
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
all (for example, if you're using BP as a working register, temporarily

View file

@ -10,11 +10,11 @@ chapter: '13'
pages: 254-256
---
On the 386, **ROR** was the only way to split a 32-bit register into two
16-bit registers. On the 486, however, **BSWAP** can not only do the
job, but can do it better, because **BSWAP** executes in just one cycle.
**BSWAP** has the added benefit of not affecting any flags, unlike
**ROR**. With **BSWAP**-based code like that in Listing 13.6, the upper
On the 386, `ROR` was the only way to split a 32-bit register into two
16-bit registers. On the 486, however, `BSWAP` can not only do the
job, but can do it better, because `BSWAP` executes in just one cycle.
`BSWAP` has the added benefit of not affecting any flags, unlike
`ROR`. With `BSWAP`-based code like that in Listing 13.6, the upper
16 bits of a register can be accessed with only 2 cycles of overhead and
without altering any flags, making the technique of packing two 16-bit
registers into one 32-bit register much more useful.
@ -37,8 +37,8 @@ looptop:
### Pushing and Popping Memory {#Heading5}
Pushing or popping a memory location, as in **PUSH WORD PTR [BX]** or
**POP [MemVar]**, is a compact, easy way to get a value onto or off of
Pushing or popping a memory location, as in `PUSH WORD PTR [BX]` or
`POP [MemVar]`, is a compact, easy way to get a value onto or off of
the stack, especially when pushing parameters for calling a C-compatible
function. However, on a 486, these are unattractive instructions from a
performance perspective. Pushing a memory location takes four cycles; by
@ -70,23 +70,23 @@ is so slow? The rule on the 486 is that simple operations, which can be
executed in a single cycle by the 486's RISC core, are fast; whereas
complex operations, which must be carried out in microcode just as they
were on the 386, are almost all relatively slow. Slow, complex
operations include all the string instructions except **REP MOVS,** as
well as **XLAT, LOOP,** and, of course, **PUSH *mem*** and **POP
*mem.***
operations include all the string instructions except `REP MOVS`, as
well as `XLAT`, `LOOP`, and, of course, `PUSH *mem*` and `POP
*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.
> `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}
On a 486, the n-bit forms of the shift and rotate instructions—as in
**ROR AX,2** and **SHL BX,9**—are 2-cycle instructions, but the 1-bit
forms—as in **ROR AX,1** and **SHL BX,1—**are *3-cycle* instructions. Go
`ROR AX,2` and `SHL BX,9`—are 2-cycle instructions, but the 1-bit
forms—as in `ROR AX,1` and `SHL BX,1`-are *3-cycle* instructions. Go
figure.
Assemblers default to the 1-bit instruction for 1-bit shifts and
@ -96,12 +96,12 @@ 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
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
**SHL AX,2,** then look at the hex codes that the assembler generates
and use **DB** to insert them in your program code, with the value two
replaced with the value one. For example, you could determine that **SHL
AX,2** assembles to the bytes 0C1H 0E0H 002H, either by looking at the
`SHL AX,2`, then look at the hex codes that the assembler generates
and use `DB` to insert them in your program code, with the value two
replaced with the value one. For example, you could determine that `SHL
AX,2` assembles to the bytes 0C1H 0E0H 002H, either by looking at the
disassembly in a debugger or by having the assembler generate a listing
file. You could then insert the n-bit version of **SHL AX,1** in your
file. You could then insert the n-bit version of `SHL AX,1` in your
code as follows:
```nasm
@ -111,12 +111,12 @@ mov dx,ax
```
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
your code.
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
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

View file

@ -23,7 +23,7 @@ mov al,BaseTable[ecx+edx*4]
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
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
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
@ -49,8 +49,8 @@ aforementioned 1-cycle penalty for using 32-bit addressing in real mode)
is a 1-cycle penalty imposed for using an index register. In this
context, you use an index register when you use a register that's
scaled, or when you use the sum of two registers to point to memory.
**MOV BL,[EBX\*2]** uses an index register and takes an extra cycle, as
does **MOV CL,[EAX+EDX]; MOV CL,[EAX+100H]** is not indexed, however.
`MOV BL,[EBX*2]` uses an index register and takes an extra cycle, as
does `MOV CL,[EAX+EDX]; MOV CL,[EAX+100H]` is not indexed, however.
The other cost of 32-bit addressing is in instruction size. Old-style
16-bit addressing usually (except in a few special cases) uses one extra
@ -61,8 +61,8 @@ constant value to add into the address. In many cases, 32-bit addressing
continues to use the Mod-R/M byte, albeit with a different
interpretation; in these cases, 32-bit addressing is no larger than
16-bit addressing, except when a 32-bit displacement is involved. For
example, **MOV AL, [EBX]** is a 2-byte instruction; **MOV AL,
[EBX+10H]** is a 3-byte instruction; and **MOV AL, [EBX+10000H]** is a
example, `MOV AL, [EBX]` is a 2-byte instruction; `MOV AL,
[EBX+10H]` is a 3-byte instruction; and `MOV AL, [EBX+10000H]` is a
6-byte instruction.
> ![](images/i.jpg)

View file

@ -71,7 +71,7 @@ information, you may want to refer to the discussion of string searching
in the excellent *Algorithms in C,* by Robert Sedgewick
(Addison-Wesley), which served as the primary reference for this
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.)
String searching is the simple matter of finding the first occurrence of
@ -80,7 +80,7 @@ bytes (the buffer). The obvious, brute-force approach is to try every
possible match location, starting at the beginning of the buffer and
advancing one position after each mismatch, until either a match is
found or the buffer is exhausted. There's even a nifty string
instruction, **REPZ CMPS,** that's perfect for comparing the pattern to
instruction, `REPZ CMPS`, that's perfect for comparing the pattern to
the contents of the buffer at each location. What could be simpler?
We have some important information that we're not yet using, though.
@ -91,10 +91,10 @@ usually be even, neither will any one character constitute half the
buffer, or anything close. A reasonable conclusion is that the first
character of the pattern will rarely match the first character of the
buffer location currently being checked. This allows us to use the
speedy **REPNZ SCASB** to whiz through the buffer, eliminating most
potential match locations with single repetitions of **SCASB.** Only
speedy `REPNZ SCASB` to whiz through the buffer, eliminating most
potential match locations with single repetitions of `SCASB`. Only
when that first character does (infrequently) match must we drop back to
the slower **REPZ CMPS** approach.
the slower `REPZ CMPS` approach.
It's important to understand that we're assuming that the buffer is
typical text. That's what I meant at the outset, when I said that the
@ -106,26 +106,26 @@ information you need may be under your nose.
> a great deal of useful, if somewhat imprecise, information.
If the buffer contains the letter A' repeated 1,000 times, followed by
the letter B,' then the **REPNZ SCASB/REPZ CMPS** approach will be much
slower than the brute-force **REPZ CMPS** approach when searching for
the pattern "AB," because **REPNZ SCASB** would match at every buffer
the letter B,' then the `REPNZ SCASB/REPZ CMPS` approach will be much
slower than the brute-force `REPZ CMPS` approach when searching for
the pattern "AB," because `REPNZ SCASB` would match at every buffer
location. You could construct a horrendous worst-case scenario for
almost any good optimization; the key is understanding the usual
conditions under which your code will work.
As discussed in Chapter 9, we also know that certain characters have
lower probabilities of matching than others. In a normal buffer, T'
will match far more often than X.' Therefore, if we use **REPNZ SCASB**
will match far more often than X.' Therefore, if we use `REPNZ SCASB`
to scan for the least common letter in the search string, rather than
the first letter, we'll greatly decrease the number of times we have to
drop back to **REPZ CMPS,** and the search time will become very close
to the time it takes **REPNZ SCASB** to go from the start of the buffer
drop back to `REPZ CMPS`, and the search time will become very close
to the time it takes `REPNZ SCASB` to go from the start of the buffer
to the match location. If the distance to the first match is N bytes,
the least-common **REPNZ SCASB** approach will take about as long as N
repetitions of **REPNZ SCASB.**
the least-common `REPNZ SCASB` approach will take about as long as N
repetitions of `REPNZ SCASB`.
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
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
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
matches. Can we?

View file

@ -48,7 +48,7 @@ character, as often happens, we can only skip ahead 1 byte, as usual.
Look at it differently, though: What if we compare the pattern starting
with the last (rightmost) byte, rather than the first (leftmost) byte?
In other words, what if we compare from high memory toward low, in the
direction in which string instructions go after the **STD** instruction?
direction in which string instructions go after the `STD` instruction?
After all, we're comparing one set of bytes (the pattern) to another set
of bytes (a portion of the buffer); it doesn't matter in the least in
what order we compare them, so long as all the bytes in one set are
@ -70,7 +70,7 @@ in the pattern; that's how many locations there are in the buffer that
*might* have matched, but have just been shown not to, because they
overlap the mismatched character that doesn't belong in the pattern. In
this case, we can skip ahead by the full pattern length in the buffer!
This is how we can outperform even **REPNZ SCASB; REPNZ SCASB** has to
This is how we can outperform even `REPNZ SCASB; REPNZ SCASB` has to
check every byte in the buffer, but Boyer-Moore doesn't.
Figure 14.1 illustrates the operation of a Boyer-Moore search when the

View file

@ -15,13 +15,13 @@ Boyer-Moore searching; Listing 14.2 is a test-bed program that searches
up to the first 32K of a file for a pattern. Table 14.1 (all times
measured with Turbo Profiler on a 20 MHz cached 386, searching a
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
designed primarily to make later assembly implemenmore comprehensible,
rather than faster; Sedge's implementation uses arrays rather than
pointers, is a great deal more compact and very clever, and may be
somewhat faster. Regardless, the far superior performance of **REPNZ
SCASB** clearly indicates that assembly language is in order at this
somewhat faster. Regardless, the far superior performance of `REPNZ
SCASB` clearly indicates that assembly language is in order at this
point.
| | "g;" | "Yogi" | "igoY" | "Adrian" | "Conclusion" | "You don't know what you know" |
@ -43,7 +43,7 @@ parentheses).\
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
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
inferior on the x86, due to two architectural quirks: the string
instructions and slow branch. I had even coined a neat phrase for it:

View file

@ -162,16 +162,16 @@ void main() {
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
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
approach of **REPZ CMPSB** at every location had been based on my
The path leading to `REPNZ SCASB` from the original brute-force
approach of `REPZ CMPSB` at every location had been based on my
observation that the first character comparison at each buffer location
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
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
Table 14.1 speak for themselves; Listing 14.3 is more than twice as fast
as what I assure you was already a nice, tight assembly implementation
(and unrolling **QuickSearchLoop** could boost performance by up to 10
percent more). Listing 14.3 is also *four times* faster than **REPNZ
SCASB** in one case.
(and unrolling `QuickSearchLoop` could boost performance by up to 10
percent more). Listing 14.3 is also *four times* faster than `REPNZ
SCASB` in one case.

View file

@ -14,7 +14,7 @@ Table 14.1 represents a limited and decidedly unscientific comparison of
searching techniques. Nonetheless, the overall trend is clear: For all
but the shortest patterns, well-implemented Boyer-Moore is generally as
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.)
Know your data and use your smarts. Don't stop thinking just because
@ -25,8 +25,8 @@ you're implementing a big-name algorithm; you know more than it does.
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
searched-for pattern to a maximum of 255 bytes allows us to use the
**XLAT** instruction and generally tighten the critical loop. (Be aware,
however, that **XLAT** is a relatively expensive instruction on the 486
`XLAT` instruction and generally tighten the critical loop. (Be aware,
however, that `XLAT` is a relatively expensive instruction on the 486
and Pentium.) Putting a copy of the searched-for string at the end of
the search buffer as a sentinel, so that the search never fails, frees
us from counting down the buffer length, and makes it easy to unroll the
@ -185,7 +185,7 @@ _FindString endp
```
Note that Table 14.1 includes the time required to build the skip table
each time **FindString** is called. This time could be eliminated for
each time `FindString` is called. This time could be eliminated for
all but the first search when repeatedly searching for a particular
pattern, by building the skip table externally and passing a pointer to
it as a parameter.

View file

@ -126,23 +126,23 @@ The basic concept of a linked list—the one I came up with for that *DDJ*
column—is straightforward, as shown in Figure 15.1. A head pointer
points to the first node in the list, which points to the next node,
which points to the next, and so on, until the last node in the list is
reached (typically denoted by a **NULL** next-node pointer).
reached (typically denoted by a `NULL` next-node pointer).
Conceptually, nothing could be simpler. From an implementation
perspective, however, there are serious flaws with this model.
The fundamental problem is that the model of Figure 15.1 unnecessarily
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
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
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
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.
(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
point to the head pointer and pretend it was a **LinkNode**
in the `LinkNode` structure, at offset 0, then you could successfully
point to the head pointer and pretend it was a `LinkNode`
structure—but that's an ugly and potentially dangerous trick, and we'll
see a better approach next.)

View file

@ -72,7 +72,7 @@ struct LinkNode *DeleteNodeAfter(struct LinkNode **HeadOfListPtr,
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
**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
structure types by casting them to the base link-node structure type.
This is a less than elegant approach, but it works. C++ can handle data
@ -80,7 +80,7 @@ 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
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
to backtrack in a singly linked list. For this reason, singly-linked
list operations tend to work with the structure preceding the one of
@ -112,10 +112,10 @@ Likewise, there should be a separate node for the tail of the list, so
that every node that contains real data is guaranteed to have a node on
either side of it. In this scheme, an empty list contains two nodes, as
shown in Figure 15.3. Although it is not necessary, the tail node may
point to itself as its own next node, rather than contain a **NULL**
point to itself as its own next node, rather than contain a `NULL`
pointer. This way, a deletion operation on an empty list will have no
effect—quite unlike the same operation performed on a list terminated
with a **NULL** pointer. The tail node of a list terminated like this
with a `NULL` pointer. The tail node of a list terminated like this
can be detected because it will be the only node for which the next-node
pointer equals the current-node pointer.

View file

@ -65,9 +65,9 @@ there. Look it up *before* turning on your optimizer afterburners!
Listings 15.1 and 15.6 together form a suite of C functions for
maintaining a circular linked list sorted by ascending value. (Listing
15.5 requires modification before it will work with circular lists.)
Listing 15.7 is an assembly language version of **InsertNodeSorted()**;
Listing 15.7 is an assembly language version of `InsertNodeSorted()`;
note the tremendous efficiency of the scanning loop in
**InsertNodeSorted()—**four instructions per node!—thanks to the dummy
`InsertNodeSorted()`-four instructions per node!—thanks to the dummy
head/tail/sentinel node. Listing 15.8 is a simple application that
illustrates the use of the linked-list functions in Listings 15.1 and
15.6.

View file

@ -164,7 +164,7 @@ void main()
In one of my *PC TECHNIQUES* "Pushing the Envelope" columns, I passed
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
the challenge was to do it in 24 bytes or less.
@ -172,8 +172,8 @@ 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
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,
"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
"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
in 24 bytes or less—*combined*."
Oh.

View file

@ -106,16 +106,16 @@ _ScanBuffer endp
```
Listing 16.4 features several interesting tricks. First, it uses
**LODSB** and **XLAT** in succession, a very neat way to get a
`LODSB` and `XLAT` in succession, a very neat way to get a
pointed-to byte, advance the pointer, and look up the value indexed by
the byte in a table, all with just two instruction bytes.
(Interestingly, Listing 16.4 would probably run quite a bit better still
on an 8088, where **LODSB** and **XLAT** have a greater advantage over
conventional instructions. On the 486 and Pentium, however, **LODSB**
and **XLAT** lose much of their appeal, and should be replaced with
**MOV** instructions.) Better yet, **LODSB** and **XLAT** don't alter
the flags, so the Zero flag status set before **LODSB** is still around
to be tested after **XLAT** .
on an 8088, where `LODSB` and `XLAT` have a greater advantage over
conventional instructions. On the 486 and Pentium, however, `LODSB`
and `XLAT` lose much of their appeal, and should be replaced with
`MOV` instructions.) Better yet, `LODSB` and `XLAT` don't alter
the flags, so the Zero flag status set before `LODSB` is still around
to be tested after `XLAT` .
Finally, if you look closely, you will see that Listing 16.4 jumps out
of the loop to increment the word count in the case where a word is

View file

@ -47,18 +47,18 @@ CMP DH,[EBX+EAX]
Harmless enough, save for two things. First, EBX happened to be zero at
this point (a leftover from an earlier version of the code, as it turned
out), so it was superfluous as a memory-addressing component; this made
it possible to use base-only addressing (**[EAX]**) rather than
base+index addressing (**[EBX+EAX]**), which saves a cycle on the 386.
Second: Changing the instruction to **CMP [EAX],DH** saved 2 cycles—just
it possible to use base-only addressing (`[EAX]`) rather than
base+index addressing (`[EBX+EAX]`), which saves a cycle on the 386.
Second: Changing the instruction to `CMP [EAX],DH` saved 2 cycles—just
enough, by good fortune, to speed up the whole program by 5 percent.
> ![](images/i.jpg)
> **CMP reg,[mem]** takes 6 cycles on the 386, but **CMP [ mem ],reg**
> takes only 5 cycles; you should always perform**CMP** with the memory
> `CMP reg,[mem]` takes 6 cycles on the 386, but `CMP [ mem ],reg`
> takes only 5 cycles; you should always perform`CMP` with the memory
> operand on the left on the 386.
(Granted, **CMP [*mem*],*reg*** is 1 cycle slower than **CMP
*reg*,[*mem*]** on the 286, and they're both the same on the 8088; in
(Granted, `CMP [*mem*],*reg*` is 1 cycle slower than `CMP
*reg*,[*mem*]` on the 286, and they're both the same on the 8088; in
this case, though, the code was specific to the 386. In case you're
curious, both forms take 2 cycles on the 486; quite a lot faster, eh?)
@ -69,7 +69,7 @@ no longer be valid from the 8088/286 world into the wonderful new world
of 386 native-mode programming. The second lesson is that after you've
slaved over your code for a while, you're in no shape to see its flaws,
or to be able to get the new perspectives needed to speed it up. I'll
bet Terje looked at that **[EBX+EAX]** addressing a hundred times while
bet Terje looked at that `[EBX+EAX]` addressing a hundred times while
trying to speed up his code, but he didn't really see what it did;
instead, he saw what it was supposed to do. Mental shortcuts like this
are what enable us to deal with the complexities of assembly language

View file

@ -13,7 +13,7 @@ pages: 308-311
Table 16.2 has two times for each entry listed: the first value is the
overall counting time, including time spent in the main program, disk
I/O, and everything else; the second value is the time actually spent
counting words, the time spent in **ScanBuffer** . The first value is
counting words, the time spent in `ScanBuffer` . The first value is
the time perceived by the user, but the second value best reflects the
quality of the optimization in each entry, since the rest of the overall
execution time is fixed.

View file

@ -35,20 +35,20 @@ the instruction set. The basic framework is still the same as my code
(which in turn is basically the same as that of the original C code),
but that framework is implemented more efficiently.
One obvious level 1 optimization is using a **word** rather than
**dword** counter. **ScanBuffer** can never be called upon to handle
One obvious level 1 optimization is using a `word` rather than
`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
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
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
only upon exiting the function. Another useful optimization is aligning
loop tops and other branch destinations to **word** , or better yet
**dword** , boundaries.
loop tops and other branch destinations to `word` , or better yet
`dword` , boundaries.
Eliminating branches was very popular, as it should be on x86
processors. Branches were eliminated in a remarkable variety of ways.
Many of you unrolled the loop, a technique that does pay off nicely. A
word of caution: Some of you unrolled the loop by simply stacking
repetitions of the inner loop one after the other, with **DEC CX/JZ**
repetitions of the inner loop one after the other, with `DEC CX/JZ`
appearing after each repetition to detect the end of the buffer. Part of
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
@ -60,9 +60,9 @@ fraction of the number of unrolled repetitions is required to make the
whole thing come out right. Listing 16.5 (QSCAN3.ASM) illustrates this
technique.
Another effective optimization is the use of **LODSW** rather than
**LODSB** , thereby processing two bytes per memory access. This has the
effect of unrolling the loop one time, since with **LODSW** , looping is
Another effective optimization is the use of `LODSW` rather than
`LODSB` , thereby processing two bytes per memory access. This has the
effect of unrolling the loop one time, since with `LODSW` , looping is
performed at most only once every two bytes.
Cutting down the branches used to loop is only part of the branching
@ -80,11 +80,11 @@ code.
Listing 16.6, contributed by Willem Clements, of Granada, Spain,
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
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
AX,0** , and **ALIGN 2** could be used. Nonetheless, this is good code,
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,
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
hand-optimization might accomplish significantly improved performance at

View file

@ -127,8 +127,8 @@ exactly that. They stepped back, thought about what the code actually
needed to do, rather than just improving how it already worked, and
implemented code that sprang from that new perspective.
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
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
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
break with the serial nature of the C code, a recognition that word
@ -157,7 +157,7 @@ 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
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
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
of a different perspective (in this case, a hardware perspective). John

View file

@ -29,7 +29,7 @@ ScanLoop:
```
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.)
John's approach makes it clear that word-counting is nothing more than a
@ -121,7 +121,7 @@ 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
getting the two bytes, working in the carry status from the last byte,
and using the resulting value to index into the 64K table, adding in the
1 or 0 value found in that table. A sequence of **MOV/ADC/ADD** suffices
1 or 0 value found in that table. A sequence of `MOV/ADC/ADD` suffices
to perform all word-counting tasks for a pair of bytes. Three
instructions, no branches—pretty nearly perfect code.

View file

@ -14,8 +14,8 @@ pages: 329-331
How slow is Listing 17.1? Table 17.1 shows that even on a 486, Listing
17.1 does fewer than three 96x96 generations per second. (The times in
Table 17.1 are for 1,000 generations of a 96x96 cell map with **seed=1,
LIMIT\_18\_HZ=0, WRAP\_EDGES=1,** and **magnifier=2,** running on a 33
Table 17.1 are for 1,000 generations of a 96x96 cell map with `seed=1,
LIMIT_18_HZ=0, WRAP_EDGES=1`, and `magnifier=2`, running on a 33
MHz 486.) Since my target is 18 generations per second with a 200x200
cellmap on a 20 MHz 386, Listing 17.1 is too slow by a rather wide
margin—75 times too slow, in fact. You might say we have a little
@ -25,41 +25,41 @@ 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
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
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
also recalculated the target address from scratch for each pixel would
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
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.
| | Listing 17.1 | Listing 17.3 | Listing 17.4 |
|--------------------------|--------------|--------------|--------------|
| **Total execution time** | 340 secs | 94 secs | 45 secs |
| **cell\_state()** | 275 | 21 | — |
| **next\_generation()** | 60 | 14 | 40 |
| **count\_neighbors()** | — | 54 | — |
| **draw\_pixel()** | 2 | 2 | 2 |
| **set\_cell()** | <1 | <1 | <1 |
| **clear\_cell()** | <1 | <1 | <1 |
| **copy\_cells()** | <1 | <1 | <1 |
| `cell_state()` | 275 | 21 | — |
| `next_generation()` | 60 | 14 | 40 |
| `count_neighbors()` | — | 54 | — |
| `draw_pixel()` | 2 | 2 | 2 |
| `set_cell()` | <1 | <1 | <1 |
| `clear_cell()` | <1 | <1 | <1 |
| `copy_cells()` | <1 | <1 | <1 |
Table: Table 17.1 Execution times for the game of life.
I was wrong. Wrong, wrong, wrong. (But at least I was smart enough to
use a profiler before actually writing any new code.) Table 17.1 shows
where the time actually goes in Listings 17.1 and 17.2. As you can see,
the time taken by **draw\_pixel(),** **copy\_cells(),** and *everything*
the time taken by `draw_pixel()`, `copy_cells()`, and *everything*
other than calculating the next generation is nothing more than noise.
We could optimize these routines right down to executing
*instantaneously,* and you know what? It wouldn't make the slightest
perceptible difference in how fast the program runs. Given the present
state of our Game of Life implementation, the only areas worth looking
at for possible optimizations are **cell\_state()** and
**next\_generation().**
at for possible optimizations are `cell_state()` and
`next_generation().`
> ![](images/i.jpg)
> It's worth noting, though, that one reason **draw\_pixel()** doesn't
> 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
@ -68,11 +68,11 @@ at for possible optimizations are **cell\_state()** and
### 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
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
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.
Once you know where you need to optimize, the one word to keep in mind
@ -100,7 +100,7 @@ algorithm running on top of a highly abstract programming model, you'll
almost certainly end up with a slow program. In Listing 17.1, the
abstraction that's killing us is that of looking at the eight neighbors
with eight completely independent operations, requiring eight calls to
**cell\_state()** and eight calculations of cell address and cell mask.
`cell_state()` and eight calculations of cell address and cell mask.
In fact, given the nature of cell storage, the eight neighbors are in a
fixed relationship to one another, and the addresses and masks of all
eight can generally be found very easily via hard-wired offsets and

View file

@ -35,7 +35,7 @@ all.
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
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
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

View file

@ -23,16 +23,16 @@ 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
decreased generality makes it more difficult to change or port the
program at some later date. A Game of Life implementation, such as
Listing 17.1, that's built on **set\_cell()**, **clear\_cell()**, and
**get\_cell()** is completely general; you can change the cell storage
Listing 17.1, that's built on `set_cell()`, `clear_cell()`, and
`get_cell()` is completely general; you can change the cell storage
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
functions.
So, in Listing 17.3, we've gotten under the hood and changed the cellmap
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
assembly?
@ -135,12 +135,12 @@ neighbor_count++;
```
Listing 17.4 and Listing 17.3 are functionally the same; the only
difference lies in how **next\_generation()** is implemented. (Only
**next\_generation()** is shown in Listing 17.4; the program is
difference lies in how `next_generation()` is implemented. (Only
`next_generation()` is shown in Listing 17.4; the program is
otherwise identical to Listing 17.3.) Listing 17.4 applies the following
optimizations to **next\_generation()**:
optimizations to `next_generation()`:
The neighbor-counting code is brought into **next\_generation,**
The neighbor-counting code is brought into `next_generation`,
eliminating many function calls and from-scratch address/mask
calculations; all multiplies are eliminated by using pointers and
addition; and all cells are accessed directly via pointers and masks,
@ -149,7 +149,7 @@ calculations.
The net effect of these optimizations is that Listing 17.4 is more than
twice as fast as Listing 17.3; we've achieved the desired 18 generations
per second, albeit only on a 486, and only at 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
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

View file

@ -115,8 +115,8 @@ representation for an off-cell that has no neighbors is a zero byte, we
can skip over scads of unchanged cells at a pop simply by scanning for
non-zero bytes. This is much faster than explicitly testing cell states
and neighbor counts, and lends itself beautifully to assembly language
implementation as **REPZ SCASB** or (with a little cleverness) **REPZ
SCASW.** (Unfortunately, there's no C library function that can scan
implementation as `REPZ SCASB` or (with a little cleverness) `REPZ
SCASW`. (Unfortunately, there's no C library function that can scan
memory for the next byte that's non-zero.)
Listing 17.5 is a Game of Life implementation that uses the

View file

@ -34,7 +34,7 @@ it's a lot easier to work with the neighbor-count model. There's no
complex mask and pointer management, and the only thing that *really*
needs to be optimized is scanning for zero bytes. (And, in fact, I
haven't optimized even that because it's done in a C++ loop; it should
really be **REPZ SCASB.**)
really be `REPZ SCASB`.)
In truth, none of the code in Listing 17.5 is particularly
well-optimized, and, as I noted, the program must be compiled with the

View file

@ -46,7 +46,7 @@ table indexed by the cell triplet itself. The value of the lookup table
entry is equal to what the high byte should be in the next generation.
If this value is equal to the current high byte, then no changes are
necessary to the cell. Otherwise it is placed in the change list. Look
at the code in the **Test()** and **Fix()** functions to see how this is
at the code in the `Test()` and `Fix()` functions to see how this is
done." [This step is as important as it is obscure. David has a 64K
table organized so that if you use a word describing a cell triplet as a
lookup index, the byte you will read will be the state of the high byte

View file

@ -111,7 +111,7 @@ discuss in the next chapter), so on the Pentium it is possible to
execute two instructions, even instructions that access memory, in a
single clock. The cycle times for instruction execution in a given pipe
(both pipes process instructions at the same speed) are comparable to
those for the 486, although some instructions—notably **MUL**, the
those for the 486, although some instructions—notably `MUL`, the
repeated string instructions, and some of the shifts and rotates—have
gotten faster.

View file

@ -15,21 +15,21 @@ pages: 375-377
I'll spend the rest of this chapter covering a variety of Pentium
optimization tips. For starters, effective address calculations (that
is, the addition and scaling required to calculate a memory operand's
address, as for example in **MOV EAX,[EBX+ECX\*2+4]**) never take any
address, as for example in `MOV EAX,[EBX+ECX*2+4]`) never take any
extra cycles on the Pentium (other than possibly an AGI cycle), even for
the use of base+index addressing (as in **MOV [ESI+EDI],EAX**) or
scaling (\*2, \*4, or \*8, as in **INC ARRAY[ESI\*4]**). On the 486,
the use of base+index addressing (as in `MOV [ESI+EDI],EAX`) or
scaling (\*2, \*4, or \*8, as in `INC ARRAY[ESI*4]`). On the 486,
both of the latter cases cause a 1-cycle penalty. The faster effective
address calculations have the side effect of making **LEA** very
attractive as an arithmetic instruction. **LEA** can add any two
address calculations have the side effect of making `LEA` very
attractive as an arithmetic instruction. `LEA` can add any two
registers, one of which can be multiplied by one, two, four, or eight,
plus a constant value, and can store the result in any register—all in
one cycle, apart from AGIs. Not only that, but as we'll see in the next
chapter, **LEA** can go through either pipe, whereas **SHL** can only go
through the U-pipe, so **LEA** is often a superior choice for
multiplication by three, four, five, eight, or nine. (**ADD** is the
best choice for multiplication by two.) If you use **LEA** for
arithmetic, do remember that unlike **ADD** and **SHL**, it doesn't
chapter, `LEA` can go through either pipe, whereas `SHL` can only go
through the U-pipe, so `LEA` is often a superior choice for
multiplication by three, four, five, eight, or nine. (`ADD` is the
best choice for multiplication by two.) If you use `LEA` for
arithmetic, do remember that unlike `ADD` and `SHL`, it doesn't
modify any flags.
As on the 486, memory operands should not cross any more alignment
@ -61,8 +61,8 @@ bigger overall.
Instruction prefixes are awfully expensive; avoid them if you can.
(These include size and addressing prefixes, segment overrides,
**LOCK**, and the 0FH prefixes that extend the instruction set with
instructions such as **MOVSX**. The exceptions are conditional jumps, a
`LOCK`, and the 0FH prefixes that extend the instruction set with
instructions such as `MOVSX`. The exceptions are conditional jumps, a
fast special case.) At a minimum, a prefix byte generally takes an extra
cycle and shuts down the V-pipe for that cycle, effectively costing as
much as two normal instructions (although prefix cycles can overlap with
@ -81,34 +81,34 @@ variables to longs when performing calculations.) Likewise, you should
if possible avoid putting data in the code segment and referring to it
with a CS: prefix, or otherwise using segment overrides.
**LOCK** is a particularly costly instruction, especially on
`LOCK` is a particularly costly instruction, especially on
multiprocessor machines, because it locks the bus and requires that the
hardware be brought into a synchronized state. The cost varies depending
on the processor and system, but **LOCK** can make an **INC [*mem*]**
on the processor and system, but `LOCK` can make an `INC [*mem*]`
instruction (which normally takes 3 cycles) 5, 10, or more cycles
slower. Most programmers will never use **LOCK** on purpose—it's
slower. Most programmers will never use `LOCK` on purpose—it's
primarily an operating system instruction—but there's a hidden gotcha
here because the **XCHG** instruction always locks the bus when used
here because the `XCHG` instruction always locks the bus when used
with a memory operand.
> ![](images/i.jpg)
> **XCHG** is a tempting instruction that's often used in assembly
> `XCHG` is a tempting instruction that's often used in assembly
> language; for example, exchanging with video memory is a popular way to
> read and write VGA memory in a single instruction—but it's now a bad
> idea. As it happens, on the 486 and Pentium, using **MOV**s to read and
> idea. As it happens, on the 486 and Pentium, using `MOV`s to read and
> write memory is faster, anyway; and even on the 486, my measurements
> indicate a five-cycle tax for **LOCK** in general, and a nine-cycle
> execution time for **XCHG** with memory. Avoid **XCHG** with memory if
> indicate a five-cycle tax for `LOCK` in general, and a nine-cycle
> execution time for `XCHG` with memory. Avoid `XCHG` with memory if
> you possibly can.
As with the 486, don't use **ENTER** or **LEAVE**, which are slower than
the equivalent discrete instructions. Also, start using **TEST
*reg,reg*** instead of **AND *reg,reg*** or **OR *reg,reg*** to test
As with the 486, don't use `ENTER` or `LEAVE`, which are slower than
the equivalent discrete instructions. Also, start using `TEST
*reg,reg*` instead of `AND *reg,reg*` or `OR *reg,reg*` to test
whether a register is zero. The reason, as we'll see in Chapter 21, is
that **TEST**, unlike **AND** and **OR**, never modifies the target
register. Although in this particular case **AND** and **OR** don't
that `TEST`, unlike `AND` and `OR`, never modifies the target
register. Although in this particular case `AND` and `OR` don't
modify the target register either, the Pentium has no way of knowing
that ahead of time, so if **AND** or **OR** goes through the U-pipe, the
that ahead of time, so if `AND` or `OR` goes through the U-pipe, the
Pentium may have to shut down the V-pipe for a cycle to avoid potential
dependencies on the result of the **AND** or **OR**. **TEST** suffers
dependencies on the result of the `AND` or `OR`. `TEST` suffers
from no such potential dependencies.

View file

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

View file

@ -67,7 +67,7 @@ we'll see shortly, that doesn't mean that optimized Pentium code looks
much like optimized 486 code, or that fast 486 code runs particularly
well on a Pentium. (Fast Pentium code, on the other hand, does tend to
run well on the 486; the only major downsides are that it's larger, and
that the **FXCH** instruction, which is largely free on the Pentium, is
that the `FXCH` instruction, which is largely free on the Pentium, is
expensive on the 486.) So discard your x86 preconceptions as we delve
into superscalar optimization for this one-of-a-kind processor.
@ -107,9 +107,9 @@ instructions and is always active, with the objective being to keep the
V-pipe also working as much of the time as possible.) The U-pipe is
generally similar to a full 486 in terms of both capabilities and
instruction cycle counts. The V-pipe is a 486 subset, able to execute
simple instructions such as **MOV** and **ADD**, but unable to handle
**MUL, DIV**, string instructions, any sort of rotation or shift, or
even **ADC** or **SBB**.
simple instructions such as `MOV` and `ADD`, but unable to handle
`MUL, DIV`, string instructions, any sort of rotation or shift, or
even `ADC` or `SBB`.
![**Figure 20.1**  *The Pentium's two pipes.*](images/20-01.jpg)
@ -117,16 +117,16 @@ Getting two instructions executing simultaneously in the two pipes is
trickier than it sounds, not only because the V-pipe can handle only a
relatively small subset of the Pentium's instruction set, but also
because those instructions that the V-pipe can handle are able to pair
only with certain U-pipe instructions. For example, **MOVSD** uses both
pipes, so no instruction can be executed in parallel with **MOVSD**.
only with certain U-pipe instructions. For example, `MOVSD` uses both
pipes, so no instruction can be executed in parallel with `MOVSD`.
> ![](images/i.jpg)
> The use of both pipes does make **MOVSD** nearly twice as fast on the
> The use of both pipes does make `MOVSD` nearly twice as fast on the
> Pentium as on the 486, but it's nonetheless slower than using equivalent
> simpler instructions that allow for superscalar execution. Stick to the
> Pentium's RISC-like instructions—the pairable instructions I'll discuss
> next—when you're seeking maximum performance, with just a few exceptions
> such as **REP MOVS** and **REP STOS**.
> such as `REP MOVS` and `REP STOS`.
Trickier yet, register contention can shut down the V-pipe on any given
cycle, and Address Generation Interlocks (AGIs) can stall either pipe at

View file

@ -21,9 +21,9 @@ in Table 20.1 can go through the V-pipe. In addition, the V-pipe can
execute a separate instruction only when one of the instructions listed
in Table 20.2 is executing in the U-pipe; superscalar execution is not
possible while any instruction not listed in Table 20.2 is executing in
the U-pipe. So, for example, if you use **SHR EDX,CL**, which takes 4
the U-pipe. So, for example, if you use `SHR EDX,CL`, which takes 4
cycles to execute, no other instructions can execute during those 4
cycles; if, on the other hand, you use **SHR EDX,10**, it will take 1
cycles; if, on the other hand, you use `SHR EDX,10`, it will take 1
cycle to execute in the U-pipe, and another instruction can potentially
execute concurrently in the V-pipe. (As you can see, similar instruction
sequences can have vastly different performance characteristics on the
@ -93,13 +93,13 @@ same clock speed, even without Pentium-specific optimization, contrary
to some reports.
Besides, almost all operations can be performed by combinations of
pairable instructions. For example, **PUSH [*mem*]** is not on either
list, but both **MOV *reg*,[*mem*]** and **PUSH *reg*** are, and those
pairable instructions. For example, `PUSH [*mem*]` is not on either
list, but both `MOV *reg*,[*mem*]` and `PUSH *reg*` are, and those
two instructions can be used to push a value stored in memory. In fact,
given the proper instruction stream, the discrete instructions can
perform this operation effectively in just 1 cycle (taking one-half of
each of 2 cycles, for 2\*0.5 = 1 cycle total execution time), as shown
in Figure 20.3—a full cycle *faster* than **PUSH [*mem*]**, which takes
in Figure 20.3—a full cycle *faster* than `PUSH [*mem*]`, which takes
2 cycles.
```nasm
@ -189,7 +189,7 @@ mov [MemVar],edx
The single complex instruction takes 3 cycles and is 6 bytes long; with
proper sequencing, interleaving the simple instructions with other
instructions that don't use EDX or **Mem Var**, the three-instruction
instructions that don't use EDX or `Mem Var`, the three-instruction
sequence can be reduced to 1.5 cycles, but it is *14* bytes long.
> ![](images/i.jpg)

View file

@ -12,18 +12,18 @@ pages: 390-393
### Lockstep Execution {#Heading5}
You may wonder why anyone would bother breaking **ADD [MemVar],EAX**
You may wonder why anyone would bother breaking `ADD [MemVar],EAX`
into three instructions, given that this instruction can go through
either pipe with equal ease. The answer is that while the
memory-accessing instructions other than **MOV, PUSH**, and **POP**
listed in Table 20.1 (that is, **INC/DEC [*mem*],
ADD/SUB/XOR/AND/OR/CMP/ADC/SBB *reg*,[*mem*]**, and
**ADD/SUB/XOR/AND/OR/CMP/ADC/SBB [*mem*],*reg/immed***) can be paired,
memory-accessing instructions other than `MOV, PUSH`, and `POP`
listed in Table 20.1 (that is, `INC/DEC [*mem*],
ADD/SUB/XOR/AND/OR/CMP/ADC/SBB *reg*,[*mem*]`, and
`ADD/SUB/XOR/AND/OR/CMP/ADC/SBB [*mem*],*reg/immed*`) can be paired,
they do not provide the 100 percent overlap that we seek. If you look at
Tables 20.1 and 20.2, you will see that instructions taking from 1 to 3
cycles can pair. However, any pair of instructions goes through the two
pipes in lockstep. This means, for example, that if **ADD [EBX],EDX** is
going through the U-pipe, and **INC EAX** is going through the V-pipe,
pipes in lockstep. This means, for example, that if `ADD [EBX],EDX` is
going through the U-pipe, and `INC EAX` is going through the V-pipe,
the V-pipe will be idle for 2 of the 3 cycles that the U-pipe takes to
execute its instruction, as shown in Figure 20.4. Out of the theoretical
6 cycles of work that can be done during this time, we actually get only
@ -108,12 +108,12 @@ below. This is yet another example of how different Pentium optimization
can be from everything we've learned about its predecessors.
The problem with pairing non-single-cycle instructions arises when a
pipe executes an instruction other than **MOV** that has an explicit
pipe executes an instruction other than `MOV` that has an explicit
memory operand. (I'll call these *complex memory instructions*. They're
the only pairable instructions, other than branches, that take more than
one cycle.) We've already seen that, because instructions go through the
pipes in lockstep, if one pipe executes a complex memory instruction
such as **ADD EAX,[EBX]** while the other pipe executes a single-cycle
such as `ADD EAX,[EBX]` while the other pipe executes a single-cycle
instruction, the pipe with the faster instruction will sit idle for part
of the time, wasting cycles. You might think that if both pipes execute
complex instructions of the same length, then neither would lie idle,

View file

@ -10,7 +10,7 @@ chapter: '20'
pages: 393-396
---
However, this beneficial pairing does not extend to non-**MOV**
However, this beneficial pairing does not extend to non-`MOV`
instructions with explicit memory destination operands, such as **ADD
[EBX],EAX**. The Pentium executes only one such memory instruction at a
time; if two memory-destination complex instructions get paired, first
@ -18,7 +18,7 @@ the U-pipe instruction is executed, and then the V-pipe instruction,
with only one cycle of overlap, as shown in Figure 20.6. I don't know
for sure, but I'd guess that this is to guarantee that the two pipes
will never perform out-of-order access to any given memory location.
Thus, even though **AND [EBX],AL** pairs with **AND [ECX],DL**, the two
Thus, even though `AND [EBX],AL` pairs with `AND [ECX],DL`, the two
instructions take 5 cycles in all to execute, and 4 cycles of idle
time—2 in the U-pipe and 2 in the V-pipe, out of 10 cycles in all—are
incurred in the process.
@ -54,14 +54,14 @@ mov [ebx],dl
```
and be done with it. The problem here is one of dependency. Before the
Pentium can execute **AND DL,AL,**, it must first know what is in DL,
Pentium can execute `AND DL,AL,`, it must first know what is in DL,
and it can't know that until it loads DL from the address pointed to by
EBX. Therefore, **AND DL,AL** can't happen until the cycle after **MOV
DL,[EBX]** executes. Likewise, the result can't be stored until the
cycle after **AND DL,AL** has finished. This means that these
EBX. Therefore, `AND DL,AL` can't happen until the cycle after `MOV
DL,[EBX]` executes. Likewise, the result can't be stored until the
cycle after `AND DL,AL` has finished. This means that these
instructions, as written, can't possibly pair, so the sequence takes the
same three cycles as **AND [EBX],AL**. (Now it should be clear why **AND
[EBX]**, AL takes 3 cycles.) Consequently, it's necessary to interleave
same three cycles as `AND [EBX],AL`. (Now it should be clear why `AND
[EBX]`, AL takes 3 cycles.) Consequently, it's necessary to interleave
these instructions with instructions that use other registers, so this
set of operations can execute in one pipe while the other, unrelated set
executes in the other pipe, as is done in Figure 20.7.
@ -110,7 +110,7 @@ Finally, bear in mind that if the instructions being executed have not
already been executed at least once since they were loaded into the
internal cache, they can pair only if the first (U-pipe) instruction is
not only pairable but also exactly 1 byte long, a category that includes
only **INC *reg*, DEC *reg*, PUSH *reg***, and **POP *reg***. Knowing
only `INC *reg*, DEC *reg*, PUSH *reg*`, and `POP *reg*`. Knowing
this can help you understand why sometimes, timing reveals that your
code runs slower than it seems it should, although this will generally
occur only when the cache working set for the code you're timing is on

View file

@ -114,13 +114,13 @@ mov edx,[ebp-8] ;V-pipe cycle 3 lockstep idle
;V-pipe cycle 4 mov edx,[ebp-8]
```
This commonplace code loses a U-pipe cycle to the AGI caused by **AND
EBX,EBX**, followed by the attempt two instructions later to use EBX to
This commonplace code loses a U-pipe cycle to the AGI caused by `AND
EBX,EBX`, followed by the attempt two instructions later to use EBX to
point to memory. The code loses a V-pipe cycle as well, because lockstep
execution won't let the next V-pipe instruction execute until the paired
U-pipe instruction that suffered the AGI finishes. The solution is to
use **TEST EBX,EBX** instead of **AND; TEST** can't modify EBX, so no
AGI occurs. Sure, **AND EBX,EBX** doesn't modify EBX either, but the
use `TEST EBX,EBX` instead of `AND; TEST` can't modify EBX, so no
AGI occurs. Sure, `AND EBX,EBX` doesn't modify EBX either, but the
Pentium doesn't know that, so it has to insert the AGI.
![**Figure 21.2**  *An AGI can cost as many as 3 cycles.*](images/21-02.jpg)

View file

@ -11,7 +11,7 @@ pages: 402-405
---
As on the 486, you should keep a careful eye out for AGIs involving the
stack pointer. Implicit modifiers of ESP, such as **PUSH** and **POP**,
stack pointer. Implicit modifiers of ESP, such as `PUSH` and `POP`,
are special-cased so you don't have to worry about AGIs. However, if you
explicitly modify ESP with this instruction
@ -32,7 +32,7 @@ either explicitly with instructions like this one
moveax,[esp+20h]
```
or via **PUSH**, **POP**, or other instructions that implicitly use ESP
or via `PUSH`, `POP`, or other instructions that implicitly use ESP
as an addressing register.
On the 486, any instruction that had both a constant value and an
@ -44,7 +44,7 @@ mov dword ptr [ebp+16],1
suffered a 1-cycle penalty, taking a total of 2 cycles. Such
instructions take only one cycle on the Pentium, but they cannot pair,
so they're still the most expensive sort of **MOV**. Knowing this can
so they're still the most expensive sort of `MOV`. Knowing this can
speed up something as simple as zeroing two memory variables, as in
```nasm
@ -67,7 +67,7 @@ mov [MemVar2],0 ;U-pipe 2
Note, however, that my experiments thus far indicate that the two writes
in the first case don't actually pair (possibly because the memory
variables have never been read into the internal cache), so you might
want to insert an instruction between the two **MOV**s—and, of course,
want to insert an instruction between the two `MOV`s—and, of course,
this is yet another reason why you should always measure your code's
actual performance.
@ -85,9 +85,9 @@ inc eax ;U-pipe cycle 1
and ebx,eax ;U-pipe cycle 2
```
in a single cycle; **AND EBX,EAX** can't execute until the value in EAX
is known, and that can't happen until **INC EAX** is done. Consequently,
the V-pipe idles while **INC EAX** executes in the U-pipe. We saw this
in a single cycle; `AND EBX,EAX` can't execute until the value in EAX
is known, and that can't happen until `INC EAX` is done. Consequently,
the V-pipe idles while `INC EAX` executes in the U-pipe. We saw this
in the last chapter when we discussed splitting instructions into simple
instructions, and it is by far the most common sort of register
contention, known as read-after-write register contention.
@ -109,8 +109,8 @@ mov al,[Var] ;U-pipe cycle 2
where an attempt is made to set both EAX and its AL subregister on the
same cycle. Write-after-write contention implies that the two
instructions comprising the above substitute for **MOVZX** should have
at least one unrelated instruction between them when **SUB EAX,EAX**
instructions comprising the above substitute for `MOVZX` should have
at least one unrelated instruction between them when `SUB EAX,EAX`
executes in the V-pipe.
#### Exceptions to Register Contention {#Heading5}
@ -129,10 +129,10 @@ are free of charge.
Also, stack-related instructions that modify ESP only implicitly
(without ESP as part of any explicit operand) do not cause AGIs, and
neither do they cause register contention with other instructions that
use ESP only implicitly; such instructions include **PUSH *reg/immed*,
POP *reg***, and **CALL**. (However, these instructions do cause
use ESP only implicitly; such instructions include `PUSH *reg/immed*,
POP *reg*`, and `CALL`. (However, these instructions do cause
register contention on ESP—but not AGIs—with instructions that use ESP
explicitly, such as **MOV EAX,[ESP+4]**.) Without this special case, the
explicitly, such as `MOV EAX,[ESP+4]`.) Without this special case, the
following sequence would hardly use the V-pipe at all:
```nasm
@ -167,7 +167,7 @@ predicted by the Pentium's branch prediction circuitry (as discussed in
the last chapter), it executes in a single cycle, pairing if it runs in
the V-pipe; if mispredicted, conditional jumps take 4 cycles in the
U-pipe and 5 cycles in the V-pipe, and mispredicted calls and
unconditional jumps take 3 cycles in either pipe. Note that **RET**
unconditional jumps take 3 cycles in either pipe. Note that `RET`
can't pair.
### Who's in First? {#Heading6}

View file

@ -19,7 +19,7 @@ the U-pipe, as do all instructions after conditional branches that fall
through. Instructions with prefix bytes are generally good U-pipe
markers, although they're expensive instructions that should be avoided
whenever possible, and have at least one aberration with regard to pipe
usage, as discussed below. Shifts, rotates, **ADC, SBB**, and all other
usage, as discussed below. Shifts, rotates, `ADC, SBB`, and all other
instructions not listed in Table 20.1 in the last chapter are likewise
U-pipe markers.
@ -75,11 +75,11 @@ Wrong, wrong, wrong! As detailed in Listing 21.1, this loop should take
6 cycles per checksummed word in 32-bit protected mode, a ridiculously
high number for the Pentium. (You'll see why I say "should take," not
"takes," shortly.) We should lose 2 cycles in each pipe to the two size
prefixes (because the **ADD**s are 16-bit operations in a 32-bit
prefixes (because the `ADD`s are 16-bit operations in a 32-bit
segment), and another 2 cycles because of register contention that
arises when **ADC AX,0** has to wait for the result of **ADD AX,[ESI]**.
Then, too, even though **DEC** and **JNZ** can pair and the branch
prediction for **JNZ** is presumably correct virtually all the time,
arises when `ADC AX,0` has to wait for the result of `ADD AX,[ESI]`.
Then, too, even though `DEC` and `JNZ` can pair and the branch
prediction for `JNZ` is presumably correct virtually all the time,
they do take a full cycle, and maybe we can do something about that as
well.
@ -87,11 +87,11 @@ The first thing to do is to time the code in Listing 21.1 to verify our
analysis. When I unleashed the Zen timer on Listing 21.1, I found, to my
surprise, that the code actually takes only five cycles per checksum
word processed, not six. A little more experimentation revealed that
adding a size prefix to the two-cycle **ADD EAX,[ESI]** instruction
adding a size prefix to the two-cycle `ADD EAX,[ESI]` instruction
doesn't cost anything, certainly not the one full cycle in each pipe
that a prefix is supposed to take. More experimentation showed that
prefix bytes do cost the documented extra cycle when used with one-cycle
instructions such as **MOV**. At this point, my preliminary conclusion
instructions such as `MOV`. At this point, my preliminary conclusion
is that prefixes can pair with the first cycle of at least some
multiple-cycle instructions. Determining exactly why this happens will
take further research on my part, but the most important conclusion is

View file

@ -10,19 +10,19 @@ chapter: '21'
pages: 407-409
---
The first, obvious thing we can do to Listing 21.1 is change **ADC
AX,0** to **ADC EAX,0**, eliminating a prefix byte and saving a full
cycle. Now we're down from five to four cycles. What next?
The first, obvious thing we can do to Listing 21.1 is change `ADC AX,0` to
`ADC EAX,0`, eliminating a prefix byte and saving a full cycle. Now we're
down from five to four cycles. What next?
Listing 21.2 shows one interesting alternative that doesn't really buy
us anything. Here, we've eliminated all size prefixes by doing
byte-sized **MOVs** and **ADDs**, but because the size prefix on **ADD
AX,[ESI]**, for whatever reason, didn't cost anything in Listing 21.1,
byte-sized `MOVs` and `ADDs`, but because the size prefix on `ADD
AX,[ESI]`, for whatever reason, didn't cost anything in Listing 21.1,
our efforts are to no avail—Listing 21.2 still takes 4 cycles per
checksummed word. What's worth noting about Listing 21.2 is the extent
to which the code is broken into simple instructions and reordered so as
to avoid size prefixes, register contention, AGIs, and data bank
conflicts (the latter because both **[ESI]** and **[ESI+1]** are in the
conflicts (the latter because both `[ESI]` and `[ESI+1]` are in the
same cache data bank, as discussed in the last chapter).
**LISTING 21.2 L21-2.ASM**
@ -66,7 +66,7 @@ instructions than 486-optimized code. Again, note the careful mixing of
byte-sized reads to avoid AGIs, register contention, and cache bank
collisions, in particular the way in which the byte reads of memory are
interspersed with the additions to avoid register contention, and the
placement of **ADD ESI,4** to avoid an AGI.
placement of `ADD ESI,4` to avoid an AGI.
**LISTING 21.3 L21-3.ASM**

View file

@ -109,10 +109,10 @@ significant difference in overall system performance.
I don't claim that Listing 21.5 is the fastest possible way to do a
TCP/IP checksum on a Pentium; in fact, it isn't. Unrolling the loop one
more time, together with a trick of Terje's that uses **LEA** to advance
ESI (neither **LEA** nor **DEC** affects the carry flag, allowing Terje
more time, together with a trick of Terje's that uses `LEA` to advance
ESI (neither `LEA` nor `DEC` affects the carry flag, allowing Terje
to add the carry from the previous loop iteration into the next
iteration's checksum via **ADC**), produces a version that's a full 33
iteration's checksum via `ADC`), produces a version that's a full 33
percent faster. Nonetheless, Listings 21.1 through 21.5 illustrate many
of the techniques and considerations in Pentium optimization.
Hand-optimization for the Pentium isn't simple, and requires careful

View file

@ -92,25 +92,25 @@ Bye:mov sp,bp ;restore original stack pointer
ClearS endp
```
The first thing you'll notice about Listing 22.1 is that **ClearS** uses
a **REP STOSW** instruction. That means that we're not going to improve
The first thing you'll notice about Listing 22.1 is that `ClearS` uses
a `REP STOSW` instruction. That means that we're not going to improve
performance by any great amount, no matter how clever we are. While we
can eliminate some cycles, the bulk of the work in **ClearS** is done by
can eliminate some cycles, the bulk of the work in `ClearS` is done by
that one repeated string instruction, and there's no way to improve on
that.
Does that mean that Listing 22.1 is as good as it can be? Hardly. While
the speed of **ClearS** is very good, there's another side to the
optimization equation: size. The whole of **ClearS** is 52 bytes long as
the speed of `ClearS` is very good, there's another side to the
optimization equation: size. The whole of `ClearS` is 52 bytes long as
it stands—but, as we'll see, that size is hardly set in stone.
Where do we begin with **ClearS**? For starters, there's an instruction
in there that serves no earthly purpose—**MOV SP,BP**. SP is guaranteed
Where do we begin with `ClearS`? For starters, there's an instruction
in there that serves no earthly purpose—`MOV SP,BP`. SP is guaranteed
to be equal to BP at that point anyway, so why reload it with the same
value? Removing that instruction saves us two bytes.
Well, that was certainly easy enough! We're not going to find any more
totally non-functional instructions in **ClearS**, however, so let's get
totally non-functional instructions in `ClearS`, however, so let's get
on to some serious optimizing. We'll look first for cases where we know
of better instructions for particular tasks than those that were chosen.
For example, there's no need to load any register, whether segment or

View file

@ -36,15 +36,15 @@ Bye:
ClearS endp
```
(The **OnStack** structure definition doesn't change in any of our
(The `OnStack` structure definition doesn't change in any of our
examples, so I'm not going clutter up this chapter by reproducing it for
each new version of **ClearS**.)
each new version of `ClearS`.)
Okay, loading ES and DI directly saves another four bytes. We've
squeezed a total of 6 bytes—about 11 percent—out of **ClearS**. What
squeezed a total of 6 bytes—about 11 percent—out of `ClearS`. What
next?
Well, **LES** would serve better than two **MOV** instructions for
Well, `LES` would serve better than two `MOV` instructions for
loading ES and DI as shown in Listing 22.3.
**LISTING 22.3 L22-3.ASM**
@ -77,7 +77,7 @@ That's good for another three bytes. We're down to 43 bytes, and
counting.
We can save 3 more bytes by clearing the low and high bytes of AX and
BX, respectively, by using **SUB *reg8,reg8*** rather than ANDing 16-bit
BX, respectively, by using `SUB *reg8,reg8*` rather than ANDing 16-bit
values as shown in Listing 22.4.
**LISTING 22.4 L22-4.ASM**

View file

@ -37,7 +37,7 @@ pack both the attribute and the fill value into the same word, but
that's not part of the specification for this particular routine.)
Another nifty instruction-rearrangement trick saves 6 more bytes.
**ClearS** checks to see whether the far pointer is null (zero) at the
`ClearS` checks to see whether the far pointer is null (zero) at the
start of the routine...then loads and uses that same far pointer later
on. Let's get that pointer into registers and keep it there; that way we
can check to see whether it's null with a single comparison, and can use
@ -72,11 +72,11 @@ Realistically, how much smaller can we make this code?
About one-third smaller yet, as it turns out—but in order to do that, we
must stretch our minds and use the 8088's instructions in unusual ways.
Let me ask you this: What do most of the instructions in the current
version of **ClearS** do?
version of `ClearS` do?
They either load parameters from the stack frame or set up the registers
so that the parameters can be accessed. Mind you, there's nothing wrong
with the stack-frame-oriented instructions used in **ClearS**; those
with the stack-frame-oriented instructions used in `ClearS`; those
instructions access the stack frame in a highly efficient way, exactly
as the designers of the 8088 intended, and just as the code generated by
a high-level language would. That means that we aren't going to be able
@ -90,12 +90,12 @@ stack...*the stack*...THE STACK!
Ye gods! That's easy—we can use the *stack pointer* to address the stack
rather than BP. While it's true that the stack pointer can't be used for
*mod-reg-rm* addressing, as BP can, it *can* be used to pop data off the
stack—and **POP** is a one-byte instruction. Instructions don't get any
stack—and `POP` is a one-byte instruction. Instructions don't get any
shorter than that.
There is one detail to be taken care of before we can put our plan into
action: The return address—the address of the calling code—is on top of
the stack, so the parameters we want can't be reached with **POP**.
the stack, so the parameters we want can't be reached with `POP`.
That's easily solved, however—we'll just pop the return address into an
unused register, then branch through that register when we're done, as
we learned to do in Chapter 14. As we pop the parameters, we'll also be
@ -103,7 +103,7 @@ removing them from the stack, thereby neatly avoiding the need to
discard them when it's time to return.
With that problem dealt with, Listing 22.7 shows the Zenned version of
**ClearS**.
`ClearS`.
**LISTING 22.7 L22-7.ASM**
@ -126,13 +126,13 @@ Bye:
ClearS endp
```
At long last, we're down to the bare metal. This version of **ClearS**
At long last, we're down to the bare metal. This version of `ClearS`
is just 19 bytes long. That's just 37 percent as long as the original
version, *without any change whatsoever in the functionality that
**ClearS** makes available to the calling code*. The code is bound to
`ClearS` makes available to the calling code*. The code is bound to
run a bit faster too, given that there are far fewer instruction bytes
and fewer memory accesses.
All in all, the Zenned version of **ClearS** is a vast improvement over
All in all, the Zenned version of `ClearS` is a vast improvement over
the original. Probably not the best possible implementation—*never say
never!*—but an awfully good one.

View file

@ -45,8 +45,8 @@ The ports used to control the VGA are shown in Table 23.1. The CRTC, SC,
and GC Data registers are located at the addresses of their respective
Index registers plus one. However, the AC Index and Data registers are
located at the same address, 3C0H. The function of this port toggles on
every **OUT** to 3C0H, and resets to Index mode (in which the Index
register is programmed by the next **OUT** to 3C0H) on every read from
every `OUT` to 3C0H, and resets to Index mode (in which the Index
register is programmed by the next `OUT` to 3C0H) on every read from
the Input Status 1 register (3DAH when the VGA is in a color mode,
| Register | Address |
@ -76,27 +76,27 @@ addressing of the now-vanished Color/Graphics Adapter and Monochrome
Display Adapter.
The method used in the VGA BIOS to set registers is to point DX to the
desired Index register, load AL with the index, perform a byte **OUT**,
desired Index register, load AL with the index, perform a byte `OUT`,
increment DX to point to the Data register (except in the case of the
AC, where DX remains the same), load AL with the desired data, and
perform a byte **OUT**. A handy shortcut is to point DX to the desired
perform a byte `OUT`. A handy shortcut is to point DX to the desired
Index register, load AL with the index, load AH with the data, and
perform a word **OUT**. Since the high byte of the **OUT** value goes to
perform a word `OUT`. Since the high byte of the `OUT` value goes to
port DX+1, this is equivalent to the first method but is faster.
However, this technique does not work for programming the AC Index and
Data registers; both AC registers are addressed at 3C0H, so two separate
byte **OUT**s must be used to program the AC. (Actually, word **OUT**s
byte `OUT`s must be used to program the AC. (Actually, word `OUT`s
to the AC do work in the EGA, but not in the VGA, so they shouldn't be
used.) As mentioned above, you must be sure which mode—Index or Data—the
AC is in before you do an **OUT** to 3C0H; you can read the Input Status
AC is in before you do an `OUT` to 3C0H; you can read the Input Status
1 register at any time to force the AC to Index mode.
How safe is the word-**OUT** method of addressing VGA registers? I have,
How safe is the word-`OUT` method of addressing VGA registers? I have,
in the past, run into adapter/computer combinations that had trouble
with word **OUT**s; however, all such problems I am aware of have been
with word `OUT`s; however, all such problems I am aware of have been
fixed. Moreover, a great deal of graphics software now uses word
**OUT**s, so any computer or VGA that doesn't properly support word
**OUT**s could scarcely be considered a clone at all.
`OUT`s, so any computer or VGA that doesn't properly support word
`OUT`s could scarcely be considered a clone at all.
> ![](images/i.jpg)
> A speed tip: The setting of each chip's Index register remains the same
@ -120,11 +120,11 @@ fixed. Moreover, a great deal of graphics software now uses word
> INC DX ;point to GC Data register>
>
> and then the Bit Mask register could be set repeatedly with the
> byte-size **OUT** instruction
> byte-size `OUT` instruction
>
> OUT DX,AL ;AL contains Bit Mask register setting
>
> which is generally faster (and never slower) than a word-sized **OUT**,
> which is generally faster (and never slower) than a word-sized `OUT`,
> and which does not require AH to be set, freeing up a register. Of
> course, this method only works if the GC Index register remains
> unchanged throughout the loop.

View file

@ -48,21 +48,21 @@ simultaneously.
The images are written to a nondisplayed portion of VGA memory in order
to take advantage of a useful VGA hardware feature, the ability to copy
all four planes at once. As shown by the image-loading code discussed
above, four different sets of reads and writes—and several **OUT**s as
above, four different sets of reads and writes—and several `OUT`s as
well—are required to copy a multicolored image into VGA memory as would
be needed to draw the same image into a non-planar pixel buffer. This
causes unacceptably slow performance, all the more so because the wait
states that occur on accesses to VGA memory make it very desirable to
minimize display memory accesses, and because **OUT**s tend to be very
minimize display memory accesses, and because `OUT`s tend to be very
slow.
The solution is to take advantage of the VGA's write mode 1, which is
selected via bits 0 and 1 of the GC Mode register (GC register 5). (Be
careful to preserve bits 2-7 when setting bits 0 and 1, as is done in
Listing 23.1.) In write mode 1, a single **CPU** read loads the
Listing 23.1.) In write mode 1, a single `CPU` read loads the
addressed byte from all four planes into the VGA's four internal
latches, and a single **CPU** write writes the contents of the latches
to the four planes. During the write, the byte written by the **CPU** is
latches, and a single `CPU` write writes the contents of the latches
to the four planes. During the write, the byte written by the `CPU` is
irrelevant.
The sample program uses write mode 1 to copy the images that were

View file

@ -62,8 +62,8 @@ require dozens or hundreds of images. The tradeoffs between virtual page
size, page flipping, and image storage must always be kept in mind when
designing programs for the VGA.
To see the program run in 640x200 16-color mode, comment out the **EQU**
line for **MEDRES\_VIDEO\_MODE**.
To see the program run in 640x200 16-color mode, comment out the `EQU`
line for `MEDRES_VIDEO_MODE`.
### The Hazards of VGA Clones {#Heading10}

View file

@ -43,7 +43,7 @@ writes the result to that plane. This arrangement allows four display
memory bytes to be modified by a single CPU write (which must often be
preceded by a single CPU read, as we will see). The benefit is vastly
improved performance; if the CPU had to select each of the four planes
in turn via **OUT**s and perform the four logical operations itself, VGA
in turn via `OUT`s and perform the four logical operations itself, VGA
performance would slow to a crawl.
Figure 24.1 is a simplified depiction of data flow around the ALUs. Each

View file

@ -70,7 +70,7 @@ only because it is necessary to perform a read to load the latches, and
there is no way to read without placing a value in a register. This is a
bit of a nuisance, since it means that the value of some 8-bit register
must be destroyed. Under certain circumstances, a single logical
instruction such as **XOR** or **AND** can be used to perform both the
instruction such as `XOR` or `AND` can be used to perform both the
read to load the latches and then write to modify display memory without
affecting any CPU registers, as we'll see later on.
@ -84,7 +84,7 @@ display mode. The great virtue of the BIOS write string function in the
case of the VGA is that it provides an uncomplicated way to get text on
the screen reliably in any mode and color, over any background.
The expression used to load DX in the **TEXT\_UP** macro in the sample
The expression used to load DX in the `TEXT_UP` macro in the sample
program may seem strange, but it's a convenient way to save a byte of
program code and a few cycles of execution time. DX is being loaded with
a word value that's composed of two independent immediate byte values.
@ -96,8 +96,8 @@ MOV DH,VALUE2
```
which requires four instruction bytes. By shifting the value destined
for the high byte into the high byte with MASM's shift-left operator,
**SHL** (\*100H would work also), and then logically combining the
values with MASM's **OR** operator (or the **ADD** operator), both
`SHL` (\*100H would work also), and then logically combining the
values with MASM's `OR` operator (or the `ADD` operator), both
halves of DX can be loaded with a single instruction, as in
```nasm
@ -106,7 +106,7 @@ MOV DX,(VALUE2 SHL 8) OR VALUE1
which takes only three bytes and is faster, being a single instruction.
(Note, though, that in 32-bit protected mode, there's a size and
performance penalty for 16-bit instructions such as the **MOV** above;
performance penalty for 16-bit instructions such as the `MOV` above;
see the first part of this book for details.) As shown, a macro is an
ideal place to use this technique; the macro invocation can refer to two
separate byte values, making matters easier for the programmer, while
@ -114,21 +114,21 @@ the macro itself can combine the values into a single word-sized
constant.
> ![](images/i.jpg)
> A minor optimization tip illustrated in the listing is the use of **INC
> AX** and **DEC AX** in the **DrawVerticalBox** subroutine when only AL
> A minor optimization tip illustrated in the listing is the use of `INC
> AX` and `DEC AX` in the `DrawVerticalBox` subroutine when only AL
> actually needs to be modified. Word-sized register increment and
> decrement instructions (or dword-sized instructions in 32-bit protected
> mode) are only one byte long, while byte-size register increment and
> decrement instructions are two bytes long. Consequently, when size
> counts, it is worth using a whole 16-bit (or 32-bit) register instead of
> the low 8 bits of that register for **INC** and **DEC**—if you don't
> the low 8 bits of that register for `INC` and `DEC`—if you don't
> need the upper portion of the register for any other purpose, or if you
> can be sure that the **INC** or **DEC** won't affect the upper part of
> can be sure that the `INC` or `DEC` won't affect the upper part of
> the register.
The latches and ALUs are central to high-performance VGA code, since
they allow programs to process across all four memory planes without a
series of **OUT**s and read/write operations. It is not always easy to
series of `OUT`s and read/write operations. It is not always easy to
arrange a program to exploit this power, however, because the ALUs are
far more limited than a CPU. In many instances, however, additional
hardware in the VGA, including the bit mask, the set/reset features, and

View file

@ -43,14 +43,14 @@ unmodified, AND, OR, and XOR) that we looked at in the last chapter.
The barrel shifter is powerful, but (as sometimes happens in this
business) it sounds more useful than it really is. This is because the
GC can only rotate CPU data, a task that the CPU itself is perfectly
capable of performing. Two **OUT**s are needed to select a given
capable of performing. Two `OUT`s are needed to select a given
rotation: one to set the GC Index register, and one to set the Data
Rotate register. However, with careful programming it's sometimes
possible to leave the GC Index always pointing to the Data Rotate
register, so only one **OUT** is needed. Even so, it's often easier
register, so only one `OUT` is needed. Even so, it's often easier
and/or faster to simply have the CPU rotate the data of interest CL
times than to set the Data Rotate register. (Bear in mind that a single
**OUT** takes from 11 to 31 cycles on a 486—and longer if the VGA is
`OUT` takes from 11 to 31 cycles on a 486—and longer if the VGA is
sluggish at responding to OUTs, as many VGAs are.) If only the VGA could
rotate *latched* data, then there would be all sorts of useful
applications for rotation, but, sadly, only CPU data can be rotated.

View file

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

View file

@ -37,7 +37,7 @@ Write mode 3 is strange indeed, and its use is not immediately obvious.
The first time I encountered write mode 3, I understood immediately how
it functioned, but could think of very few useful applications for it.
As time passed, and as I came to understand the atrocious performance
characteristics of **OUT** instructions, and the importance of text and
characteristics of `OUT` instructions, and the importance of text and
pattern drawing as well, write mode 3 grew considerably in my
estimation. In fact, my esteem for this mode ultimately reached the
point where in the last major chunk of 16-color graphics code I wrote,
@ -57,7 +57,7 @@ first.)
That's what write mode 3 does—but what is it *for?* It turns out that
write mode 3 is excellent for a surprisingly large number of purposes,
because it makes it possible to avoid the bane of VGA performance,
**OUT**s. Some uses for write mode 3 include lines, circles, and solid
`OUT`s. Some uses for write mode 3 include lines, circles, and solid
and two-color pattern fills. Most importantly, write mode 3 is ideal for
transparent text; that is, it makes it possible to draw text in 16-color
graphics mode quickly without wiping out the background in the process.

View file

@ -20,7 +20,7 @@ without rotation characters could only be drawn on byte boundaries.
> ![](images/i.jpg)
> As I pointed out in Chapter 25, the CPU is perfectly capable of rotating
> the data itself, and it's often the case that that's more efficient. The
> problem with using the Data Rotate register is that the **OUT** that
> problem with using the Data Rotate register is that the `OUT` that
> sets that register is time-consuming, especially for proportional text,
> which requires a different rotation for each character. Also, if the
> code performs full-byte accesses to display memory—that is, if it
@ -68,7 +68,7 @@ character to the left of the byte boundary) and then draw the left
portions only of all 40 characters in write mode 3. Then the bit mask
could be set up for the right portion of each character, and the right
portions of all 40 characters could be drawn. The VGA's fast rotator
would be used to do all rotation, and the only **OUT**s required would
would be used to do all rotation, and the only `OUT`s required would
be those required to set the bit mask and data rotation. This technique
could well outperform single-character bit-mapped text drivers such as
the one in Listing 26.1 by a significant margin. Listing 26.2
@ -425,7 +425,7 @@ by ANDing in software. Even more significantly, we would have the CPU
combine adjacent characters into complete, rotated bytes whenever
possible, so that only one drawing operation would be required per byte
of display memory modified. By doing this, we would eliminate all
per-character **OUT**s, and would minimize display memory accesses,
per-character `OUT`s, and would minimize display memory accesses,
approximately doubling text-drawing speed.
As a final note, consider that non-transparent text could also be

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