Change bold code snippets to inline code blocks
This commit is contained in:
parent
f658bfbb40
commit
3c76d884ef
185 changed files with 1772 additions and 1772 deletions
2
01-02.md
2
01-02.md
|
|
@ -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:
|
||||
|
|
|
|||
10
01-03.md
10
01-03.md
|
|
@ -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.
|
||||
10
01-04.md
10
01-04.md
|
|
@ -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
|
||||
|
|
|
|||
4
02-01.md
4
02-01.md
|
|
@ -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
|
||||
|
|
|
|||
2
03-01.md
2
03-01.md
|
|
@ -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.
|
||||
22
03-03.md
22
03-03.md
|
|
@ -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
|
||||
|
|
|
|||
62
03-04.md
62
03-04.md
|
|
@ -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
|
||||
|
|
|
|||
22
03-05.md
22
03-05.md
|
|
@ -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.
|
||||
6
03-06.md
6
03-06.md
|
|
@ -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}
|
||||
|
|
|
|||
6
03-07.md
6
03-07.md
|
|
@ -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
|
||||
|
|
|
|||
18
03-08.md
18
03-08.md
|
|
@ -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
|
||||
|
|
|
|||
16
03-09.md
16
03-09.md
|
|
@ -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
|
||||
|
|
|
|||
10
03-10.md
10
03-10.md
|
|
@ -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
|
||||
|
|
|
|||
14
04-02.md
14
04-02.md
|
|
@ -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
|
||||
|
|
|
|||
4
04-03.md
4
04-03.md
|
|
@ -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?
|
||||
|
|
|
|||
36
04-04.md
36
04-04.md
|
|
@ -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.
|
|||

|
||||
|
||||
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.
|
||||
18
04-05.md
18
04-05.md
|
|
@ -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
|
||||
|
|
|
|||
16
04-07.md
16
04-07.md
|
|
@ -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.*
|
||||
|
|
|
|||
6
04-09.md
6
04-09.md
|
|
@ -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
|
||||
|
|
|
|||
2
04-10.md
2
04-10.md
|
|
@ -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.
|
||||
|
|
|
|||
8
05-01.md
8
05-01.md
|
|
@ -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.
|
||||
38
05-02.md
38
05-02.md
|
|
@ -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()`.
|
||||
|
||||

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

|
||||
|
||||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
24
05-04.md
24
05-04.md
|
|
@ -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
|
||||
|
|
|
|||
2
05-05.md
2
05-05.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
8
06-01.md
8
06-01.md
|
|
@ -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
|
||||
|
|
|
|||
58
06-02.md
58
06-02.md
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||
> 
|
||||
> 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.
|
||||
34
07-01.md
34
07-01.md
|
|
@ -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:
|
||||
|
||||
> 
|
||||
> 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.)
|
||||
|
||||
> 
|
||||
> 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!
|
||||
28
07-02.md
28
07-02.md
|
|
@ -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.)
|
||||
4
07-03.md
4
07-03.md
|
|
@ -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
|
||||
|
|
|
|||
4
07-04.md
4
07-04.md
|
|
@ -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
|
||||
|
|
|
|||
60
07-05.md
60
07-05.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> 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).
|
||||
|
||||
> 
|
||||
> 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
|
||||
|
|
|
|||
14
08-02.md
14
08-02.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> 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.
|
||||
8
08-03.md
8
08-03.md
|
|
@ -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.
|
|||

|
||||
|
||||
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**
|
||||
|
|
|
|||
6
08-04.md
6
08-04.md
|
|
@ -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,
|
||||
|
|
|
|||
6
08-05.md
6
08-05.md
|
|
@ -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.
|
||||
|
|
|
|||
32
09-01.md
32
09-01.md
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
50
09-02.md
50
09-02.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> 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`.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
> 
|
||||
> 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"
|
||||
|
|
|
|||
14
09-03.md
14
09-03.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> The difference between Listings 9.1 and 9.2 (which gives you more than a
|
||||
|
|
|
|||
4
09-05.md
4
09-05.md
|
|
@ -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.
|
||||
44
09-07.md
44
09-07.md
|
|
@ -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!)
|
|||

|
||||
|
||||
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.
|
||||
2
11-01.md
2
11-01.md
|
|
@ -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]
|
||||
|
|
|
|||
4
11-02.md
4
11-02.md
|
|
@ -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
|
||||
|
|
|
|||
12
11-03.md
12
11-03.md
|
|
@ -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.
|
||||
|
|
|
|||
20
11-04.md
20
11-04.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> 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
|
||||
|
|
|
|||
20
11-05.md
20
11-05.md
|
|
@ -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`.
|
||||
20
11-06.md
20
11-06.md
|
|
@ -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.
|
||||
|
|
|
|||
26
11-07.md
26
11-07.md
|
|
@ -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`.
|
||||
40
11-08.md
40
11-08.md
|
|
@ -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
|
|||
|
||||

|
||||
|
||||
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.
|
||||
18
12-01.md
18
12-01.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> Therefore, in real mode, the rule is to avoid using two registers to
|
||||
|
|
|
|||
14
12-02.md
14
12-02.md
|
|
@ -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
|
||||
|
|
|
|||
34
12-03.md
34
12-03.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> 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
|
||||
|
|
|
|||
6
12-04.md
6
12-04.md
|
|
@ -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
|
||||
|
|
|
|||
22
13-02.md
22
13-02.md
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
|
|
|||
48
13-03.md
48
13-03.md
|
|
@ -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*`.
|
||||
|
||||
> 
|
||||
> 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
|
||||
|
|
|
|||
10
13-04.md
10
13-04.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
|
|
|
|||
30
14-01.md
30
14-01.md
|
|
@ -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?
|
||||
|
|
|
|||
4
14-02.md
4
14-02.md
|
|
@ -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
|
||||
|
|
|
|||
8
14-03.md
8
14-03.md
|
|
@ -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:
|
||||
|
|
|
|||
12
14-04.md
12
14-04.md
|
|
@ -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.
|
||||
8
14-06.md
8
14-06.md
|
|
@ -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.
|
||||
|
|
|
|||
12
15-01.md
12
15-01.md
|
|
@ -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.)
|
||||
|
||||
|
|
|
|||
8
15-02.md
8
15-02.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
4
15-03.md
4
15-03.md
|
|
@ -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.
|
||||
|
|
|
|||
6
15-04.md
6
15-04.md
|
|
@ -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.
|
||||
|
|
|
|||
14
16-03.md
14
16-03.md
|
|
@ -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
|
||||
|
|
|
|||
16
16-04.md
16
16-04.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> **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
|
||||
|
|
|
|||
2
16-05.md
2
16-05.md
|
|
@ -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.
|
||||
|
|
|
|||
26
16-06.md
26
16-06.md
|
|
@ -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
|
||||
|
|
|
|||
6
16-07.md
6
16-07.md
|
|
@ -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
|
||||
|
|
|
|||
4
16-08.md
4
16-08.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
38
17-03.md
38
17-03.md
|
|
@ -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().`
|
||||
|
||||
> 
|
||||
> 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
|
||||
|
|
|
|||
2
17-04.md
2
17-04.md
|
|
@ -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
|
||||
|
|
|
|||
18
17-05.md
18
17-05.md
|
|
@ -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
|
||||
|
|
|
|||
4
17-06.md
4
17-06.md
|
|
@ -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
|
||||
|
|
|
|||
2
17-08.md
2
17-08.md
|
|
@ -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
|
||||
|
|
|
|||
2
18-05.md
2
18-05.md
|
|
@ -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
|
||||
|
|
|
|||
2
19-01.md
2
19-01.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
54
19-03.md
54
19-03.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> **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.
|
||||
|
|
|
|||
8
19-04.md
8
19-04.md
|
|
@ -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.
|
||||
|
|
|
|||
16
20-01.md
16
20-01.md
|
|
@ -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`.
|
||||
|
||||

|
||||
|
||||
|
|
@ -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`.
|
||||
|
||||
> 
|
||||
> 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
|
||||
|
|
|
|||
12
20-02.md
12
20-02.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
|
|
|
|||
18
20-03.md
18
20-03.md
|
|
@ -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,
|
||||
|
|
|
|||
18
20-04.md
18
20-04.md
|
|
@ -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
|
||||
|
|
|
|||
8
21-01.md
8
21-01.md
|
|
@ -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.
|
||||
|
||||

|
||||
|
|
|
|||
26
21-02.md
26
21-02.md
|
|
@ -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}
|
||||
|
|
|
|||
14
21-03.md
14
21-03.md
|
|
@ -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
|
||||
|
|
|
|||
14
21-04.md
14
21-04.md
|
|
@ -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**
|
||||
|
||||
|
|
|
|||
6
21-05.md
6
21-05.md
|
|
@ -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
|
||||
|
|
|
|||
16
22-01.md
16
22-01.md
|
|
@ -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
|
||||
|
|
|
|||
10
22-02.md
10
22-02.md
|
|
@ -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**
|
||||
|
|
|
|||
18
22-03.md
18
22-03.md
|
|
@ -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.
|
||||
|
|
|
|||
26
23-02.md
26
23-02.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> 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.
|
||||
10
23-05.md
10
23-05.md
|
|
@ -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
|
||||
|
|
|
|||
4
23-06.md
4
23-06.md
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
2
24-01.md
2
24-01.md
|
|
@ -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
|
||||
|
|
|
|||
20
24-03.md
20
24-03.md
|
|
@ -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.
|
||||
|
||||
> 
|
||||
> 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
|
||||
|
|
|
|||
6
25-01.md
6
25-01.md
|
|
@ -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.
|
||||
|
|
|
|||
14
25-06.md
14
25-06.md
|
|
@ -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.
|
||||
|
|
|
|||
4
26-01.md
4
26-01.md
|
|
@ -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.
|
||||
|
|
|
|||
6
26-03.md
6
26-03.md
|
|
@ -20,7 +20,7 @@ without rotation characters could only be drawn on byte boundaries.
|
|||
> 
|
||||
> As I pointed out in Chapter 25, the CPU is perfectly capable of rotating
|
||||
> the data itself, and it's often the case that that's more efficient. The
|
||||
> problem with using the Data Rotate register is that the **OUT** that
|
||||
> 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
Loading…
Reference in a new issue