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.
|
and be able to handle any size file at all.
|
||||||
|
|
||||||
Sounds good, eh? Listing 1.1 shows an implementation of this approach.
|
Sounds good, eh? Listing 1.1 shows an implementation of this approach.
|
||||||
Listing 1.1 uses C's **read()** function to read a single byte, adds the
|
Listing 1.1 uses C's `read()` function to read a single byte, adds the
|
||||||
byte into the checksum value, and loops back to handle the next byte
|
byte into the checksum value, and loops back to handle the next byte
|
||||||
until the end of the file is reached. The code is compact, easy to
|
until the end of the file is reached. The code is compact, easy to
|
||||||
write, and functions perfectly—with one slight hitch:
|
write, and functions perfectly—with one slight hitch:
|
||||||
|
|
|
||||||
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}
|
#### Know the Territory {#Heading9}
|
||||||
|
|
||||||
Just why is Listing 1.1 so slow? In a word: overhead. The C library
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
You can verify this for yourself by watching the code with a debugger or
|
||||||
using a code profiler, but take my word for it: There's a great deal of
|
using a code profiler, but take my word for it: There's a great deal of
|
||||||
|
|
@ -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
|
reading them from memory rather than calling DOS. Let's try using stream
|
||||||
I/O and see what happens.
|
I/O and see what happens.
|
||||||
|
|
||||||
Listing 1.4 is similar to Listing 1.1, but uses **fopen()** and
|
Listing 1.4 is similar to Listing 1.1, but uses `fopen()` and
|
||||||
**getc()** (rather than **open()** and **read()**) to access the file
|
`getc()` (rather than `open()` and `read()`) to access the file
|
||||||
being checksummed. The results confirm our theories splendidly, and
|
being checksummed. The results confirm our theories splendidly, and
|
||||||
validate our new design. As shown in Table 1.1, Listing 1.4 runs more
|
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
|
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
|
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
|
slightly different but pretty much interchangeable, and yet in this
|
||||||
application the performance difference between the two is about the same
|
application the performance difference between the two is about the same
|
||||||
as that between a 4.77 MHz PC and a 16 MHz 386.
|
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
|
Listing 1.4 is good, but let's see if there are other—perhaps less
|
||||||
obvious—ways to get the same results faster. Let's start by considering
|
obvious—ways to get the same results faster. Let's start by considering
|
||||||
why Listing 1.4 is so much better than Listing 1.1. Like **read()**,
|
why Listing 1.4 is so much better than Listing 1.1. Like `read()`,
|
||||||
**getc()** calls DOS to read from the file; the speed improvement of
|
`getc()` calls DOS to read from the file; the speed improvement of
|
||||||
Listing 1.4 over Listing 1.1 occurs because **getc()** eads many bytes
|
Listing 1.4 over Listing 1.1 occurs because `getc()` eads many bytes
|
||||||
at once via DOS, then manages those bytes for us. That's faster than
|
at once via DOS, then manages those bytes for us. That's faster than
|
||||||
reading them one at a time using **read()**—but there's no reason to
|
reading them one at a time using `read()`—but there's no reason to
|
||||||
think that it's faster than having our program read and manage blocks
|
think that it's faster than having our program read and manage blocks
|
||||||
itself. Easier, yes, but not faster.
|
itself. Easier, yes, but not faster.
|
||||||
|
|
||||||
Consider this: Every invocation of **getc()** involves pushing a
|
Consider this: Every invocation of `getc()` involves pushing a
|
||||||
parameter, executing a call to the C library function, getting the
|
parameter, executing a call to the C library function, getting the
|
||||||
parameter (in the C library code), looking up information about the
|
parameter (in the C library code), looking up information about the
|
||||||
desired stream, unbuffering the next byte from the stream, and returning
|
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
|
much different from the original, and in fact still contains exactly the
|
||||||
same number of instructions, the performance of the entire subroutine
|
same number of instructions, the performance of the entire subroutine
|
||||||
improved by about 10 percent from just this one change. (Incidentally,
|
improved by about 10 percent from just this one change. (Incidentally,
|
||||||
that wasn't the end of the optimization; I eliminated the **DEC** and
|
that wasn't the end of the optimization; I eliminated the `DEC` and
|
||||||
**JNJ** instructions by expanding the four iterations of the loop—but
|
`JNJ` instructions by expanding the four iterations of the loop—but
|
||||||
that's a tale for another chapter.)
|
that's a tale for another chapter.)
|
||||||
|
|
||||||
The point is this: To write truly superior assembly programs, you need
|
The point is this: To write truly superior assembly programs, you need
|
||||||
|
|
|
||||||
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.)
|
one billionth of a second, and is abbreviated ns.)
|
||||||
|
|
||||||
Listing 3.1 shows 8253-based timer software, consisting of three
|
Listing 3.1 shows 8253-based timer software, consisting of three
|
||||||
subroutines: **ZTimerOn, ZTimerOff**, and **ZTimerReport**. For the
|
subroutines: `ZTimerOn`, `ZTimerOff`, and `ZTimerReport`. For the
|
||||||
remainder of this book, I'll refer to these routines collectively as the
|
remainder of this book, I'll refer to these routines collectively as the
|
||||||
"Zen timer." C-callable versions of the two precision Zen timers are
|
"Zen timer." C-callable versions of the two precision Zen timers are
|
||||||
presented in Chapter K on the companion CD-ROM.
|
presented in Chapter K on the companion CD-ROM.
|
||||||
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}
|
#### Starting the Zen Timer {#Heading6}
|
||||||
|
|
||||||
**ZTimerOn** is called at the start of a segment of code to be timed.
|
`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` saves the context of the calling code, disables interrupts,
|
||||||
sets timer 0 of the 8253 to mode 2 (divide-by-N mode), sets the initial
|
sets timer 0 of the 8253 to mode 2 (divide-by-N mode), sets the initial
|
||||||
timer count to 0, restores the context of the calling code, and returns.
|
timer count to 0, restores the context of the calling code, and returns.
|
||||||
(I'd like to note that while Intel's documentation for the 8253 seems to
|
(I'd like to note that while Intel's documentation for the 8253 seems to
|
||||||
|
|
@ -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
|
in actual practice, timers seem to reset to 0 as soon as they're
|
||||||
loaded.)
|
loaded.)
|
||||||
|
|
||||||
Two aspects of **ZTimerOn** are worth discussing further. One point of
|
Two aspects of `ZTimerOn` are worth discussing further. One point of
|
||||||
interest is that **ZTimerOn** disables interrupts. (**ZTimerOff** later
|
interest is that `ZTimerOn` disables interrupts. (`ZTimerOff` later
|
||||||
restores interrupts to the state they were in when **ZTimerOn** was
|
restores interrupts to the state they were in when `ZTimerOn` was
|
||||||
called.) Were interrupts not disabled by **ZTimerOn**, keyboard, mouse,
|
called.) Were interrupts not disabled by `ZTimerOn`, keyboard, mouse,
|
||||||
timer, and other interrupts could occur during the timing interval, and
|
timer, and other interrupts could occur during the timing interval, and
|
||||||
the time required to service those interrupts would incorrectly and
|
the time required to service those interrupts would incorrectly and
|
||||||
erratically appear to be part of the execution time of the code being
|
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
|
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
|
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.
|
enable interrupts during that time.
|
||||||
|
|
||||||
### Time and the PC {#Heading7}
|
### 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.
|
some small inaccuracy into the system clock time whenever it is called.
|
||||||
To understand why this is so, we need to examine the way in which both
|
To understand why this is so, we need to examine the way in which both
|
||||||
the 8253 and the PC's system clock (which keeps the current time) work.
|
the 8253 and the PC's system clock (which keeps the current time) work.
|
||||||
|
|
@ -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.
|
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
|
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
|
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
|
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
|
BIOS data area at address 0000:046C (all addresses in this book are
|
||||||
given in segment:offset hexadecimal pairs); this count turns over once
|
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.
|
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
|
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
|
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
|
while the Zen timer runs, so the timer interrupt cannot be recognized
|
||||||
until the Zen timer stops and enables interrupts.) If an IRQ0 interrupt
|
until the Zen timer stops and enables interrupts.) If an IRQ0 interrupt
|
||||||
is pending, then timer 0 has turned over and generated a timer
|
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
|
order to allow for the longest possible period—about 54 ms—before timer
|
||||||
0 reaches 0 and generates the timer interrupt.
|
0 reaches 0 and generates the timer interrupt.
|
||||||
|
|
||||||
|
|
@ -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
|
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
|
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
|
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
|
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.
|
(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}
|
### Stopping the Zen Timer {#Heading8}
|
||||||
|
|
||||||
At some point after **ZTimerOn** is called, **ZTimerOff** must always be
|
At some point after `ZTimerOn` is called, `ZTimerOff` must always be
|
||||||
called to mark the end of the timing interval. **ZTimerOff** saves the
|
called to mark the end of the timing interval. `ZTimerOff` saves the
|
||||||
context of the calling program, latches and reads the timer 0 count,
|
context of the calling program, latches and reads the timer 0 count,
|
||||||
converts that count from the countdown value that the timer maintains to
|
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
|
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
|
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.
|
that the timer overflowed if there is indeed a pending timer interrupt.
|
||||||
|
|
||||||
After that, **ZTimerOff** executes just the overhead code of
|
After that, `ZTimerOff` executes just the overhead code of
|
||||||
**ZTimerOn** and **ZTimerOff** 16 times, and averages and saves the
|
`ZTimerOn` and `ZTimerOff` 16 times, and averages and saves the
|
||||||
results in order to determine how many of the counts in the timing
|
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
|
result just obtained were incurred by the overhead of the Zen timer
|
||||||
rather than by the code being timed.
|
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
|
including the state of the interrupt flag that was in effect when
|
||||||
**ZTimerOn** was called to start timing, and returns.
|
`ZTimerOn` was called to start timing, and returns.
|
||||||
|
|
||||||
One interesting aspect of **ZTimerOff** is the manner in which timer 0
|
One interesting aspect of `ZTimerOff` is the manner in which timer 0
|
||||||
is stopped in order to read the timer count. We don't actually have to
|
is stopped in order to read the timer count. We don't actually have to
|
||||||
stop timer 0 to read the count; the 8253 provides a special latched read
|
stop timer 0 to read the count; the 8253 provides a special latched read
|
||||||
feature for the specific purpose of reading the count while a time is
|
feature for the specific purpose of reading the count while a time is
|
||||||
|
|
@ -102,48 +102,48 @@ without breaking stride.
|
||||||
|
|
||||||
### Reporting Timing Results {#Heading9}
|
### Reporting Timing Results {#Heading9}
|
||||||
|
|
||||||
**ZTimerReport** may be called to display timing results at any time
|
`ZTimerReport` may be called to display timing results at any time
|
||||||
after both **ZTimerOn** and **ZTimerOff** have been called.
|
after both `ZTimerOn` and `ZTimerOff` have been called.
|
||||||
**ZTimerReport** first checks to see whether the timer overflowed
|
`ZTimerReport` first checks to see whether the timer overflowed
|
||||||
(counted down to 0 and turned over) before **ZTimerOff** was called; if
|
(counted down to 0 and turned over) before `ZTimerOff` was called; if
|
||||||
overflow did occur, **ZTimerOff** prints a message to that effect and
|
overflow did occur, `ZTimerOff` prints a message to that effect and
|
||||||
returns. Otherwise, **ZTimerReport** subtracts the reference count
|
returns. Otherwise, `ZTimerReport` subtracts the reference count
|
||||||
(representing the overhead of the Zen timer) from the count measured
|
(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
|
from timer counts to microseconds, and prints the resulting time in
|
||||||
microseconds to the standard output.
|
microseconds to the standard output.
|
||||||
|
|
||||||
Note that **ZTimerReport** need not be called immediately after
|
Note that `ZTimerReport` need not be called immediately after
|
||||||
**ZTimerOff**. In fact, after a given call to **ZTimerOff,
|
`ZTimerOff`. In fact, after a given call to `ZTimerOff,
|
||||||
ZTimerReport** can be called at any time right up until the next call to
|
ZTimerReport` can be called at any time right up until the next call to
|
||||||
**ZTimerOn**.
|
`ZTimerOn`.
|
||||||
|
|
||||||
You may want to use the Zen timer to measure several portions of a
|
You may want to use the Zen timer to measure several portions of a
|
||||||
program while it executes normally, in which case it may not be
|
program while it executes normally, in which case it may not be
|
||||||
desirable to have the text printed by **ZTimerReport** interfere with
|
desirable to have the text printed by `ZTimerReport` interfere with
|
||||||
the program's normal display. There are many ways to deal with this. One
|
the program's normal display. There are many ways to deal with this. One
|
||||||
approach is removal of the invocations of the DOS print string function
|
approach is removal of the invocations of the DOS print string function
|
||||||
(INT 21H with AH equal to 9) from **ZTimerReport**, instead running the
|
(INT 21H with AH equal to 9) from `ZTimerReport`, instead running the
|
||||||
program under a debugger that supports screen flipping (such as Turbo
|
program under a debugger that supports screen flipping (such as Turbo
|
||||||
Debugger or CodeView), placing a breakpoint at the start of
|
Debugger or CodeView), placing a breakpoint at the start of
|
||||||
**ZTimerReport**, and directly observing the count in microseconds as
|
`ZTimerReport`, and directly observing the count in microseconds as
|
||||||
**ZTimerReport** calculates it.
|
`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
|
result at some safe location in memory, such as an unused portion of the
|
||||||
BIOS data area.
|
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.
|
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
|
Similarly, many debuggers can be run from a remote terminal via a serial
|
||||||
link.
|
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.
|
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
|
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
|
would automatically be directed. This device driver could send the
|
||||||
result anywhere you might desire. The result might go to the secondary
|
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
|
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}
|
### Notes on the Zen Timer {#Heading10}
|
||||||
|
|
||||||
The Zen timer subroutines are designed to be near-called from assembly
|
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
|
subroutines can, however, be called from any assembly or high-level
|
||||||
language code that generates OBJ files that are compatible with the
|
language code that generates OBJ files that are compatible with the
|
||||||
Microsoft linker, simply by modifying the segment that the timer code
|
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
|
If you do change the Zen timer routines to far procedures in order to
|
||||||
call them from code running in another segment, be sure to make *all*
|
call them from code running in another segment, be sure to make *all*
|
||||||
the Zen timer routines far, including **ReferenceZTimerOn** and
|
the Zen timer routines far, including `ReferenceZTimerOn` and
|
||||||
**ReferenceZTimerOff**. (You'll have to put **FAR PTR** overrides on the
|
`ReferenceZTimerOff`. (You'll have to put `FAR PTR` overrides on the
|
||||||
calls from **ZTimerOff** to the latter two routines if you do make them
|
calls from `ZTimerOff` to the latter two routines if you do make them
|
||||||
far.) If the reference routines aren't the same type—near or far—as the
|
far.) If the reference routines aren't the same type—near or far—as the
|
||||||
other routines, they won't reflect the true overhead incurred by
|
other routines, they won't reflect the true overhead incurred by
|
||||||
starting and stopping the Zen timer.
|
starting and stopping the Zen timer.
|
||||||
|
|
@ -76,9 +76,9 @@ computers.
|
||||||
Listing 3.2 shows a test-bed program for measuring code performance with
|
Listing 3.2 shows a test-bed program for measuring code performance with
|
||||||
the Zen timer. This program sets DS equal to CS (for reasons we'll
|
the Zen timer. This program sets DS equal to CS (for reasons we'll
|
||||||
discuss shortly), includes the code to be measured from the file
|
discuss shortly), includes the code to be measured from the file
|
||||||
TESTCODE, and calls **ZTimerReport** to display the timing results.
|
TESTCODE, and calls `ZTimerReport` to display the timing results.
|
||||||
Consequently, the code being measured should be in the file TESTCODE,
|
Consequently, the code being measured should be in the file TESTCODE,
|
||||||
and should contain calls to **ZTimerOn** and **ZTimerOff** .
|
and should contain calls to `ZTimerOn` and `ZTimerOff` .
|
||||||
|
|
||||||
**LISTING 3.2 PZTEST.ASM**
|
**LISTING 3.2 PZTEST.ASM**
|
||||||
|
|
||||||
|
|
@ -124,10 +124,10 @@ Code ends
|
||||||
|
|
||||||
Listing 3.3 shows some sample code to be timed. This listing measures
|
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
|
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,
|
`MemVar` . Note that Listing 3.3 calls `ZTimerOn` to start timing,
|
||||||
performs 1,000 **MOV** instructions in a row, and calls **ZTimerOff** to
|
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
|
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.
|
after the code in Listing 3.3 has been run.
|
||||||
|
|
||||||
**LISTING 3.3 LST3-3.ASM**
|
**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
|
It's worth noting that Listing 3.3 begins by jumping around the memory
|
||||||
variable **MemVar**. This approach lets us avoid reproducing Listing 3.2
|
variable `MemVar`. This approach lets us avoid reproducing Listing 3.2
|
||||||
in its entirety for each code fragment we want to measure; by defining
|
in its entirety for each code fragment we want to measure; by defining
|
||||||
any needed data right in the code segment and jumping around that data,
|
any needed data right in the code segment and jumping around that data,
|
||||||
each listing becomes self-contained and can be plugged directly into
|
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
|
being timed. Note that only after the initial jump is performed in
|
||||||
Listing 3.3 is the Zen timer started, since we don't want to include the
|
Listing 3.3 is the Zen timer started, since we don't want to include the
|
||||||
execution time of start-up code in the timing interval. That's why the
|
execution time of start-up code in the timing interval. That's why the
|
||||||
calls to **ZTimerOn** and **ZTimerOff** are in TESTCODE, not in
|
calls to `ZTimerOn` and `ZTimerOff` are in TESTCODE, not in
|
||||||
PZTEST.ASM; this way, we have full control over which portion of
|
PZTEST.ASM; this way, we have full control over which portion of
|
||||||
TESTCODE is timed, and we can keep set-up code and the like out of the
|
TESTCODE is timed, and we can keep set-up code and the like out of the
|
||||||
timing interval.
|
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
|
noise in the timing process—between dynamic RAM refresh, the prefetch
|
||||||
queue, and the internal state of the processor at the start of
|
queue, and the internal state of the processor at the start of
|
||||||
timing—for that last digit to have any significance.) Given the test
|
timing—for that last digit to have any significance.) Given the test
|
||||||
PC's 4.77 MHz clock, this works out to about 17 cycles per **MOV**,
|
PC's 4.77 MHz clock, this works out to about 17 cycles per `MOV`,
|
||||||
which is actually a good bit longer than Intel's specified 10-cycle
|
which is actually a good bit longer than Intel's specified 10-cycle
|
||||||
execution time for this instruction. (See the MASM or TASM
|
execution time for this instruction. (See the MASM or TASM
|
||||||
documentation, or Intel's processor reference manuals, for official
|
documentation, or Intel's processor reference manuals, for official
|
||||||
|
|
@ -127,8 +127,8 @@ pztime <filename>
|
||||||
In fact, that's exactly how I timed each of the listings in this book.
|
In fact, that's exactly how I timed each of the listings in this book.
|
||||||
Code fragments you write yourself can be timed in just the same way. If
|
Code fragments you write yourself can be timed in just the same way. If
|
||||||
you wish to time code directly in place in your programs, rather than in
|
you wish to time code directly in place in your programs, rather than in
|
||||||
the test-bed program of Listing 3.2, simply insert calls to **ZTimerOn,
|
the test-bed program of Listing 3.2, simply insert calls to `ZTimerOn,
|
||||||
ZTimerOff**, and **ZTimerReport** in the appropriate places and link
|
ZTimerOff`, and `ZTimerReport` in the appropriate places and link
|
||||||
PZTIMER to your program.
|
PZTIMER to your program.
|
||||||
|
|
||||||
### The Long-Period Zen Timer {#Heading12}
|
### 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.
|
reading that's off by 54 ms, and leave it at that.
|
||||||
|
|
||||||
I've set up Listing 3.5 so that it can assemble to either use or not use
|
I've set up Listing 3.5 so that it can assemble to either use or not use
|
||||||
the undocumented timer-stopping feature, as you please. The **PS2**
|
the undocumented timer-stopping feature, as you please. The `PS2`
|
||||||
equate selects between the two modes of operation. If **PS2** is 1 (as
|
equate selects between the two modes of operation. If `PS2` is 1 (as
|
||||||
it is in Listing 3.5), then the latch-and-read method is used; if
|
it is in Listing 3.5), then the latch-and-read method is used; if
|
||||||
**PS2** is 0, then the undocumented timer-stop approach is used. The
|
`PS2` is 0, then the undocumented timer-stop approach is used. The
|
||||||
latch-and-read method will work on all PC-compatible computers, but may
|
latch-and-read method will work on all PC-compatible computers, but may
|
||||||
occasionally produce results that are incorrect by 54 ms. The timer-stop
|
occasionally produce results that are incorrect by 54 ms. The timer-stop
|
||||||
approach avoids synchronization problems, but doesn't work on all
|
approach avoids synchronization problems, but doesn't work on all
|
||||||
|
|
|
||||||
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.
|
turn seriously affect your computer's operation until the next reboot.
|
||||||
In non-8253-compatible systems, I've observed not only wildly incorrect
|
In non-8253-compatible systems, I've observed not only wildly incorrect
|
||||||
timing results, but also failure of a diskette drive to operate properly
|
timing results, but also failure of a diskette drive to operate properly
|
||||||
after the long-period Zen timer with **PS2** set to 0 has run, so be
|
after the long-period Zen timer with `PS2` set to 0 has run, so be
|
||||||
alert for signs of trouble if you do set **PS2** to 0.
|
alert for signs of trouble if you do set `PS2` to 0.
|
||||||
|
|
||||||
Rebooting should clear up any timer-related problems of the sort
|
Rebooting should clear up any timer-related problems of the sort
|
||||||
described above. (This gives us another reason to reboot at the end of
|
described above. (This gives us another reason to reboot at the end of
|
||||||
each code-timing session.) You should *immediately* reboot and set the
|
each code-timing session.) You should *immediately* reboot and set the
|
||||||
**PS2** equate to 1 if you get erratic or obviously incorrect results
|
`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
|
with the long-period Zen timer when `PS2` is set to 0. If you want to
|
||||||
set **PS2** to 0, it would be a good idea to time a few of the listings
|
set `PS2` to 0, it would be a good idea to time a few of the listings
|
||||||
in this book with **PS2** set first to 1 and then to 0, to make sure
|
in this book with `PS2` set first to 1 and then to 0, to make sure
|
||||||
that the results match. If they're consistently different, you should
|
that the results match. If they're consistently different, you should
|
||||||
set **PS2** to 1.
|
set `PS2` to 1.
|
||||||
|
|
||||||
While the the non-PS/2 version is more dangerous than the PS/2 version,
|
While the the non-PS/2 version is more dangerous than the PS/2 version,
|
||||||
it also produces more accurate results when it does work. If you have a
|
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
|
non-PS/2 PC-compatible computer, the choice between the two timing
|
||||||
approaches is yours.
|
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
|
repeat each code-timing run several times before relying on the results
|
||||||
to be accurate to more than 54 ms, since variations may result from the
|
to be accurate to more than 54 ms, since variations may result from the
|
||||||
possible lack of synchronization between the timer 0 count and the BIOS
|
possible lack of synchronization between the timer 0 count and the BIOS
|
||||||
|
|
@ -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.
|
Listing 3.6 shows a test-bed program for the long-period Zen timer.
|
||||||
While this program is similar to Listing 3.2, it's worth noting that
|
While this program is similar to Listing 3.2, it's worth noting that
|
||||||
Listing 3.6 waits for a few seconds before calling **ZTimerOn**, thereby
|
Listing 3.6 waits for a few seconds before calling `ZTimerOn`, thereby
|
||||||
allowing any pending keyboard interrupts to be processed. Since
|
allowing any pending keyboard interrupts to be processed. Since
|
||||||
interrupts must be left on in order to time periods longer than 54 ms,
|
interrupts must be left on in order to time periods longer than 54 ms,
|
||||||
the interrupts generated by keystrokes (including the upstroke of the
|
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
|
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
|
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
|
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
|
the BIOS code that handles the 18.2 timer interrupts that occur each
|
||||||
second.
|
second.
|
||||||
|
|
||||||
Note that the command can take as much as 10 minutes to finish on a slow
|
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
|
PC if you are using MASM, with most of that time spent assembling
|
||||||
Listing 3.8. Why? Because MASM is notoriously slow at 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}
|
### Using the Zen Timer from C {#Heading15}
|
||||||
|
|
||||||
The Zen timer can be used to measure code performance when programming
|
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
|
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
|
designed to be called from assembly language; some relatively minor
|
||||||
modifications are required before the **ZTimerOn** (start timer),
|
modifications are required before the `ZTimerOn` (start timer),
|
||||||
**ZTimerOff** (stop timer), and **ZTimerReport** (display timing
|
`ZTimerOff` (stop timer), and `ZTimerReport` (display timing
|
||||||
results) routines can be called from C. There are two separate cases to
|
results) routines can be called from C. There are two separate cases to
|
||||||
be dealt with here: small code model and large; I'll tackle the simpler
|
be dealt with here: small code model and large; I'll tackle the simpler
|
||||||
one, the small code model, first.
|
one, the small code model, first.
|
||||||
|
|
||||||
Altering the Zen timer for linking to a small code model C program
|
Altering the Zen timer for linking to a small code model C program
|
||||||
involves the following steps: **C** hange **ZTimerOn** to
|
involves the following steps: `C` hange `ZTimerOn` to
|
||||||
**\_ZTimerOn**, change **ZTimerOff** to **\_ZTimerOff**, change
|
`_ZTimerOn`, change `ZTimerOff` to `_ZTimerOff`, change
|
||||||
**ZTimerReport** to **\_ZTimerReport**, and change **Code** to
|
`ZTimerReport` to `_ZTimerReport`, and change `Code` to
|
||||||
**\_TEXT** . Figure 3.2 shows the line numbers and new states of all
|
`_TEXT` . Figure 3.2 shows the line numbers and new states of all
|
||||||
lines from Listing 3.1 that must be changed. These changes convert the
|
lines from Listing 3.1 that must be changed. These changes convert the
|
||||||
code to use C-style external label names and the small model C code
|
code to use C-style external label names and the small model C code
|
||||||
segment. (In C++, use the "C" specifier, as in
|
segment. (In C++, use the "C" specifier, as in
|
||||||
|
|
|
||||||
10
03-10.md
10
03-10.md
|
|
@ -10,7 +10,7 @@ chapter: '03'
|
||||||
pages: 070-073
|
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.)
|
doesn't occur, and the linker can find the routines' C-style names.)
|
||||||
|
|
||||||
That's all it takes; after doing this, you'll be able to use the Zen
|
That's all it takes; after doing this, you'll be able to use the Zen
|
||||||
|
|
@ -59,8 +59,8 @@ push cs
|
||||||
call near ptr ReferenceZTimerOn
|
call near ptr ReferenceZTimerOn
|
||||||
```
|
```
|
||||||
|
|
||||||
(and likewise for **ReferenceZTimerOff** ), which works because
|
(and likewise for `ReferenceZTimerOff` ), which works because
|
||||||
**ReferenceZTimerOn** is in the same segment as the calling code. This
|
`ReferenceZTimerOn` is in the same segment as the calling code. This
|
||||||
is normally a great optimization, being both smaller and faster than a
|
is normally a great optimization, being both smaller and faster than a
|
||||||
far call. However, it's not so great for the Zen
|
far call. However, it's not so great for the Zen
|
||||||
|
|
||||||
|
|
@ -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
|
timer, because our purpose in calling the reference timing code is to
|
||||||
determine exactly how much time is taken by overhead code—including the
|
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
|
to push/near call pairs within the Zen timer module, TASM makes it
|
||||||
impossible to emulate exactly the overhead of the Zen timer, and makes
|
impossible to emulate exactly the overhead of the Zen timer, and makes
|
||||||
timings slightly (about 16 cycles on a 386) less accurate.
|
timings slightly (about 16 cycles on a 386) less accurate.
|
||||||
|
|
||||||
What's the solution? Put the **NOSMART** directive at the start of the
|
What's the solution? Put the `NOSMART` directive at the start of the
|
||||||
Zen timer code. This directive instructs TASM to turn off all
|
Zen timer code. This directive instructs TASM to turn off all
|
||||||
optimizations, including converting far calls to push/near call pairs.
|
optimizations, including converting far calls to push/near call pairs.
|
||||||
By the way, there is, to the best of my knowledge, no such problem with
|
By the way, there is, to the best of my knowledge, no such problem with
|
||||||
|
|
|
||||||
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]
|
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
|
```nasm
|
||||||
mov al,byte ptr [MemVar]
|
mov al,byte ptr [MemVar]
|
||||||
```
|
```
|
||||||
|
|
||||||
takes to read the byte at address **MemVar.** (Actually, the difference
|
takes to read the byte at address `MemVar`. (Actually, the difference
|
||||||
between the two isn't very likely to be exactly 4 cycles, for reasons
|
between the two isn't very likely to be exactly 4 cycles, for reasons
|
||||||
that will become clear once we discuss the prefetch queue and dynamic
|
that will become clear once we discuss the prefetch queue and dynamic
|
||||||
RAM refresh cycle-eaters later in this chapter.)
|
RAM refresh cycle-eaters later in this chapter.)
|
||||||
|
|
@ -88,7 +88,7 @@ add byte ptr [MemVar],al
|
||||||
|
|
||||||
String instructions can suffer from the 8-bit bus cycle-eater to a
|
String instructions can suffer from the 8-bit bus cycle-eater to a
|
||||||
greater extent than other instructions. Believe it or not, a single
|
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!
|
accesses x 4 cycles, or *524,280 cycles* to the 8-bit bus cycle-eater!
|
||||||
In other words, one 8088 instruction (admittedly, an instruction that
|
In other words, one 8088 instruction (admittedly, an instruction that
|
||||||
does a great deal) can take over one-tenth of a second longer on an 8088
|
does a great deal) can take over one-tenth of a second longer on an 8088
|
||||||
|
|
@ -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
|
counter, runs in 10.03 s per loop. That's 20 percent faster than the
|
||||||
12.05 µs per loop execution time of Listing 4.2, which uses a word-sized
|
12.05 µs per loop execution time of Listing 4.2, which uses a word-sized
|
||||||
counter. Why the difference in execution times? Simply because each
|
counter. Why the difference in execution times? Simply because each
|
||||||
word-sized **DEC** performs 4 byte-sized memory accesses (two to read
|
word-sized `DEC` performs 4 byte-sized memory accesses (two to read
|
||||||
the word-sized operand and two to write the result back to memory),
|
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.
|
in all.
|
||||||
|
|
||||||
**LISTING 4.1 LST4-1.ASM**
|
**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
|
after working code so that the timing results are applicable to
|
||||||
real-world programming. In Listings 4.1 and 4.2, for example, I could
|
real-world programming. In Listings 4.1 and 4.2, for example, I could
|
||||||
have shown a still greater advantage for byte-sized operands simply by
|
have shown a still greater advantage for byte-sized operands simply by
|
||||||
performing 1,000 **DEC** instructions in a row, with no branching at
|
performing 1,000 `DEC` instructions in a row, with no branching at
|
||||||
all. However, **DEC** instructions don't exist in a vacuum, so in the
|
all. However, `DEC` instructions don't exist in a vacuum, so in the
|
||||||
listings I used code that both decremented the counter and tested the
|
listings I used code that both decremented the counter and tested the
|
||||||
result. The difference is that between decrementing a memory location
|
result. The difference is that between decrementing a memory location
|
||||||
(simply an instruction) and using a loop counter (a functional
|
(simply an instruction) and using a loop counter (a functional
|
||||||
|
|
|
||||||
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
|
byte-sized memory accesses in order to handle a word-sized memory
|
||||||
operand.) However, Listing 4.3 is considerably faster because it expends
|
operand.) However, Listing 4.3 is considerably faster because it expends
|
||||||
only 4 additional cycles to read the second byte of each word, while
|
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.
|
the second byte of each word.
|
||||||
|
|
||||||
**LISTING 4.3 LST4-3.ASM**
|
**LISTING 4.3 LST4-3.ASM**
|
||||||
|
|
@ -166,7 +166,7 @@ shr ax,1
|
||||||
shr ax,1
|
shr ax,1
|
||||||
```
|
```
|
||||||
|
|
||||||
should execute in 10 cycles, since each **SHR** takes 2 cycles to
|
should execute in 10 cycles, since each `SHR` takes 2 cycles to
|
||||||
execute, according to Intel's specifications. Those specifications
|
execute, according to Intel's specifications. Those specifications
|
||||||
contain Intel's official instruction execution times, but in this
|
contain Intel's official instruction execution times, but in this
|
||||||
case—and in many others—the specifications are drastically wrong. Why?
|
case—and in many others—the specifications are drastically wrong. Why?
|
||||||
|
|
|
||||||
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}
|
#### 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
|
long. That means that it can never execute in less than 24 cycles even
|
||||||
if the 4-byte prefetch queue is full when it starts, since 6 instruction
|
if the 4-byte prefetch queue is full when it starts, since 6 instruction
|
||||||
bytes would still remain to be fetched, at 4 cycles per fetch. If the
|
bytes would still remain to be fetched, at 4 cycles per fetch. If the
|
||||||
|
|
@ -90,17 +90,17 @@ that), and some of the other prefetch states may well produce distinctly
|
||||||
different results.
|
different results.
|
||||||
|
|
||||||
For example, consider the code in Listings 4.5 and 4.6. Listing 4.5
|
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,
|
always empty, execution time should work out to about 4 cycles per byte,
|
||||||
or 8 cycles per **SHR,** as shown in Figure 4.3. (Figure 4.3 illustrates
|
or 8 cycles per `SHR`, as shown in Figure 4.3. (Figure 4.3 illustrates
|
||||||
the relationship between instruction fetching and execution in a
|
the relationship between instruction fetching and execution in a
|
||||||
simplified way, and is not intended to show the exact timings of 8088
|
simplified way, and is not intended to show the exact timings of 8088
|
||||||
operations.) That's quite a contrast to the official 2-cycle execution
|
operations.) That's quite a contrast to the official 2-cycle execution
|
||||||
time of **SHR**. In fact, the Zen timer reports that Listing 4.5
|
time of `SHR`. In fact, the Zen timer reports that Listing 4.5
|
||||||
executes in 1.81µs per byte, or slightly *more* than 4 cycles per byte.
|
executes in 1.81µs per byte, or slightly *more* than 4 cycles per byte.
|
||||||
(The extra time is the result of the dynamic RAM refresh cycle-eater,
|
(The extra time is the result of the dynamic RAM refresh cycle-eater,
|
||||||
which we'll discuss shortly.) Going by Listing 4.5, we would conclude
|
which we'll discuss shortly.) Going by Listing 4.5, we would conclude
|
||||||
that the "true" execution time of **SHR** is 8.64 cycles.
|
that the "true" execution time of `SHR` is 8.64 cycles.
|
||||||
|
|
||||||
**LISTING 4.5 LST4-5.ASM**
|
**LISTING 4.5 LST4-5.ASM**
|
||||||
|
|
||||||
|
|
@ -138,27 +138,27 @@ that the "true" execution time of **SHR** is 8.64 cycles.
|
||||||

|
Listing 4.5.*](images/04-03.jpg)
|
||||||
|
|
||||||
Now let's examine Listing 4.6. Here each **SHR** follows a **MUL**
|
Now let's examine Listing 4.6. Here each `SHR` follows a `MUL`
|
||||||
instruction. Since **MUL** instructions take so long to execute that the
|
instruction. Since `MUL` instructions take so long to execute that the
|
||||||
prefetch queue is always full when they finish, each **SHR** should be
|
prefetch queue is always full when they finish, each `SHR` should be
|
||||||
ready and waiting in the prefetch queue when the preceding **MUL** ends.
|
ready and waiting in the prefetch queue when the preceding `MUL` ends.
|
||||||
As a result, we'd expect that each **SHR** would execute in 2 cycles;
|
As a result, we'd expect that each `SHR` would execute in 2 cycles;
|
||||||
together with the 118-cycle execution time of multiplying 0 times 0, the
|
together with the 118-cycle execution time of multiplying 0 times 0, the
|
||||||
total execution time should come to 120 cycles per **SHR/MUL** pair, as
|
total execution time should come to 120 cycles per `SHR/MUL` pair, as
|
||||||
shown in Figure 4.4. And, by God, when we run Listing 4.6 we get an
|
shown in Figure 4.4. And, by God, when we run Listing 4.6 we get an
|
||||||
execution time of 25.14 µs per **SHR/MUL** pair, or *exactly* 120
|
execution time of 25.14 µs per `SHR/MUL` pair, or *exactly* 120
|
||||||
cycles! According to these results, the "true" execution time of **SHR**
|
cycles! According to these results, the "true" execution time of `SHR`
|
||||||
would seem to be 2 cycles, quite a change from the conclusion we drew
|
would seem to be 2 cycles, quite a change from the conclusion we drew
|
||||||
from Listing 4.5.
|
from Listing 4.5.
|
||||||
|
|
||||||
The key point is this: We've seen one code sequence in which **SHR**
|
The key point is this: We've seen one code sequence in which `SHR`
|
||||||
took 8-plus cycles to execute, and another in which it took only 2
|
took 8-plus cycles to execute, and another in which it took only 2
|
||||||
cycles. Are we talking about two different forms of **SHR** here? Of
|
cycles. Are we talking about two different forms of `SHR` here? Of
|
||||||
course not—the difference is purely a reflection of the differing states
|
course not—the difference is purely a reflection of the differing states
|
||||||
in which the preceding code left the prefetch queue. In Listing 4.5,
|
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
|
instructions which have sucked the prefetch queue dry, so overall
|
||||||
performance reflects instruction fetch time. By contrast, each **SHR**
|
performance reflects instruction fetch time. By contrast, each `SHR`
|
||||||
in Listing 4.6 follows a **MUL** instruction which leaves the prefetch
|
in Listing 4.6 follows a `MUL` instruction which leaves the prefetch
|
||||||
queue full, so overall performance reflects Execution Unit execution
|
queue full, so overall performance reflects Execution Unit execution
|
||||||
time.
|
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
|
count or the byte count of a given bit of code, but there's no guarantee
|
||||||
that one or the other will improve performance either. For example,
|
that one or the other will improve performance either. For example,
|
||||||
consider Listing 4.7, which consists of a series of 4-cycle, 2-byte
|
consider Listing 4.7, which consists of a series of 4-cycle, 2-byte
|
||||||
**MOV AL,0** instructions, and which executes at the rate of 1.81 µs per
|
`MOV AL,0` instructions, and which executes at the rate of 1.81 µs per
|
||||||
instruction. Now consider Listing 4.8, which replaces the 4-cycle **MOV
|
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
|
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
|
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,
|
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
|
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
|
Don't think that because overall instruction execution time is
|
||||||
determined by both instruction fetch time and Execution Unit execution
|
determined by both instruction fetch time and Execution Unit execution
|
||||||
time, the two times should be added together when estimating
|
time, the two times should be added together when estimating
|
||||||
performance. For example, practically speaking, each **SHR** in Listing
|
performance. For example, practically speaking, each `SHR` in Listing
|
||||||
4.5 does not take 8 cycles of instruction fetch time plus 2 cycles of
|
4.5 does not take 8 cycles of instruction fetch time plus 2 cycles of
|
||||||
Execution Unit execution time to execute. Figure 4.3 shows that while a
|
Execution Unit execution time to execute. Figure 4.3 shows that while a
|
||||||
given **SHR** is executing, the fetch of the next **SHR** is starting,
|
given `SHR` is executing, the fetch of the next `SHR` is starting,
|
||||||
and since the two operations are overlapped for 2 cycles, there's no
|
and since the two operations are overlapped for 2 cycles, there's no
|
||||||
sense in charging the time to both instructions. You could think of the
|
sense in charging the time to both instructions. You could think of the
|
||||||
extra instruction fetch time for **SHR** in Listing 4.5 as being 6
|
extra instruction fetch time for `SHR` in Listing 4.5 as being 6
|
||||||
cycles, which yields an overall execution time of 8 cycles when added to
|
cycles, which yields an overall execution time of 8 cycles when added to
|
||||||
the 2 cycles of Execution Unit execution time.
|
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
|
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
|
The important point is that the time during which the execution of one
|
||||||
instruction and the fetching of the next instruction overlap should only
|
instruction and the fetching of the next instruction overlap should only
|
||||||
be counted toward the overall execution time of one of the instructions.
|
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
|
actually often suffer more from the prefetch queue bottleneck than do
|
||||||
long instructions. Short instructions generally have such fast execution
|
long instructions. Short instructions generally have such fast execution
|
||||||
times that they drain the prefetch queue despite their small size. For
|
times that they drain the prefetch queue despite their small size. For
|
||||||
example, consider the **SHR** of Listing 4.5, which runs at only 25
|
example, consider the `SHR` of Listing 4.5, which runs at only 25
|
||||||
percent of its Execution Unit execution time even though it's only 2
|
percent of its Execution Unit execution time even though it's only 2
|
||||||
bytes long, thanks to the prefetch queue bottleneck. Short instructions
|
bytes long, thanks to the prefetch queue bottleneck. Short instructions
|
||||||
are nonetheless generally faster than long instructions, thanks to the
|
are nonetheless generally faster than long instructions, thanks to the
|
||||||
|
|
|
||||||
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
|
Let's look at examples from opposite ends of the spectrum in terms of
|
||||||
the impact of DRAM refresh on code performance. First, consider the
|
the impact of DRAM refresh on code performance. First, consider the
|
||||||
series of **MUL** instructions in Listing 4.9. Since a 16-bit **MUL** on
|
series of `MUL` instructions in Listing 4.9. Since a 16-bit `MUL` on
|
||||||
the 8088 executes in between 118 and 133 cycles and is only 2 bytes
|
the 8088 executes in between 118 and 133 cycles and is only 2 bytes
|
||||||
long, there should be plenty of time for the prefetch queue to fill
|
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
|
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
|
call ZTimerOff
|
||||||
```
|
```
|
||||||
|
|
||||||
Running Listing 4.9, we find that each **MUL** executes in 24.72 µs, or
|
Running Listing 4.9, we find that each `MUL` executes in 24.72 µs, or
|
||||||
exactly 118 cycles. Since that's the shortest time in which **MUL** can
|
exactly 118 cycles. Since that's the shortest time in which `MUL` can
|
||||||
execute, we can see that no performance is lost to DRAM refresh. Listing
|
execute, we can see that no performance is lost to DRAM refresh. Listing
|
||||||
4.9 clearly illustrates that DRAM refresh only affects code performance
|
4.9 clearly illustrates that DRAM refresh only affects code performance
|
||||||
when a DRAM refresh forces the Execution Unit of the 8088 to wait for a
|
when a DRAM refresh forces the Execution Unit of the 8088 to wait for a
|
||||||
memory access.
|
memory access.
|
||||||
|
|
||||||
Now let's look at the series of **SHR** instructions shown in Listing
|
Now let's look at the series of `SHR` instructions shown in Listing
|
||||||
4.10. Since **SHR** executes in 2 cycles but is 2 bytes long, the
|
4.10. Since `SHR` executes in 2 cycles but is 2 bytes long, the
|
||||||
prefetch queue should be empty while Listing 4.10 executes, with the
|
prefetch queue should be empty while Listing 4.10 executes, with the
|
||||||
8088 prefetching instruction bytes non-stop. As a result, the time per
|
8088 prefetching instruction bytes non-stop. As a result, the time per
|
||||||
instruction of Listing 4.10 should precisely reflect the time required
|
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
|
Since 4 cycles are required to read each instruction byte, we'd expect
|
||||||
each **SHR** to execute in 8 cycles, or 1.676 µs, if there were no DRAM
|
each `SHR` to execute in 8 cycles, or 1.676 µs, if there were no DRAM
|
||||||
refresh. In fact, each **SHR** in Listing 4.10 executes in 1.81 µs,
|
refresh. In fact, each `SHR` in Listing 4.10 executes in 1.81 µs,
|
||||||
indicating that DRAM refresh is taking 7.4 percent of the program's
|
indicating that DRAM refresh is taking 7.4 percent of the program's
|
||||||
execution time. That's nearly 2 percent more than our worst-case
|
execution time. That's nearly 2 percent more than our worst-case
|
||||||
estimate of the loss to DRAM refresh overhead! In fact, the result
|
estimate of the loss to DRAM refresh overhead! In fact, the result
|
||||||
|
|
@ -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
|
refresh's DMA request relative to the 8088's internal instruction
|
||||||
execution state. When the code in Listing 4.10 runs, each DRAM refresh
|
execution state. When the code in Listing 4.10 runs, each DRAM refresh
|
||||||
holds up the CPU for either 5 or 6 cycles, depending on where the 8088
|
holds up the CPU for either 5 or 6 cycles, depending on where the 8088
|
||||||
is in executing the current **SHR** instruction when the refresh request
|
is in executing the current `SHR` instruction when the refresh request
|
||||||
occurs. Now we see that things can get even worse than we thought: *DRAM
|
occurs. Now we see that things can get even worse than we thought: *DRAM
|
||||||
refresh can steal as much as 8.33 percent of available memory access
|
refresh can steal as much as 8.33 percent of available memory access
|
||||||
time—6 out of every 72 cycles—from the 8088.*
|
time—6 out of every 72 cycles—from the 8088.*
|
||||||
|
|
|
||||||
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
|
That sounds pretty serious, but we did make an unfounded assumption
|
||||||
about memory access speed. Let's get some hard numbers. Listing 4.11
|
about memory access speed. Let's get some hard numbers. Listing 4.11
|
||||||
accesses display memory at the 8088's maximum speed, by way of a **REP
|
accesses display memory at the 8088's maximum speed, by way of a `REP
|
||||||
MOVSW** with display memory as both source and destination. The code in
|
MOVSW` with display memory as both source and destination. The code in
|
||||||
Listing 4.11 executes in 3.18 µs per access to display memory—not as
|
Listing 4.11 executes in 3.18 µs per access to display memory—not as
|
||||||
long as we had assumed, but a long time nonetheless.
|
long as we had assumed, but a long time nonetheless.
|
||||||
|
|
||||||
|
|
@ -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
|
For comparison, let's see how long the same code takes when accessing
|
||||||
normal system RAM instead of display memory. The code in Listing 4.12,
|
normal system RAM instead of display memory. The code in Listing 4.12,
|
||||||
which performs a **REP MOVSW** from the code segment to the code
|
which performs a `REP MOVSW` from the code segment to the code
|
||||||
segment, executes in 1.39 µs per display memory access. That means that
|
segment, executes in 1.39 µs per display memory access. That means that
|
||||||
on average, 1.79 µs (more than 8 cycles!) are lost to the display
|
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
|
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
|
> A line-drawing subroutine, which executes perhaps a dozen instructions
|
||||||
> for each display memory access, generally loses less performance to the
|
> for each display memory access, generally loses less performance to the
|
||||||
> display adapter cycle-eater than does a block-copy or scrolling
|
> 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
|
> three-dimensional graphics, which spend a great deal of time performing
|
||||||
> calculations (often using very slow floating-point arithmetic), tend to
|
> calculations (often using very slow floating-point arithmetic), tend to
|
||||||
> suffer less.
|
> 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
|
string. We'll develop a program that will search the file specified on
|
||||||
the command line for a string (also specified on the comline), then
|
the command line for a string (also specified on the comline), then
|
||||||
report whether the string was found or not. (Because the searched-for
|
report whether the string was found or not. (Because the searched-for
|
||||||
string is obtained via **argv**, it can't contain any whitespace
|
string is obtained via `argv`, it can't contain any whitespace
|
||||||
characters.)
|
characters.)
|
||||||
|
|
||||||
This is a *very* limited subset of what search utilities such as grep
|
This is a *very* limited subset of what search utilities such as grep
|
||||||
|
|
@ -115,9 +115,9 @@ access overhead).
|
||||||
### Avoiding the String Trap {#Heading4}
|
### Avoiding the String Trap {#Heading4}
|
||||||
|
|
||||||
The easiest approach would be to use a C/C++ library function. The
|
The easiest approach would be to use a C/C++ library function. The
|
||||||
closest match to what we need is **strstr()**, which searches one string
|
closest match to what we need is `strstr()`, which searches one string
|
||||||
for the first occurrence of a second string. However, while **strstr()**
|
for the first occurrence of a second string. However, while `strstr()`
|
||||||
would work, it isn't ideal for our purposes. The problem is this: Where
|
would work, it isn't ideal for our purposes. The problem is this: Where
|
||||||
we want to search a fixed-length buffer for the first occurrence of a
|
we want to search a fixed-length buffer for the first occurrence of a
|
||||||
string, **strstr()** searches a *string* for the first occurrence of
|
string, `strstr()` searches a *string* for the first occurrence of
|
||||||
another string.
|
another string.
|
||||||
38
05-02.md
38
05-02.md
|
|
@ -10,11 +10,11 @@ chapter: '05'
|
||||||
pages: 115-118
|
pages: 115-118
|
||||||
---
|
---
|
||||||
|
|
||||||
We could put a zero byte at the end of our buffer to allow **strstr()**
|
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
|
to work, but why bother? The `strstr()` function must spend time
|
||||||
either checking for the end of the string being searched or determining
|
either checking for the end of the string being searched or determining
|
||||||
the length of that string—wasted effort given that we already know
|
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
|
implementation is well-written, its performance will suffer, at least
|
||||||
for our application, from unnecessary overhead.
|
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
|
Given that no C/C++ library function meets our needs precisely, an
|
||||||
obvious alternative approach is the brute-force technique that uses
|
obvious alternative approach is the brute-force technique that uses
|
||||||
**memcmp()** to compare *every* potential matching location in the
|
`memcmp()` to compare *every* potential matching location in the
|
||||||
buffer to the string we're searching for, as illustrated in Figure 5.1.
|
buffer to the string we're searching for, as illustrated in Figure 5.1.
|
||||||
|
|
||||||
By the way, we could, of course, use our own code, working with pointers
|
By the way, we could, of course, use our own code, working with pointers
|
||||||
in a loop, to perform the comparison in place of **memcmp()**. But
|
in a loop, to perform the comparison in place of `memcmp()`. But
|
||||||
**memcmp()** will almost certainly use the very fast **REPZ CMPS**
|
`memcmp()` will almost certainly use the very fast `REPZ CMPS`
|
||||||
instruction. However, *never assume!* It wouldn't hurt to use a debugger
|
instruction. However, *never assume!* It wouldn't hurt to use a debugger
|
||||||
to check out the actual machine-code implementation of **memcmp()** from
|
to check out the actual machine-code implementation of `memcmp()` from
|
||||||
your compiler. If necessary, you could always write your own assembly
|
your compiler. If necessary, you could always write your own assembly
|
||||||
language implementation of **memcmp()**.
|
language implementation of `memcmp()`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Invoking **memcmp()** for each potential match location works, but
|
Invoking `memcmp()` for each potential match location works, but
|
||||||
entails considerable overhead. Each comparison requires that parameters
|
entails considerable overhead. Each comparison requires that parameters
|
||||||
be pushed and that a call to and return from **memcmp()** be performed,
|
be pushed and that a call to and return from `memcmp()` be performed,
|
||||||
along with a pass through the comparison loop. Surely there's a better
|
along with a pass through the comparison loop. Surely there's a better
|
||||||
way!
|
way!
|
||||||
|
|
||||||
Indeed there is. We can eliminate most calls to **memcmp()** by
|
Indeed there is. We can eliminate most calls to `memcmp()` by
|
||||||
performing a simple test on each potential match location that will
|
performing a simple test on each potential match location that will
|
||||||
reject most such locations right off the bat. We'll just check whether
|
reject most such locations right off the bat. We'll just check whether
|
||||||
the first character of the potentially matching buffer location matches
|
the first character of the potentially matching buffer location matches
|
||||||
|
|
@ -60,16 +60,16 @@ Figure 5.2.
|
||||||
### Using memchr() {#Heading6}
|
### Using memchr() {#Heading6}
|
||||||
|
|
||||||
There's yet a better way to implement this approach, however. Use the
|
There's yet a better way to implement this approach, however. Use the
|
||||||
**memchr()** function, which does nothing more or less than find the
|
`memchr()` function, which does nothing more or less than find the
|
||||||
next occurrence of a specified character in a fixed-length buffer
|
next occurrence of a specified character in a fixed-length buffer
|
||||||
(presumably by using the extremely efficient **REPNZ SCASB**
|
(presumably by using the extremely efficient `REPNZ SCASB`
|
||||||
instruction, although again it wouldn't hurt to check). By using
|
instruction, although again it wouldn't hurt to check). By using
|
||||||
**memchr()** to scan for potential matches that can then be fully tested
|
`memchr()` to scan for potential matches that can then be fully tested
|
||||||
with **memcmp()**, we can build a highly efficient search engine that
|
with `memcmp()`, we can build a highly efficient search engine that
|
||||||
takes good advantage of the information we have about the buffer being
|
takes good advantage of the information we have about the buffer being
|
||||||
searched and the string we're searching for. Our engine also relies
|
searched and the string we're searching for. Our engine also relies
|
||||||
heavily on repeated string instructions, assuming that the **memchr()**
|
heavily on repeated string instructions, assuming that the `memchr()`
|
||||||
and **memcmp()** library functions are properly coded.
|
and `memcmp()` library functions are properly coded.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@ let's make it restartable.
|
||||||
As it happens, there's no great trick to putting the pieces of this
|
As it happens, there's no great trick to putting the pieces of this
|
||||||
search program together. Basically, we'll read in a buffer of data
|
search program together. Basically, we'll read in a buffer of data
|
||||||
(we'll work with 16K at a time to avoid signed overflow problems with
|
(we'll work with 16K at a time to avoid signed overflow problems with
|
||||||
integers), search it for a match with the **memchr()/memcmp()** engine
|
integers), search it for a match with the `memchr()`/`memcmp()` engine
|
||||||
described, and exit with a "string found" response if the desired string
|
described, and exit with a "string found" response if the desired string
|
||||||
is found.
|
is found.
|
||||||
|
|
||||||
|
|
@ -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
|
program. As you can see, it's not particularly complex, although a few
|
||||||
fairly opaque lines of code are required to handle merging the end of
|
fairly opaque lines of code are required to handle merging the end of
|
||||||
one block with the start of the next. The code that searches a single
|
one block with the start of the next. The code that searches a single
|
||||||
block—the function **SearchForString()—**is simple and compact (as it
|
block—the function `SearchForString()`—is simple and compact (as it
|
||||||
should be, given that it's by far the most heavily-executed code in the
|
should be, given that it's by far the most heavily-executed code in the
|
||||||
listing).
|
listing).
|
||||||
|
|
||||||
|
|
|
||||||
24
05-04.md
24
05-04.md
|
|
@ -13,7 +13,7 @@ pages: 121-122
|
||||||
### Interpreting Where the Cycles Go {#Heading8}
|
### Interpreting Where the Cycles Go {#Heading8}
|
||||||
|
|
||||||
To boost the overall performance of Listing 5.1, I would normally
|
To boost the overall performance of Listing 5.1, I would normally
|
||||||
convert **SearchForString()** to assembly language at this point.
|
convert `SearchForString()` to assembly language at this point.
|
||||||
However, I'm not going to do that, and the reason is as important a
|
However, I'm not going to do that, and the reason is as important a
|
||||||
lesson as any discussion of optimized assembly code is likely to be.
|
lesson as any discussion of optimized assembly code is likely to be.
|
||||||
Take a moment to examine some interesting performance aspects of the C
|
Take a moment to examine some interesting performance aspects of the C
|
||||||
|
|
@ -28,7 +28,7 @@ think.
|
||||||
When Listing 5.1 is run on a 1 MB assembly source file, it takes about
|
When Listing 5.1 is run on a 1 MB assembly source file, it takes about
|
||||||
three seconds to find the string "xxxend" (which is at the end of the
|
three seconds to find the string "xxxend" (which is at the end of the
|
||||||
file) on a 20 MHz 386 machine, with the entire file in a disk cache. If
|
file) on a 20 MHz 386 machine, with the entire file in a disk cache. If
|
||||||
**BLOCK\_SIZE** is trimmed from 16K to 4K, *execution time does not
|
`BLOCK_SIZE` is trimmed from 16K to 4K, *execution time does not
|
||||||
increase perceptibly!* At 2K, the program slows slightly; it's not until
|
increase perceptibly!* At 2K, the program slows slightly; it's not until
|
||||||
the block size shrinks to 64 bytes that execution time becomes
|
the block size shrinks to 64 bytes that execution time becomes
|
||||||
approximately double that of the 16K buffer.
|
approximately double that of the 16K buffer.
|
||||||
|
|
@ -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
|
large, and might not justify the extra memory required for those larger
|
||||||
blocks. Our next discovery is that, even though we read the file in
|
blocks. Our next discovery is that, even though we read the file in
|
||||||
large chunks, most of the execution time of Listing 5.1 is nonetheless
|
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
|
that simply fools the program into thinking that a 1 MB file is being
|
||||||
read, the program ran almost instantaneously—in less than 1/2 second,
|
read, the program ran almost instantaneously—in less than 1/2 second,
|
||||||
even when the searched-for string wasn't anywhere to be found. By
|
even when the searched-for string wasn't anywhere to be found. By
|
||||||
contrast, Listing 5.1 requires three seconds to run even when searching
|
contrast, Listing 5.1 requires three seconds to run even when searching
|
||||||
for a single character that isn't found anywhere in the file, the case
|
for a single character that isn't found anywhere in the file, the case
|
||||||
in which a single call to **memchr()** (and thus a single **REPNZ
|
in which a single call to `memchr()` (and thus a single `REPNZ
|
||||||
SCASB**) can eliminate an entire block at a time.
|
SCASB`) can eliminate an entire block at a time.
|
||||||
|
|
||||||
All in all, the time required for DOS disk access calls is taking up at
|
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
|
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
|
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
|
good deal less than 20 percent of the total, given that the overhead of
|
||||||
loading the program, running through the C startup code, opening the
|
loading the program, running through the C startup code, opening the
|
||||||
file, executing **printf()**, and exiting the program and returning to
|
file, executing `printf()`, and exiting the program and returning to
|
||||||
the DOS shell are also included in my timings. Given which, it should be
|
the DOS shell are also included in my timings. Given which, it should be
|
||||||
apparent why converting to assembly language isn't worth the trouble—the
|
apparent why converting to assembly language isn't worth the trouble—the
|
||||||
best we could do by speeding up the search is a 10 percent or so
|
best we could do by speeding up the search is a 10 percent or so
|
||||||
|
|
@ -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 first character of the search string occurs frequently as might be
|
||||||
the case when searching a text buffer for a string starting with the
|
the case when searching a text buffer for a string starting with the
|
||||||
space character an assembly implementation might be several times
|
space character an assembly implementation might be several times
|
||||||
faster. Why? Because assembly code can switch from **REPNZ SCASB** to
|
faster. Why? Because assembly code can switch from `REPNZ SCASB` to
|
||||||
match the first character to **REPZ CMPS** to check the remaining
|
match the first character to `REPZ CMPS` to check the remaining
|
||||||
characters in just a few instructions.
|
characters in just a few instructions.
|
||||||
|
|
||||||
In contrast, Listing 5.1 must return from **memchr()**, set up
|
In contrast, Listing 5.1 must return from `memchr()`, set up
|
||||||
parameters, and call **memcmp()** in order to do the same thing.
|
parameters, and call `memcmp()` in order to do the same thing.
|
||||||
Likewise, assembly can switch back to **REPNZ SCASB** after a non-match
|
Likewise, assembly can switch back to `REPNZ SCASB` after a non-match
|
||||||
much more quickly than Listing 5.1. The switching overhead is high; when
|
much more quickly than Listing 5.1. The switching overhead is high; when
|
||||||
searching a file completely filled with the character z for the string
|
searching a file completely filled with the character z for the string
|
||||||
"zy," Listing 5.1 takes almost 1/2 minute, or nearly an order of
|
"zy," Listing 5.1 takes almost 1/2 minute, or nearly an order of
|
||||||
|
|
|
||||||
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
|
assembly language version of Listing 5.1, and I expected the assembly
|
||||||
version to be much faster. When I actually looked at where execution
|
version to be much faster. When I actually looked at where execution
|
||||||
time was going (which I did by modifying the program to remove the calls
|
time was going (which I did by modifying the program to remove the calls
|
||||||
to the **read()** function, but a code profiler could be used to do the
|
to the `read()` function, but a code profiler could be used to do the
|
||||||
same thing much more easily), I found that the best code in the world
|
same thing much more easily), I found that the best code in the world
|
||||||
wouldn't make much difference.
|
wouldn't make much difference.
|
||||||
|
|
||||||
|
|
|
||||||
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.
|
many instructions have talents above and beyond their obvious abilities.
|
||||||
|
|
||||||
On the other hand, there are also a number of instructions, such as
|
On the other hand, there are also a number of instructions, such as
|
||||||
**LOOP**, that are designed to perform specific functions but aren't
|
`LOOP`, that are designed to perform specific functions but aren't
|
||||||
always the best instructions for those functions. So don't judge a book
|
always the best instructions for those functions. So don't judge a book
|
||||||
by its cover, either.
|
by its cover, either.
|
||||||
|
|
||||||
|
|
@ -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
|
The point to all this: You must come to regard the x86 family
|
||||||
instructions for what they do, not what you're used to thinking they do.
|
instructions for what they do, not what you're used to thinking they do.
|
||||||
Yes, **SHL** shifts a pattern left—but a look-up table can do the same
|
Yes, `SHL` shifts a pattern left—but a look-up table can do the same
|
||||||
thing, and can often do it faster. **ADD** can indeed add two operands,
|
thing, and can often do it faster. `ADD` can indeed add two operands,
|
||||||
but it can't put the result in a third register; **LEA** can. The
|
but it can't put the result in a third register; `LEA` can. The
|
||||||
instruction set is your raw material for writing high-performance code.
|
instruction set is your raw material for writing high-performance code.
|
||||||
By limiting yourself to thinking only in certain well-established ways
|
By limiting yourself to thinking only in certain well-established ways
|
||||||
about the various instructions, you're putting yourself at a substantial
|
about the various instructions, you're putting yourself at a substantial
|
||||||
|
|
|
||||||
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
|
from a performance standpoint, and which is better depends on the
|
||||||
particular context. If it's a one-shot memory access, it's best to let
|
particular context. If it's a one-shot memory access, it's best to let
|
||||||
the processor perform the addition; it's generally faster at doing this
|
the processor perform the addition; it's generally faster at doing this
|
||||||
than a separate **ADD** instruction would be. If it's a memory access
|
than a separate `ADD` instruction would be. If it's a memory access
|
||||||
within a loop, however, it's advantageous on the 8088 CPU to perform the
|
within a loop, however, it's advantageous on the 8088 CPU to perform the
|
||||||
addition outside the loop, if possible, reducing effective address
|
addition outside the loop, if possible, reducing effective address
|
||||||
calculation time inside the loop, as in the following:
|
calculation time inside the loop, as in the following:
|
||||||
|
|
@ -27,13 +27,13 @@ LoopTop:
|
||||||
loop 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
|
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
|
no longer than `MOV AL,[BX]` on these processors because effective
|
||||||
address calculations generally take no extra time at all. (According to
|
address calculations generally take no extra time at all. (According to
|
||||||
the MASM manual, one extra clock is required if three memory addressing
|
the MASM manual, one extra clock is required if three memory addressing
|
||||||
components, as in **MOV AL,[BX+SI+1]**, are used. I have not been able
|
components, as in `MOV AL,[BX+SI+1]`, are used. I have not been able
|
||||||
to confirm this from Intel publications, but then I haven't looked all
|
to confirm this from Intel publications, but then I haven't looked all
|
||||||
that hard.) If you're optimizing for the 286 or 386, then, you can take
|
that hard.) If you're optimizing for the 286 or 386, then, you can take
|
||||||
advantage of the processor's ability to perform arithmetic as part of
|
advantage of the processor's ability to perform arithmetic as part of
|
||||||
|
|
@ -59,15 +59,15 @@ instructions, at that.
|
||||||
|
|
||||||
How?
|
How?
|
||||||
|
|
||||||
With **LEA**, the only instruction that performs memory addressing
|
With `LEA`, the only instruction that performs memory addressing
|
||||||
calculations but doesn't actually address memory. **LEA** accepts a
|
calculations but doesn't actually address memory. `LEA` accepts a
|
||||||
standard memory addressing operand, but does nothing more than store the
|
standard memory addressing operand, but does nothing more than store the
|
||||||
calculated memory offset in the specified register, which may be any
|
calculated memory offset in the specified register, which may be any
|
||||||
general-purpose register. The operation of **LEA** is illustrated in
|
general-purpose register. The operation of `LEA` is illustrated in
|
||||||
Figure 6.1, which also shows the operation of register-to-register
|
Figure 6.1, which also shows the operation of register-to-register
|
||||||
**ADD**, for comparis on.
|
`ADD`, for comparis on.
|
||||||
|
|
||||||
What does that give us? Two things that **ADD** doesn't provide: the
|
What does that give us? Two things that `ADD` doesn't provide: the
|
||||||
ability to perform addition with either two or three operands, and the
|
ability to perform addition with either two or three operands, and the
|
||||||
ability to store the result in *any* register, not just in one of the
|
ability to store the result in *any* register, not just in one of the
|
||||||
source operands.
|
source operands.
|
||||||
|
|
@ -102,20 +102,20 @@ or:
|
||||||
lea di,[si+2]
|
lea di,[si+2]
|
||||||
```
|
```
|
||||||
|
|
||||||
Mind you, the only components **LEA** can add are BX or BP, SI or DI,
|
Mind you, the only components `LEA` can add are BX or BP, SI or DI,
|
||||||
and a constant displacement, so it's not going to replace **ADD** most
|
and a constant displacement, so it's not going to replace `ADD` most
|
||||||
of the time. Also, **LEA** is considerably slower than **ADD** on an
|
of the time. Also, `LEA` is considerably slower than `ADD` on an
|
||||||
8088, although it is just as fast as **ADD** on a 286 or 386 when fewer
|
8088, although it is just as fast as `ADD` on a 286 or 386 when fewer
|
||||||
than three memory addressing components are used. **LEA** is 1 cycle
|
than three memory addressing components are used. `LEA` is 1 cycle
|
||||||
slower than **ADD** on a 486 if the sum of two registers is used to
|
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
|
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.
|
and Pentium, `LEA` can also be slowed down by addressing interlocks.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### The Wonders of LEA on the 386 {#Heading5}
|
#### The Wonders of LEA on the 386 {#Heading5}
|
||||||
|
|
||||||
**LEA** really comes into its own as a "super-ADD" instruction on the
|
`LEA` really comes into its own as a "super-ADD" instruction on the
|
||||||
386, 486, and Pentium, where it can take advantage of the enhanced
|
386, 486, and Pentium, where it can take advantage of the enhanced
|
||||||
memory addressing modes of those processors. (The 486 and Pentium offer
|
memory addressing modes of those processors. (The 486 and Pentium offer
|
||||||
the same modes as the 386, so I'll refer only to the 386 from now on.)
|
the same modes as the 386, so I'll refer only to the 386 from now on.)
|
||||||
|
|
@ -129,15 +129,15 @@ that's good for.
|
||||||
Well, the obvious advantage is that any two 32-bit registers, or any
|
Well, the obvious advantage is that any two 32-bit registers, or any
|
||||||
32-bit register and any constant, or any two 32-bit registers and any
|
32-bit register and any constant, or any two 32-bit registers and any
|
||||||
constant, can be added together, with the result stored in any register.
|
constant, can be added together, with the result stored in any register.
|
||||||
This makes the 32-bit **LEA** much more generally useful than the
|
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
|
standard 16-bit `LEA` in the role of an `ADD` with an independent
|
||||||
destination.
|
destination.
|
||||||
|
|
||||||
![**Figure 6.2** *Operation of the 32-bit LEA reg,[Addr].*](images/06-02.jpg)
|
![**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
|
by the power-of-two values 2, 4, or 8, but that's useful more often than
|
||||||
you might imagine, especially when dealing with pointers into tables.
|
you might imagine, especially when dealing with pointers into tables.
|
||||||
Besides, multiplying by 2, 4, or 8 amounts to a left shift of 1, 2, or 3
|
Besides, multiplying by 2, 4, or 8 amounts to a left shift of 1, 2, or 3
|
||||||
|
|
@ -162,8 +162,8 @@ when pointing to an entry in a doubly indexed table.
|
||||||
|
|
||||||
### Multiplication with LEA Using Non-Powers of Two {#Heading6}
|
### Multiplication with LEA Using Non-Powers of Two {#Heading6}
|
||||||
|
|
||||||
Are you impressed yet with all that **LEA** can do on the 386? Believe
|
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
|
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
|
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
|
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
|
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]
|
lea ebx,[ebx+ebx*4]
|
||||||
```
|
```
|
||||||
|
|
||||||
Without **LEA** and scaling, multiplication of EBX by 5 would require
|
Without `LEA` and scaling, multiplication of EBX by 5 would require
|
||||||
either a relatively slow **MUL**, along with a set-up instruction or
|
either a relatively slow `MUL`, along with a set-up instruction or
|
||||||
two, or three separate instructions along the lines of the following
|
two, or three separate instructions along the lines of the following
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -192,11 +192,11 @@ cycles is a pretty neat trick, even though it works only on a 386 or
|
||||||
486.
|
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
|
> 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
|
> multiplier you might want, but it covers some commonly used ones, and
|
||||||
> the performance is hard to beat.
|
> the performance is hard to beat.
|
||||||
|
|
||||||
I'd like to extend my thanks to Duane Strong of Metagraphics for his
|
I'd like to extend my thanks to Duane Strong of Metagraphics for his
|
||||||
help in brainstorming uses for the 386 version of **LEA** and for
|
help in brainstorming uses for the 386 version of `LEA` and for
|
||||||
pointing out the complications of 486 instruction timings.
|
pointing out the complications of 486 instruction timings.
|
||||||
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}
|
#### When LOOP Is a Bad Idea {#Heading3}
|
||||||
|
|
||||||
Let's examine first an instruction that is less than it appears to be:
|
Let's examine first an instruction that is less than it appears to be:
|
||||||
**LOOP**. There's no mystery about what **LOOP** does; it decrements CX
|
`LOOP`. There's no mystery about what `LOOP` does; it decrements CX
|
||||||
and branches if CX doesn't decrement to zero. It's so beautifully suited
|
and branches if CX doesn't decrement to zero. It's so beautifully suited
|
||||||
to the task of counting down loops that any experienced x86 programmer
|
to the task of counting down loops that any experienced x86 programmer
|
||||||
instinctively stuffs the loop count in CX and reaches for **LOOP** when
|
instinctively stuffs the loop count in CX and reaches for `LOOP` when
|
||||||
setting up a loop. That's fine—**LOOP** does, of course, work as
|
setting up a loop. That's fine—`LOOP` does, of course, work as
|
||||||
advertised—but there is one problem:
|
advertised—but there is one problem:
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> On half of the processors in the x86 family, **LOOP** is slower than
|
> 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
|
> `DEC CX` followed by `JNZ`. (Granted, `DEC CX/JNZ` isn't precisely
|
||||||
> equivalent to **LOOP,** because **DEC** alters the flags and LOOP
|
> equivalent to `LOOP`, because `DEC` alters the flags and LOOP
|
||||||
> doesn't, but in most situations they're comparable.)
|
> doesn't, but in most situations they're comparable.)
|
||||||
|
|
||||||
How can this be? Don't ask me, ask Intel. On the 8088 and 80286,
|
How can this be? Don't ask me, ask Intel. On the 8088 and 80286,
|
||||||
**LOOP** is indeed faster than **DEC CX/JNZ** by a cycle, and **LOOP**
|
`LOOP` is indeed faster than `DEC CX/JNZ` by a cycle, and `LOOP`
|
||||||
is generally a little faster still because it's a byte shorter and so
|
is generally a little faster still because it's a byte shorter and so
|
||||||
can be fetched faster. On the 386, however, things change; **LOOP** is
|
can be fetched faster. On the 386, however, things change; `LOOP` is
|
||||||
two cycles *slower* than **DEC/JNZ,** and the fetch time for one extra
|
two cycles *slower* than `DEC/JNZ` and the fetch time for one extra
|
||||||
byte on even an uncached 386 generally isn't significant. (Remember that
|
byte on even an uncached 386 generally isn't significant. (Remember that
|
||||||
the 386 fetches four instruction bytes at a pop.) **LOOP** is three
|
the 386 fetches four instruction bytes at a pop.) `LOOP` is three
|
||||||
cycles slower than **DEC/JNZ** on the 486, and the 486 executes
|
cycles slower than `DEC/JNZ` on the 486, and the 486 executes
|
||||||
instructions in so few cycles that those three cycles mean that
|
instructions in so few cycles that those three cycles mean that
|
||||||
**DEC/JNZ** is nearly *twice* as fast as **LOOP**. Then, too, unlike
|
`DEC/JNZ` is nearly *twice* as fast as `LOOP`. Then, too, unlike
|
||||||
**LOOP, DEC** doesn't require that **CX** be used, so the **DEC/JNZ**
|
`LOOP, DEC` doesn't require that `CX` be used, so the `DEC/JNZ`
|
||||||
solution is both faster and more flexible on the 386 and 486, and on the
|
solution is both faster and more flexible on the 386 and 486, and on the
|
||||||
Pentium as well. (By the way, all this is not just theory; I've timed
|
Pentium as well. (By the way, all this is not just theory; I've timed
|
||||||
the relative performances of **LOOP** and **DEC CX/JNZ** on a cached
|
the relative performances of `LOOP` and `DEC CX/JNZ` on a cached
|
||||||
386, and LOOP really is slower.)
|
386, and LOOP really is slower.)
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> Things are stranger still for **LOOP**'s relative **JCXZ,** which
|
> Things are stranger still for `LOOP`'s relative `JCXZ`, which
|
||||||
> branches if and only if CX is zero. **JCXZ** is faster than **AND
|
> 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
|
> CX,CX/JZ` on the 8088 and 80286, and equivalent on the 80386—but is
|
||||||
> about twice as slow on the 486!
|
> about twice as slow on the 486!
|
||||||
28
07-02.md
28
07-02.md
|
|
@ -10,7 +10,7 @@ chapter: '07'
|
||||||
pages: 139-141
|
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:
|
like this:
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -18,19 +18,19 @@ and cx,ofh ;Isolate the desired field
|
||||||
jcxz SkipLoop ;If field is 0, don't bother
|
jcxz SkipLoop ;If field is 0, don't bother
|
||||||
```
|
```
|
||||||
|
|
||||||
The **AND** instruction has already set the Zero flag, so this
|
The `AND` instruction has already set the Zero flag, so this
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
and cx,0fh ;Isolate the desired field
|
and cx,0fh ;Isolate the desired field
|
||||||
jz SkipLoop ;If field is 0, don't bother
|
jz SkipLoop ;If field is 0, don't bother
|
||||||
```
|
```
|
||||||
|
|
||||||
will do just fine and is faster on all processors. Use **JCXZ** only
|
will do just fine and is faster on all processors. Use `JCXZ` only
|
||||||
when the Zero flag isn't already set to reflect the status of CX.
|
when the Zero flag isn't already set to reflect the status of CX.
|
||||||
|
|
||||||
### The Lessons of LOOP and JCXZ {#Heading4}
|
### The Lessons of LOOP and JCXZ {#Heading4}
|
||||||
|
|
||||||
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
|
instruction that is intended to do a complex task is not necessarily
|
||||||
faster than several instructions that together do the same thing.
|
faster than several instructions that together do the same thing.
|
||||||
Second, that the relative merits of instructions and optimization rules
|
Second, that the relative merits of instructions and optimization rules
|
||||||
|
|
@ -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
|
In particular, if you're going to write 386 protected mode code, which
|
||||||
will run only on the 386, 486, and Pentium, you'd be well advised to
|
will run only on the 386, 486, and Pentium, you'd be well advised to
|
||||||
rethink your use of the more esoteric members of the x86 instruction
|
rethink your use of the more esoteric members of the x86 instruction
|
||||||
set. **LOOP, JCXZ,** the various accumulator-specific instructions, and
|
set. `LOOP, JCXZ`, the various accumulator-specific instructions, and
|
||||||
even the string instructions in many circumstances no longer offer the
|
even the string instructions in many circumstances no longer offer the
|
||||||
advantages they did on the 8088. Sometimes they're just not any faster
|
advantages they did on the 8088. Sometimes they're just not any faster
|
||||||
than more general instructions, so they're not worth going out of your
|
than more general instructions, so they're not worth going out of your
|
||||||
way to use; sometimes, as with **LOOP,** they're actually slower, and
|
way to use; sometimes, as with `LOOP`, they're actually slower, and
|
||||||
you'd do well to avoid them altogether in the 386/486 world. Reviewing
|
you'd do well to avoid them altogether in the 386/486 world. Reviewing
|
||||||
the instruction cycle times in the MASM or TASM manuals, or looking over
|
the instruction cycle times in the MASM or TASM manuals, or looking over
|
||||||
the cycle times in Intel's literature, is a good place to start;
|
the cycle times in Intel's literature, is a good place to start;
|
||||||
|
|
@ -53,8 +53,8 @@ relative performance levels of x86 instructions.
|
||||||
|
|
||||||
#### Avoiding LOOPS of Any Stripe {#Heading5}
|
#### Avoiding LOOPS of Any Stripe {#Heading5}
|
||||||
|
|
||||||
Cycle counting and directly substituting instructions (**DEC CX/JNZ**
|
Cycle counting and directly substituting instructions (`DEC CX/JNZ`
|
||||||
for **LOOP,** for example) are techniques that belong at the lowest
|
for `LOOP`, for example) are techniques that belong at the lowest
|
||||||
level of optimization. It's an important level, but it's fairly
|
level of optimization. It's an important level, but it's fairly
|
||||||
mechanical; once you've learned the capabilities and relative
|
mechanical; once you've learned the capabilities and relative
|
||||||
performance levels of the various instructions, you should be able to
|
performance levels of the various instructions, you should be able to
|
||||||
|
|
@ -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
|
perform well. It doesn't matter what the instructions are intended to do
|
||||||
or what their names are; all that matters is what they *do.*
|
or what their names are; all that matters is what they *do.*
|
||||||
|
|
||||||
Our discussion of **LOOP** versus **DEC/JNZ** is an excellent example of
|
Our discussion of `LOOP` versus `DEC/JNZ` is an excellent example of
|
||||||
optimization by cycle counting. It's worth knowing, but once you've
|
optimization by cycle counting. It's worth knowing, but once you've
|
||||||
learned it, you just routinely use **DEC/JNZ** at the bottom of loops in
|
learned it, you just routinely use `DEC/JNZ` at the bottom of loops in
|
||||||
386/486-specific code, and that's that. Besides, you'll save at most a
|
386/486-specific code, and that's that. Besides, you'll save at most a
|
||||||
few cycles each time, and while that helps a little, it's not going to
|
few cycles each time, and while that helps a little, it's not going to
|
||||||
make all *that* much difference.
|
make all *that* much difference.
|
||||||
|
|
||||||
Now let's step back for a moment, and with no preconceptions consider
|
Now let's step back for a moment, and with no preconceptions consider
|
||||||
what the x86 instruction set can do for us. The bulk of the time with
|
what the x86 instruction set can do for us. The bulk of the time with
|
||||||
both **LOOP** and **DEC/JNZ** is taken up by branching, which just
|
both `LOOP` and `DEC/JNZ` is taken up by branching, which just
|
||||||
happens to be one of the slowest aspects of every processor in the x86
|
happens to be one of the slowest aspects of every processor in the x86
|
||||||
family, and the rest is taken up by decrementing the count register and
|
family, and the rest is taken up by decrementing the count register and
|
||||||
checking whether it's zero. There may be ways to perform those tasks a
|
checking whether it's zero. There may be ways to perform those tasks a
|
||||||
|
|
@ -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
|
byte is found, a zero byte is found, or the specified number of
|
||||||
characters have been checked. Such a function would be useful for
|
characters have been checked. Such a function would be useful for
|
||||||
scanning up to a maximum number of characters in a zero-terminated
|
scanning up to a maximum number of characters in a zero-terminated
|
||||||
buffer. Listing 7.1, which uses **LOOP** in the main loop, performs a
|
buffer. Listing 7.1, which uses `LOOP` in the main loop, performs a
|
||||||
search of the sample string for a period (‘.') in 170 µs on a 20 MHz
|
search of the sample string for a period (‘.') in 170 µs on a 20 MHz
|
||||||
cached 386.
|
cached 386.
|
||||||
|
|
||||||
When the **LOOP** in Listing 7.1 is replaced with **DEC CX/JNZ,**
|
When the `LOOP` in Listing 7.1 is replaced with `DEC CX/JNZ`,
|
||||||
performance improves to 168 µs, less than 2 percent faster than Listing
|
performance improves to 168 µs, less than 2 percent faster than Listing
|
||||||
7.1. Actually, instruction fetching, instruction alignment, cache
|
7.1. Actually, instruction fetching, instruction alignment, cache
|
||||||
characteristics, or something similar is affecting these results; I'd
|
characteristics, or something similar is affecting these results; I'd
|
||||||
expect a slightly larger improvement—around 7 percent—but that's the
|
expect a slightly larger improvement—around 7 percent—but that's the
|
||||||
most that counting cycles could buy us in this case. (All right,
|
most that counting cycles could buy us in this case. (All right,
|
||||||
already; **LOOPNZ** could be used at the bottom of the loop, and other
|
already; `LOOPNZ` could be used at the bottom of the loop, and other
|
||||||
optimizations are surely possible, but all that won't add up to anywhere
|
optimizations are surely possible, but all that won't add up to anywhere
|
||||||
near the benefits we're about to see from local optimization, and that's
|
near the benefits we're about to see from local optimization, and that's
|
||||||
the whole point.)
|
the whole point.)
|
||||||
4
07-03.md
4
07-03.md
|
|
@ -108,9 +108,9 @@ SearchMaxLengthendp
|
||||||
### Unrolling Loops {#Heading7}
|
### Unrolling Loops {#Heading7}
|
||||||
|
|
||||||
Listing 7.2 takes a different tack, unrolling the loop so that four
|
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
|
used inside the loop in each listing, but Listing 7.2 is arranged so
|
||||||
that three-quarters of the **LOOP**s are eliminated. Listings 7.1 and
|
that three-quarters of the `LOOP`s are eliminated. Listings 7.1 and
|
||||||
7.2 perform exactly the same task, and they use the same instructions in
|
7.2 perform exactly the same task, and they use the same instructions in
|
||||||
the loop—the searching algorithm hasn't changed in any way—but we have
|
the loop—the searching algorithm hasn't changed in any way—but we have
|
||||||
sequenced the instructions differently in Listing 7.2, and that makes
|
sequenced the instructions differently in Listing 7.2, and that makes
|
||||||
|
|
|
||||||
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
|
How much difference? Listing 7.2 runs in 121 µs—40 percent faster than
|
||||||
Listing 7.1, even though Listing 7.2 still uses **LOOP** rather than
|
Listing 7.1, even though Listing 7.2 still uses `LOOP` rather than
|
||||||
**DEC CX/JNZ.** (The loop in Listing 7.2 could be unrolled further, too;
|
`DEC CX/JNZ`. (The loop in Listing 7.2 could be unrolled further, too;
|
||||||
it's just a question of how much more memory you want to trade for
|
it's just a question of how much more memory you want to trade for
|
||||||
ever-decreasing performance benefits.) That's typical of local
|
ever-decreasing performance benefits.) That's typical of local
|
||||||
optimization; it won't often yield the order-of-magnitude improvements
|
optimization; it won't often yield the order-of-magnitude improvements
|
||||||
|
|
|
||||||
60
07-05.md
60
07-05.md
|
|
@ -80,25 +80,25 @@ BIT_PATTERN=BIT_PATTERN SHL 1
|
||||||
|
|
||||||
#### NOT Flips Bits—Not Flags {#Heading9}
|
#### NOT Flips Bits—Not Flags {#Heading9}
|
||||||
|
|
||||||
The **NOT** instruction flips all the bits in the operand, from 0 to 1
|
The `NOT` instruction flips all the bits in the operand, from 0 to 1
|
||||||
or from 1 to 0. That's as simple as could be, but **NOT** nonetheless
|
or from 1 to 0. That's as simple as could be, but `NOT` nonetheless
|
||||||
has a minor but interesting talent: It doesn't affect the flags. That
|
has a minor but interesting talent: It doesn't affect the flags. That
|
||||||
can be irritating; I once spent a good hour tracking down a bug caused
|
can be irritating; I once spent a good hour tracking down a bug caused
|
||||||
by my unconscious assumption that **NOT** does set the flags. After all,
|
by my unconscious assumption that `NOT` does set the flags. After all,
|
||||||
every other arithmetic and logical instruction sets the flags; why not
|
every other arithmetic and logical instruction sets the flags; why not
|
||||||
**NOT**? Probably because **NOT** isn't considered to be an arithmetic
|
`NOT`? Probably because `NOT` isn't considered to be an arithmetic
|
||||||
or logical instruction at all; rather, it's a data manipulation
|
or logical instruction at all; rather, it's a data manipulation
|
||||||
instruction, like **MOV** and the various rotates. (These are **RCR,
|
instruction, like `MOV` and the various rotates. (These are `RCR`,
|
||||||
RCL, ROR,** and **ROL,** which affect only the Carry and Overflow
|
`RCL`, `ROR`, and `ROL`, which affect only the Carry and Overflow
|
||||||
flags.) NOT is often used for tasks, such as flipping masks, where
|
flags.) NOT is often used for tasks, such as flipping masks, where
|
||||||
there's no reason to test the state of the result, and in that context
|
there's no reason to test the state of the result, and in that context
|
||||||
it can be handy to keep the flags unmodified for later testing.
|
it can be handy to keep the flags unmodified for later testing.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> Besides, if you want to **NOT** an operand and set the flags in the
|
> 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
|
> process, you can just `XOR` it with -1. Put another way, the only
|
||||||
> functional difference between **NOT AX** and **XOR AX,0FFFFH** is that
|
> functional difference between `NOT AX` and `XOR AX,0FFFFH` is that
|
||||||
> **XOR** modifies the flags and **NOT** doesn't.
|
> `XOR` modifies the flags and `NOT` doesn't.
|
||||||
|
|
||||||
The x86 instruction set offers many ways to accomplish almost any task.
|
The x86 instruction set offers many ways to accomplish almost any task.
|
||||||
Understanding the subtle distinctions between the instructions—whether
|
Understanding the subtle distinctions between the instructions—whether
|
||||||
|
|
@ -109,17 +109,17 @@ you're trying to minimize branching.
|
||||||
#### Incrementing with and without Carry {#Heading10}
|
#### Incrementing with and without Carry {#Heading10}
|
||||||
|
|
||||||
Another case in which there are two slightly different ways to perform a
|
Another case in which there are two slightly different ways to perform a
|
||||||
task involves adding 1 to an operand. You can do this with **INC,** as
|
task involves adding 1 to an operand. You can do this with `INC`, as
|
||||||
in **INC AX,** or you can do it with **ADD,** as in **ADD AX,1.** What's
|
in `INC AX`, or you can do it with `ADD`, as in `ADD AX,1`. What's
|
||||||
the difference? The obvious difference is that **INC** is usually a byte
|
the difference? The obvious difference is that `INC` is usually a byte
|
||||||
or two shorter (the exception being **ADD AL,1,** which at two bytes is
|
or two shorter (the exception being `ADD AL,1`, which at two bytes is
|
||||||
the same length as **INC AL**), and is faster on some processors. Less
|
the same length as `INC AL`), and is faster on some processors. Less
|
||||||
obvious, but no less important, is that **ADD** sets the Carry flag
|
obvious, but no less important, is that `ADD` sets the Carry flag
|
||||||
while **INC** leaves the Carry flag untouched.
|
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
|
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
|
without having to do any work to preserve the Carry status from one
|
||||||
addition to the next.
|
addition to the next.
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ LOOP_TOP:
|
||||||
LOOP 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.
|
additions, with code along the lines shown in Listing 7.6.
|
||||||
|
|
||||||
**LISTING 7.6 L7-6.ASM**
|
**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
|
that depends on the processor and the situation. The Listing 7.6
|
||||||
approach is *different,* and if you understand the differences, you'll
|
approach is *different,* and if you understand the differences, you'll
|
||||||
be able to choose the best approach for whatever code you happen to
|
be able to choose the best approach for whatever code you happen to
|
||||||
write. (**DEC** has the same property of preserving the Carry flag, by
|
write. (`DEC` has the same property of preserving the Carry flag, by
|
||||||
the way.)
|
the way.)
|
||||||
|
|
||||||
There are a couple of interesting aspects to the last example. First,
|
There are a couple of interesting aspects to the last example. First,
|
||||||
note that **LOOP** doesn't affect any flags at all; this allows the
|
note that `LOOP` doesn't affect any flags at all; this allows the
|
||||||
Carry flag to remain unchanged from one addition to the next. Not
|
Carry flag to remain unchanged from one addition to the next. Not
|
||||||
altering the arithmetic flags is a common characteristic of program
|
altering the arithmetic flags is a common characteristic of program
|
||||||
control instructions (as opposed to arithmetic and logical instructions
|
control instructions (as opposed to arithmetic and logical instructions
|
||||||
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
|
> The rule is not that the arithmetic flags change whenever the CPU
|
||||||
> performs a calculation; rather, the flags change whenever you execute an
|
> 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.
|
> 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.
|
which counts down CX to 0, doesn't affect the flags either.
|
||||||
|
|
||||||
The other interesting point about the last example is the use of
|
The other interesting point about the last example is the use of
|
||||||
**LAHF** and **SAHF,** which transfer the low byte of the FLAGS register
|
`LAHF` and `SAHF`, which transfer the low byte of the FLAGS register
|
||||||
to and from AH, respectively. These instructions were created to help
|
to and from AH, respectively. These instructions were created to help
|
||||||
provide compatibility with the 8080's (that's *8080*, not *8088*)
|
provide compatibility with the 8080's (that's *8080*, not *8088*)
|
||||||
**PUSH** **PSW** and **POP PSW** instructions, but turn out to be
|
`PUSH` `PSW` and `POP PSW` instructions, but turn out to be
|
||||||
compact (one byte) instructions for saving and restoring the arithmetic
|
compact (one byte) instructions for saving and restoring the arithmetic
|
||||||
flags. A word of caution, however: **SAHF** restores the Carry, Zero,
|
flags. A word of caution, however: `SAHF` restores the Carry, Zero,
|
||||||
Sign, Auxiliary Carry, and Parity flags—but *not* the Overflow flag,
|
Sign, Auxiliary Carry, and Parity flags—but *not* the Overflow flag,
|
||||||
which resides in the high byte of the FLAGS register. Also, be aware
|
which resides in the high byte of the FLAGS register. Also, be aware
|
||||||
that **LAHF** and **SAHF** provide a fast way to preserve the flags on
|
that `LAHF` and `SAHF` provide a fast way to preserve the flags on
|
||||||
an 8088 but are relatively slow instructions on the 486 and Pentium.
|
an 8088 but are relatively slow instructions on the 486 and Pentium.
|
||||||
|
|
||||||
There are times when it's a clear liability that **INC** doesn't set the
|
There are times when it's a clear liability that `INC` doesn't set the
|
||||||
Carry flag. For instance
|
Carry flag. For instance
|
||||||
|
|
||||||
```nasm
|
```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 in all ways, whereas C compilers tend to use a subset of
|
||||||
registers and instructions in a limited number of ways. Yes and no. It's
|
registers and instructions in a limited number of ways. Yes and no. It's
|
||||||
true that C compilers typically don't generate instructions such as
|
true that C compilers typically don't generate instructions such as
|
||||||
**XLAT,** rotates, or the string instructions. On the other hand,
|
`XLAT`, rotates, or the string instructions. On the other hand,
|
||||||
**XLAT** and rotates are useful in a limited set of circumstances, and
|
`XLAT` and rotates are useful in a limited set of circumstances, and
|
||||||
string instructions *are* used in the C library functions. In fact, C
|
string instructions *are* used in the C library functions. In fact, C
|
||||||
library code is likely to be carefully optimized by experts, and may be
|
library code is likely to be carefully optimized by experts, and may be
|
||||||
much better than equivalent code you'd produce yourself.
|
much better than equivalent code you'd produce yourself.
|
||||||
|
|
||||||
Am I saying that C compilers produce better code than you do? No, I'm
|
Am I saying that C compilers produce better code than you do? No, I'm
|
||||||
saying that they *can,* unless you use assembly language properly.
|
saying that they *can*, unless you use assembly language properly.
|
||||||
Writing code in assembly language rather than C guarantees nothing.
|
Writing code in assembly language rather than C guarantees nothing.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
|
|
@ -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.
|
the code shown in Figure 8.1B.
|
||||||
|
|
||||||
Congratulations! You've successfully eliminated all stack frame access,
|
Congratulations! You've successfully eliminated all stack frame access,
|
||||||
you've used **LOOP** (although **DEC SI/JNZ** is actually faster on 386
|
you've used `LOOP` (although `DEC SI/JNZ` is actually faster on 386
|
||||||
and later machines, as I explained in the last chapter), and you've used
|
and later machines, as I explained in the last chapter), and you've used
|
||||||
a string instruction. Unfortunately, the new code isn't going to run
|
a string instruction. Unfortunately, the new code isn't going to run
|
||||||
very much faster. Maybe 25 percent faster, maybe a little more. Big
|
very much faster. Maybe 25 percent faster, maybe a little more. Big
|
||||||
|
|
@ -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
|
affects overall performance. For example, consider that the data
|
||||||
searched in the last example is stored in an array of structures, with
|
searched in the last example is stored in an array of structures, with
|
||||||
each structure in the array containing other information as well. In
|
each structure in the array containing other information as well. In
|
||||||
this situation, **REP SCASW** couldn't be used because the data searched
|
this situation, `REP SCASW` couldn't be used because the data searched
|
||||||
through wouldn't be contiguous.
|
through wouldn't be contiguous.
|
||||||
|
|
||||||
However, if the need for performance in searching the array is urgent
|
However, if the need for performance in searching the array is urgent
|
||||||
enough, there's no reason why you can't reorganize the data. This might
|
enough, there's no reason why you can't reorganize the data. This might
|
||||||
mean removing the array elements from the structures and storing them in
|
mean removing the array elements from the structures and storing them in
|
||||||
their own array so that **REP SCASW** *could* be used.
|
their own array so that `REP SCASW` *could* be used.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> Organizing a program's data so that the performance of the critical
|
> 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
|
Listing 8.1 is the sample C application I'm going to use to examine
|
||||||
optimization in action. Listing 8.1 isn't really complete—it doesn't
|
optimization in action. Listing 8.1 isn't really complete—it doesn't
|
||||||
handle the "no-matches" case well, and it assumes that the sum of all
|
handle the "no-matches" case well, and it assumes that the sum of all
|
||||||
matches will fit into an **int—**but it will do just fine as an
|
matches will fit into an `int`-but it will do just fine as an
|
||||||
optimization example.
|
optimization example.
|
||||||
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
|
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,
|
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
|
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
|
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 and the inner one repeating once for each array element in each
|
||||||
block. The inner loop—the critical one—is compact, containing only four
|
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
|
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
|
generates for the inner loop, consisting of a mere seven assembly
|
||||||
language instructions inside the loop. The compiler is smart enough to
|
language instructions inside the loop. The compiler is smart enough to
|
||||||
convert the loop index variable, which counts up but is used for nothing
|
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.
|
instruction can be used.
|
||||||
|
|
||||||
**LISTING 8.2 L8-2.COD**
|
**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's hard to squeeze much more performance from this code by tweaking
|
||||||
it, as exemplified by Listing 8.3, a fine-tuned assembly version of
|
it, as exemplified by Listing 8.3, a fine-tuned assembly version of
|
||||||
**FindIDAverage** that was produced by looking at the assembly output of
|
`FindIDAverage` that was produced by looking at the assembly output of
|
||||||
MS C/C++ and tightening it. Listing 8.3 eliminates all stack frame
|
MS C/C++ and tightening it. Listing 8.3 eliminates all stack frame
|
||||||
access in the inner loop, but that's about all the tightening there is
|
access in the inner loop, but that's about all the tightening there is
|
||||||
to do. The result, as shown in Table 8.1, is that Listing 8.3 runs a
|
to do. The result, as shown in Table 8.1, is that Listing 8.3 runs a
|
||||||
|
|
@ -97,8 +97,8 @@ _FindIDAverage ENDP
|
||||||
|
|
||||||
Listing 8.4 tosses some sophisticated optimization techniques into the
|
Listing 8.4 tosses some sophisticated optimization techniques into the
|
||||||
mix. The loop is unrolled eight times, eliminating a good deal of
|
mix. The loop is unrolled eight times, eliminating a good deal of
|
||||||
branching, and **SCASW** is used instead of **CMP [DI],AX.** (Note,
|
branching, and `SCASW` is used instead of `CMP [DI],AX`. (Note,
|
||||||
however, that **SCASW** is in fact slower than **CMP [DI],AX** on the
|
however, that `SCASW` is in fact slower than `CMP [DI],AX` on the
|
||||||
386 and 486, and is sometimes faster on the 286 and 8088 only because
|
386 and 486, and is sometimes faster on the 286 and 8088 only because
|
||||||
it's shorter and therefore may prefetch faster.) This advanced tweaking
|
it's shorter and therefore may prefetch faster.) This advanced tweaking
|
||||||
produces a 39 percent improvement over the original C code—substantial,
|
produces a 39 percent improvement over the original C code—substantial,
|
||||||
|
|
|
||||||
6
08-05.md
6
08-05.md
|
|
@ -170,9 +170,9 @@ _FindIDAverage2 ENDP
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The whole point of this rearrangement is to allow us to use **REP
|
The whole point of this rearrangement is to allow us to use `REP
|
||||||
SCASW** to search through each block, and that's exactly what
|
SCASW` to search through each block, and that's exactly what
|
||||||
**FindIDAverage2** in Listing 8.6 does. The result: Listing 8.6
|
`FindIDAverage2` in Listing 8.6 does. The result: Listing 8.6
|
||||||
calculates the average about *three times* as fast as the original C
|
calculates the average about *three times* as fast as the original C
|
||||||
implementation and more than twice as fast as Listing 8.4, heavily
|
implementation and more than twice as fast as Listing 8.4, heavily
|
||||||
optimized as the latter code is.
|
optimized as the latter code is.
|
||||||
|
|
|
||||||
32
09-01.md
32
09-01.md
|
|
@ -78,14 +78,14 @@ feedback.
|
||||||
|
|
||||||
#### Another Look at LEA {#Heading3}
|
#### Another Look at LEA {#Heading3}
|
||||||
|
|
||||||
Several people have pointed out that while **LEA** is great for
|
Several people have pointed out that while `LEA` is great for
|
||||||
performing certain additions (see Chapter 6), it isn't a perfect
|
performing certain additions (see Chapter 6), it isn't a perfect
|
||||||
replacement for **ADD**. What's the difference? **LEA**, an addressing
|
replacement for `ADD`. What's the difference? `LEA`, an addressing
|
||||||
instruction by trade, doesn't affect the flags, while the arithmetic
|
instruction by trade, doesn't affect the flags, while the arithmetic
|
||||||
**ADD** instruction most certainly does. This is no problem when
|
`ADD` instruction most certainly does. This is no problem when
|
||||||
performing additions that involve only quantities that fit in one
|
performing additions that involve only quantities that fit in one
|
||||||
machine word (32 bits in 386 protected mode, 16 bits otherwise), but it
|
machine word (32 bits in 386 protected mode, 16 bits otherwise), but it
|
||||||
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
|
flag to tie together partial results. For example, these instructions
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -100,11 +100,11 @@ LEA EAX,[EAX+EBX]
|
||||||
ADC EDX,ECX
|
ADC EDX,ECX
|
||||||
```
|
```
|
||||||
|
|
||||||
because **LEA** doesn't affect the Carry flag.
|
because `LEA` doesn't affect the Carry flag.
|
||||||
|
|
||||||
The no-carry characteristic of **LEA** becomes a distinct advantage when
|
The no-carry characteristic of `LEA` becomes a distinct advantage when
|
||||||
performing pointer arithmetic, however. For instance, the following code
|
performing pointer arithmetic, however. For instance, the following code
|
||||||
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:
|
variable to another such variable:
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -120,15 +120,15 @@ ADDLOOP:
|
||||||
LOOP ADDLOOP
|
LOOP ADDLOOP
|
||||||
```
|
```
|
||||||
|
|
||||||
(Yes, I could use **LODSD** instead of **MOV/LEA**; I'm just
|
(Yes, I could use `LODSD` instead of `MOV/LEA`; I'm just
|
||||||
illustrating a point here. Besides, **LODS** is only 1 cycle faster than
|
illustrating a point here. Besides, `LODS` is only 1 cycle faster than
|
||||||
**MOV/LEA** on the 386, and is actually more than twice as slow on the
|
`MOV/LEA` on the 386, and is actually more than twice as slow on the
|
||||||
486.) If we used **ADD** rather than **LEA** to advance the pointers,
|
486.) If we used `ADD` rather than `LEA` to advance the pointers,
|
||||||
the carry from one **ADC** to the next would have to be preserved with
|
the carry from one `ADC` to the next would have to be preserved with
|
||||||
either **PUSHF/POPF** or **LAHF/SAHF**. (Alternatively, we could use
|
either `PUSHF/POPF` or `LAHF/SAHF`. (Alternatively, we could use
|
||||||
multiple **INC**s, since **INC** doesn't affect the Carry flag.)
|
multiple `INC`s, since `INC` doesn't affect the Carry flag.)
|
||||||
|
|
||||||
In short, **LEA** is indeed different from **ADD**. Sometimes it's
|
In short, `LEA` is indeed different from `ADD`. Sometimes it's
|
||||||
better. Sometimes not; that's the nature of the various instruction
|
better. Sometimes not; that's the nature of the various instruction
|
||||||
substitutions and optimizations that will occur to you over time.
|
substitutions and optimizations that will occur to you over time.
|
||||||
There's no such thing as "best" instructions on the x86; it all depends
|
There's no such thing as "best" instructions on the x86; it all depends
|
||||||
|
|
@ -180,7 +180,7 @@ ADC CX,CX ;CX=1 if copy length was odd,
|
||||||
REP MOVSB ;copy any odd byte
|
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
|
depends on the processor type.) It might be hard to believe that the
|
||||||
above is faster than this:
|
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
|
However, it generally is. Sure, if the length is odd, John's approach
|
||||||
incurs a penalty approximately equal to the **REP** startup time for
|
incurs a penalty approximately equal to the `REP` startup time for
|
||||||
**MOVSB**. However, if the length is even, John's approach doesn't
|
`MOVSB`. However, if the length is even, John's approach doesn't
|
||||||
branch, saving cycles and not emptying the prefetch queue. If copy
|
branch, saving cycles and not emptying the prefetch queue. If copy
|
||||||
lengths are evenly distributed between even and odd, John's approach is
|
lengths are evenly distributed between even and odd, John's approach is
|
||||||
faster in most x86 systems. (Not on the 486, though.)
|
faster in most x86 systems. (Not on the 486, though.)
|
||||||
|
|
||||||
John also points out that on the 386, multiple **LEA**s can be combined
|
John also points out that on the 386, multiple `LEA`s can be combined
|
||||||
to perform multiplications that can't be handled by a single **LEA**,
|
to perform multiplications that can't be handled by a single `LEA`,
|
||||||
much as multiple shifts and adds can be used for multiplication, only
|
much as multiple shifts and adds can be used for multiplication, only
|
||||||
faster. **LEA** can be used to multiply in a single instruction on the
|
faster. `LEA` can be used to multiply in a single instruction on the
|
||||||
386, but only by the values 2, 3, 4, 5, 8, and 9; several **LEA**s
|
386, but only by the values 2, 3, 4, 5, 8, and 9; several `LEA`s
|
||||||
strung together can handle a much wider range of values. For example,
|
strung together can handle a much wider range of values. For example,
|
||||||
video programmers are undoubtedly familiar with the following code to
|
video programmers are undoubtedly familiar with the following code to
|
||||||
multiply AX times 80 (the width in bytes of the bitmap in most PC
|
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
|
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
|
```nasm
|
||||||
LEA EAX,[EAX*2] ;*2
|
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
|
That brings us to multiplication, one of the slowest of x86 operations
|
||||||
and one that allows for considerable optimization. One way to speed up
|
and one that allows for considerable optimization. One way to speed up
|
||||||
multiplication is to use shift and add, **LEA**, or a lookup table to
|
multiplication is to use shift and add, `LEA`, or a lookup table to
|
||||||
hard-code a multiplication operation for a fixed multiplier, as shown
|
hard-code a multiplication operation for a fixed multiplier, as shown
|
||||||
above. Another is to take advantage of the early-out feature of the 386
|
above. Another is to take advantage of the early-out feature of the 386
|
||||||
(and the 486, but in the interests of brevity I'll just say "386" from
|
(and the 486, but in the interests of brevity I'll just say "386" from
|
||||||
now on) by arranging your operands so that the multiplier (always the
|
now on) by arranging your operands so that the multiplier (always the
|
||||||
rightmost operand following **MUL** or **IMUL**) is no larger than the
|
rightmost operand following `MUL` or `IMUL`) is no larger than the
|
||||||
other operand.
|
other operand.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
|
|
@ -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
|
the specs indicate, but the cycle-per-bit advantage of smaller
|
||||||
multipliers holds true nonetheless.)
|
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
|
386 are so fast that alternative multiplication approaches, while
|
||||||
generally still faster, are worthwhile only in truly time-critical code.
|
generally still faster, are worthwhile only in truly time-critical code.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> On 386SXs and uncached 386s, where code size can significantly affect
|
> On 386SXs and uncached 386s, where code size can significantly affect
|
||||||
> performance due to instruction prefetching, the compact **MUL** and
|
> performance due to instruction prefetching, the compact `MUL` and
|
||||||
> **IMUL** instructions can approach and in some cases even outperform the
|
> `IMUL` instructions can approach and in some cases even outperform the
|
||||||
> "optimized" alternatives.
|
> "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
|
no longer to be avoided in most cases—and you can help that along by
|
||||||
arranging your code to make the smaller operand the multiplier whenever
|
arranging your code to make the smaller operand the multiplier whenever
|
||||||
you know which operand is smaller.
|
you know which operand is smaller.
|
||||||
|
|
@ -121,33 +121,33 @@ scale value is the logical choice for the multiplier.
|
||||||
|
|
||||||
#### Optimizing Optimized Searching {#Heading6}
|
#### Optimizing Optimized Searching {#Heading6}
|
||||||
|
|
||||||
Rob Williams writes with a wonderful optimization to the **REPNZ
|
Rob Williams writes with a wonderful optimization to the `REPNZ
|
||||||
SCASB-**based optimized searching routine I discussed in Chapter 5. As a
|
SCASB`-based optimized searching routine I discussed in Chapter 5. As a
|
||||||
quick refresher, I described searching a buffer for a text string as
|
quick refresher, I described searching a buffer for a text string as
|
||||||
follows: Scan for the first byte of the text string with **REPNZ
|
follows: Scan for the first byte of the text string with `REPNZ
|
||||||
SCASB**, then use **REPZ CMPS** to check for a full match whenever
|
SCASB`, then use `REPZ CMPS` to check for a full match whenever
|
||||||
**REPNZ SCASB** finds a match for the first character, as shown in
|
`REPNZ SCASB` finds a match for the first character, as shown in
|
||||||
Figure 9.1. The principle is that most buffer characters won't match the
|
Figure 9.1. The principle is that most buffer characters won't match the
|
||||||
first character of any given string, so **REPNZ SCASB**, by far the
|
first character of any given string, so `REPNZ SCASB`, by far the
|
||||||
fastest way to search on the PC, can be used to eliminate most potential
|
fastest way to search on the PC, can be used to eliminate most potential
|
||||||
matches; each remaining potential match can then be checked in its
|
matches; each remaining potential match can then be checked in its
|
||||||
entirety with **REPZ CMPS**.
|
entirety with `REPZ CMPS`.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Rob's revelation, which he credits without explanation to Edgar Allen
|
Rob's revelation, which he credits without explanation to Edgar Allen
|
||||||
Poe (search nevermore?), was that by far the slowest part of the whole
|
Poe (search nevermore?), was that by far the slowest part of the whole
|
||||||
deal is handling **REPNZ SCASB** matches, which require checking the
|
deal is handling `REPNZ SCASB` matches, which require checking the
|
||||||
remainder of the string with **REPZ CMPS** and restarting **REPNZ
|
remainder of the string with `REPZ CMPS` and restarting `REPNZ
|
||||||
SCASB** if no match is found.
|
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
|
> reduced simply by scanning for the character in the searched-for string
|
||||||
> that appears least often in the buffer being searched.
|
> that appears least often in the buffer being searched.
|
||||||
|
|
||||||
Imagine, if you will, that you're searching for the string "EQUAL." By
|
Imagine, if you will, that you're searching for the string "EQUAL." By
|
||||||
my approach, you'd use **REPNZ SCASB** to scan for each occurrence of
|
my approach, you'd use `REPNZ SCASB` to scan for each occurrence of
|
||||||
"E," which crops up quite often in normal text. Rob points out that it
|
"E," which crops up quite often in normal text. Rob points out that it
|
||||||
would make more sense to scan for "Q," then back up one character and
|
would make more sense to scan for "Q," then back up one character and
|
||||||
check the whole string when a "Q" is found, as shown in Figure 9.2. "Q"
|
check the whole string when a "Q" is found, as shown in Figure 9.2. "Q"
|
||||||
|
|
|
||||||
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
|
scans for whatever character the caller specifies. Listing 9.3 is a test
|
||||||
program used to compare the two approaches. How much difference does
|
program used to compare the two approaches. How much difference does
|
||||||
Rob's revelation make? Plenty. Even when the entire C function call to
|
Rob's revelation make? Plenty. Even when the entire C function call to
|
||||||
**FindString** is timed—**strlen** calls, parameter pushing, calling,
|
`FindString` is timed—`strlen` calls, parameter pushing, calling,
|
||||||
setup, and all—the version of **FindString** in Listing 9.2, which is
|
setup, and all—the version of `FindString` in Listing 9.2, which is
|
||||||
directed by Listing 9.3 to scan for the infrequently-occurring "Q," is
|
directed by Listing 9.3 to scan for the infrequently-occurring "Q," is
|
||||||
about 40 percent faster on a 20 MHz cached 386 for the test search of
|
about 40 percent faster on a 20 MHz cached 386 for the test search of
|
||||||
Listing 9.3 than is the version of **FindString** in Listing 9.1, which
|
Listing 9.3 than is the version of `FindString` in Listing 9.1, which
|
||||||
always scans for the first character, in this case "E." However, when
|
always scans for the first character, in this case "E." However, when
|
||||||
only the search loops (the code that actually does the searching) in the
|
only the search loops (the code that actually does the searching) in the
|
||||||
two versions of **FindString** are compared, Listing 9.2 is more than
|
two versions of `FindString` are compared, Listing 9.2 is more than
|
||||||
*twice* as fast as Listing 9.1—a remarkable improvement over code that
|
*twice* as fast as Listing 9.1—a remarkable improvement over code that
|
||||||
already uses **REPNZ SCASB** and **REPZ CMPS**.
|
already uses `REPNZ SCASB` and `REPZ CMPS`.
|
||||||
|
|
||||||
What I like so much about Rob's approach is that it demonstrates that
|
What I like so much about Rob's approach is that it demonstrates that
|
||||||
optimization involves much more than instruction selection and cycle
|
optimization involves much more than instruction selection and cycle
|
||||||
counting. Listings 9.1 and 9.2 use pretty much the same instructions,
|
counting. Listings 9.1 and 9.2 use pretty much the same instructions,
|
||||||
and even use the same approach of scanning with **REPNZ SCASB** and
|
and even use the same approach of scanning with `REPNZ SCASB` and
|
||||||
using **REPZ CMPS** to check scanning matches.
|
using `REPZ CMPS` to check scanning matches.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> The difference between Listings 9.1 and 9.2 (which gives you more than a
|
> 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}
|
#### Full 32-Bit Division {#Heading8}
|
||||||
|
|
||||||
One of the most annoying limitations of the x86 is that while the
|
One of the most annoying limitations of the x86 is that while the
|
||||||
dividend operand to the **DIV** instruction can be 32 bits in size, both
|
dividend operand to the `DIV` instruction can be 32 bits in size, both
|
||||||
the divisor and the result must be 16 bits. That's particularly annoying
|
the divisor and the result must be 16 bits. That's particularly annoying
|
||||||
in regards to the result because sometimes you just don't know whether
|
in regards to the result because sometimes you just don't know whether
|
||||||
the ratio of the dividend to the divisor is greater than 64K-1 or
|
the ratio of the dividend to the divisor is greater than 64K-1 or
|
||||||
|
|
@ -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
|
divisor, dividing the absolute value of the dividend by the absolute
|
||||||
value of the divisor, and applying the stored signs to set the proper
|
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
|
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.
|
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
|
Next, we come to an item that cycle counters will love, especially since
|
||||||
it involves apparently incorrect documentation on Intel's part.
|
it involves apparently incorrect documentation on Intel's part.
|
||||||
According to Intel's documents, all **RCR** and **RCL** instructions,
|
According to Intel's documents, all `RCR` and `RCL` instructions,
|
||||||
which perform rotations through the Carry flag, as shown in Figure 9.4,
|
which perform rotations through the Carry flag, as shown in Figure 9.4,
|
||||||
take 9 cycles on the 386 when working with a register operand. My
|
take 9 cycles on the 386 when working with a register operand. My
|
||||||
measurements indicate that the 9-cycle execution time almost holds true
|
measurements indicate that the 9-cycle execution time almost holds true
|
||||||
for *multibit* rotate-through-carries, which I've timed at 8 cycles
|
for *multibit* rotate-through-carries, which I've timed at 8 cycles
|
||||||
apiece; for example, **RCR AX,CL** takes 8 cycles on *my* 386, as does
|
apiece; for example, `RCR AX,CL` takes 8 cycles on *my* 386, as does
|
||||||
**RCL DX,2**. Contrast that with **ROR** and **ROL**, which can rotate
|
`RCL DX,2`. Contrast that with `ROR` and `ROL`, which can rotate
|
||||||
the contents of a register any number of bits in just 3 cycles.
|
the contents of a register any number of bits in just 3 cycles.
|
||||||
|
|
||||||
However, rotating by one bit through the Carry flag does *not* take 9
|
However, rotating by one bit through the Carry flag does *not* take 9
|
||||||
cycles, contrary to Intel's *80386 Programmer's Reference Manual*, or
|
cycles, contrary to Intel's *80386 Programmer's Reference Manual*, or
|
||||||
even 8 cycles. In fact, **RCR** *reg*,1 and **RCL** *reg*,1 take 3
|
even 8 cycles. In fact, `RCR` *reg*,1 and `RCL` *reg*,1 take 3
|
||||||
cycles, just like **ROR, ROL, SHR,** and **SHL**. At least, that's how
|
cycles, just like `ROR`, `ROL`, `SHR`, and `SHL`. At least, that's how
|
||||||
fast they run on my 386, and I very much doubt that you'll find
|
fast they run on my 386, and I very much doubt that you'll find
|
||||||
different execution times on other 386s. (Please let me know if you do,
|
different execution times on other 386s. (Please let me know if you do,
|
||||||
though!)
|
though!)
|
||||||
|
|
@ -34,12 +34,12 @@ though!)
|
||||||

|

|
||||||
|
|
||||||
Interestingly, according to Intel's *i486 Microprocessor Programmer's
|
Interestingly, according to Intel's *i486 Microprocessor Programmer's
|
||||||
Reference Manual*, the 486 can **RCR** or **RCL** a register by one bit
|
Reference Manual*, the 486 can `RCR` or `RCL` a register by one bit
|
||||||
in 3 cycles, but takes between 8 and 30 cycles to perform a multibit
|
in 3 cycles, but takes between 8 and 30 cycles to perform a multibit
|
||||||
register **RCR** or **RCL**!
|
register `RCR` or `RCL`!
|
||||||
|
|
||||||
No great lesson here, just a caution to be leery of multibit **RCR** and
|
No great lesson here, just a caution to be leery of multibit `RCR` and
|
||||||
**RCL** when performance matters—and to take cycle-time documentation
|
`RCL` when performance matters—and to take cycle-time documentation
|
||||||
with a grain of salt.
|
with a grain of salt.
|
||||||
|
|
||||||
#### Hardwired Far Jumps {#Heading11}
|
#### 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
|
Did you ever wonder how to code a far jump to an absolute address in
|
||||||
assembly language? Probably not, but if you ever do, you're going to be
|
assembly language? Probably not, but if you ever do, you're going to be
|
||||||
glad for this next item, because the obvious solution doesn't work. You
|
glad for this next item, because the obvious solution doesn't work. You
|
||||||
might think all it would take to jump to, say, 1000:5 would be **JMP FAR
|
might think all it would take to jump to, say, 1000:5 would be `JMP FAR
|
||||||
PTR 1000:5**, but you'd be wrong. That won't even assemble. You might
|
PTR 1000:5`, but you'd be wrong. That won't even assemble. You might
|
||||||
then think to construct in memory a far pointer containing 1000:5, as in
|
then think to construct in memory a far pointer containing 1000:5, as in
|
||||||
the following:
|
the following:
|
||||||
|
|
||||||
|
|
@ -60,9 +60,9 @@ Ptr dd ?
|
||||||
jmp [Ptr]
|
jmp [Ptr]
|
||||||
```
|
```
|
||||||
|
|
||||||
That will work, but at a price in performance. On an 8088, **JMP DWORD
|
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
|
PTR [*mem*]` (an indirect far jump) takes at least 37 cycles; `JMP
|
||||||
DWORD PTR *label*** (a direct far jump) takes only 15 cycles (plus,
|
DWORD PTR *label*` (a direct far jump) takes only 15 cycles (plus,
|
||||||
almost certainly, some cycles for instruction fetching). On a 386, an
|
almost certainly, some cycles for instruction fetching). On a 386, an
|
||||||
indirect far jump is documented to take at least 43 cycles in real mode
|
indirect far jump is documented to take at least 43 cycles in real mode
|
||||||
(31 in protected mode); a direct far jump is documented to take at least
|
(31 in protected mode); a direct far jump is documented to take at least
|
||||||
|
|
@ -81,7 +81,7 @@ preferable.
|
||||||
Listing 9.7 shows a short program that performs a direct far call to
|
Listing 9.7 shows a short program that performs a direct far call to
|
||||||
1000:5. (Don't run it, unless you want to crash your system!) It does
|
1000:5. (Don't run it, unless you want to crash your system!) It does
|
||||||
this by creating a dummy segment at 1000H, so that the label
|
this by creating a dummy segment at 1000H, so that the label
|
||||||
**FarLabel** can be created with the desired far attribute at the proper
|
`FarLabel` can be created with the desired far attribute at the proper
|
||||||
location. (Segments created with "AT" don't cause the generation of any
|
location. (Segments created with "AT" don't cause the generation of any
|
||||||
actual bytes or the allocation of any memory; they're just templates.)
|
actual bytes or the allocation of any memory; they're just templates.)
|
||||||
It's a little kludgey, but at least it does work. There may be a better
|
It's a little kludgey, but at least it does work. There may be a better
|
||||||
|
|
@ -132,8 +132,8 @@ mov eax,1
|
||||||
```
|
```
|
||||||
|
|
||||||
takes only 2 cycles to execute, but is 5 bytes long (because native mode
|
takes only 2 cycles to execute, but is 5 bytes long (because native mode
|
||||||
constants are dwords and the **MOV** instruction doesn't sign-extend).
|
constants are dwords and the `MOV` instruction doesn't sign-extend).
|
||||||
Both code fragments are ways to set **EAX** to 1 (although the first
|
Both code fragments are ways to set `EAX` to 1 (although the first
|
||||||
affects the flags and the second doesn't); this is a classic trade-off
|
affects the flags and the second doesn't); this is a classic trade-off
|
||||||
of speed for space. Second,
|
of speed for space. Second,
|
||||||
|
|
||||||
|
|
@ -148,15 +148,15 @@ move bx,-1
|
||||||
```
|
```
|
||||||
|
|
||||||
takes 2 cycles to execute and is 5 bytes long. Both instructions set
|
takes 2 cycles to execute and is 5 bytes long. Both instructions set
|
||||||
**EBX** to -1; this is a classic trade-off of—gee, it's not a trade-off
|
`EBX` to -1; this is a classic trade-off of—gee, it's not a trade-off
|
||||||
at all, is it? **OR** is a better way to set a 32-bit register to all
|
at all, is it? `OR` is a better way to set a 32-bit register to all
|
||||||
1-bits, just as **SUB** or **XOR** is a better way to set a register to
|
1-bits, just as `SUB` or `XOR` is a better way to set a register to
|
||||||
all 0-bits. Who woulda thunk it? Just goes to show how the 32-bit
|
all 0-bits. Who woulda thunk it? Just goes to show how the 32-bit
|
||||||
displacements and constants of 386 native mode change the familiar
|
displacements and constants of 386 native mode change the familiar
|
||||||
landscape of 80x86 optimization.
|
landscape of 80x86 optimization.
|
||||||
|
|
||||||
Be warned, though, that I've found **OR, AND, ADD**, and the like to be
|
Be warned, though, that I've found `OR`, `AND`, `ADD`, and the like to be
|
||||||
a cycle slower than **MOV** when working with immediate operands on the
|
a cycle slower than `MOV` when working with immediate operands on the
|
||||||
386 under some circumstances, for reasons that thus far escape me. This
|
386 under some circumstances, for reasons that thus far escape me. This
|
||||||
just reinforces the first rule of optimization: Measure your code in
|
just reinforces the first rule of optimization: Measure your code in
|
||||||
action, and place not your trust in documented cycle times.
|
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
|
mov dx,es
|
||||||
```
|
```
|
||||||
|
|
||||||
which loads **LongVar** into DX:AX faster than this:
|
which loads `LongVar` into DX:AX faster than this:
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
mov ax,word ptr [LongVar]
|
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.
|
fetching.
|
||||||
|
|
||||||
Consider this: The 286 can store an immediate value to memory, as in
|
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;
|
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
|
however, the one-wait-state architecture of the AT stretches that to 3
|
||||||
cycles. Consequently, nine cycles are needed to fetch the six
|
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.
|
time is in a class with the best (or worst!) that's found on the 8088.
|
||||||
|
|
||||||
Let's check out the prefetch queue cycle-eater in action. Listing 11.1
|
Let's check out the prefetch queue cycle-eater in action. Listing 11.1
|
||||||
times **MOV [WordVar],0**. The Zen timer reports that on a
|
times `MOV [WordVar],0`. The Zen timer reports that on a
|
||||||
one-wait-state 10 MHz 286-based AT clone (the computer used for all
|
one-wait-state 10 MHz 286-based AT clone (the computer used for all
|
||||||
tests in this chapter), Listing 11.1 runs in 1.27 µs per instruction.
|
tests in this chapter), Listing 11.1 runs in 1.27 µs per instruction.
|
||||||
That's 12.7 cycles per instruction, just as we calculated. (That extra
|
That's 12.7 cycles per instruction, just as we calculated. (That extra
|
||||||
|
|
|
||||||
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:
|
The way to deal with the data alignment cycle-eater is straightforward:
|
||||||
*Don't perform word-sized accesses to odd addresses on the 286 if you
|
*Don't perform word-sized accesses to odd addresses on the 286 if you
|
||||||
can help it*. The easiest way to avoid the data alignment cycle-eater is
|
can help it*. The easiest way to avoid the data alignment cycle-eater is
|
||||||
to place the directive **EVEN** before each of your word-sized
|
to place the directive `EVEN` before each of your word-sized
|
||||||
variables. **EVEN** forces the offset of the next byte assembled to be
|
variables. `EVEN` forces the offset of the next byte assembled to be
|
||||||
even by inserting a **NOP** if the current offset is odd; consequently,
|
even by inserting a `NOP` if the current offset is odd; consequently,
|
||||||
you can ensure that any word-sized variable can be accessed efficiently
|
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
|
Listing 11.2, which accesses memory a word at a time with each word
|
||||||
starting at an odd address, runs on a 10 MHz AT clone in 1.27 ms per
|
starting at an odd address, runs on a 10 MHz AT clone in 1.27 ms per
|
||||||
repetition of **MOVSW**, or 0.64 ms per word-sized memory access. That's
|
repetition of `MOVSW`, or 0.64 ms per word-sized memory access. That's
|
||||||
6-plus cycles per word-sized access, which breaks down to two separate
|
6-plus cycles per word-sized access, which breaks down to two separate
|
||||||
memory accesses—3 cycles to access the high byte of each word and 3
|
memory accesses—3 cycles to access the high byte of each word and 3
|
||||||
cycles to access the low byte of each word, the inevitable result of
|
cycles to access the low byte of each word, the inevitable result of
|
||||||
|
|
@ -123,7 +123,7 @@ Skip:
|
||||||
|
|
||||||
On the other hand, Listing 11.3, which is exactly the same as Listing
|
On the other hand, Listing 11.3, which is exactly the same as Listing
|
||||||
11.2 save that the memory accesses are word-aligned (start at even
|
11.2 save that the memory accesses are word-aligned (start at even
|
||||||
addresses), runs in 0.64 ms per repetition of **MOVSW**, or 0.32 µs per
|
addresses), runs in 0.64 ms per repetition of `MOVSW`, or 0.32 µs per
|
||||||
word-sized memory access. That's 3 cycles per word-sized access—exactly
|
word-sized memory access. That's 3 cycles per word-sized access—exactly
|
||||||
twice as fast as the non-word-aligned accesses of Listing 11.2, just as
|
twice as fast as the non-word-aligned accesses of Listing 11.2, just as
|
||||||
we predicted.
|
we predicted.
|
||||||
|
|
|
||||||
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
|
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
|
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
|
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
|
one word-aligned word, and another to get the displacement byte, which
|
||||||
resided at the start of the next word-aligned word.
|
resided at the start of the next word-aligned word.
|
||||||
|
|
||||||
|
|
@ -51,8 +51,8 @@ LoopTop:
|
||||||
|
|
||||||
While word-aligning branch destinations can improve branching
|
While word-aligning branch destinations can improve branching
|
||||||
performance, it's a nuisance and can increase code size a good deal, so
|
performance, it's a nuisance and can increase code size a good deal, so
|
||||||
it's not worth doing in most code. Besides, **EVEN** inserts a **NOP**
|
it's not worth doing in most code. Besides, `EVEN` inserts a `NOP`
|
||||||
instruction if necessary, and the time required to execute a **NOP** can
|
instruction if necessary, and the time required to execute a `NOP` can
|
||||||
sometimes cancel the performance advantage of having a word-aligned
|
sometimes cancel the performance advantage of having a word-aligned
|
||||||
branch destination.
|
branch destination.
|
||||||
|
|
||||||
|
|
@ -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
|
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
|
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
|
protected mode on the 386, 486, or Pentium) will significantly reduce
|
||||||
the performance of **PUSH,** **POP,** **CALL**, and **RET**, as well as
|
the performance of `PUSH`, `POP`, `CALL`, and `RET`, as well as
|
||||||
**INT** and **IRET**, which are executed to invoke DOS and BIOS
|
`INT` and `IRET`, which are executed to invoke DOS and BIOS
|
||||||
functions, handle keystrokes and incoming serial characters, and manage
|
functions, handle keystrokes and incoming serial characters, and manage
|
||||||
the mouse. I know of a Forth programmer who vastly improved the
|
the mouse. I know of a Forth programmer who vastly improved the
|
||||||
performance of a complex application on the AT simply by forcing the
|
performance of a complex application on the AT simply by forcing the
|
||||||
Forth interpreter to maintain an even stack pointer at all times.
|
Forth interpreter to maintain an even stack pointer at all times.
|
||||||
|
|
||||||
An interesting corollary to this rule is that you shouldn't **INC SP**
|
An interesting corollary to this rule is that you shouldn't `INC SP`
|
||||||
twice to add 2, even though that takes fewer bytes than **ADD SP,2**.
|
twice to add 2, even though that takes fewer bytes than `ADD SP,2`.
|
||||||
The stack pointer is odd between the first and second **INC**, so any
|
The stack pointer is odd between the first and second `INC`, so any
|
||||||
interrupt occurring between the two instructions will be serviced more
|
interrupt occurring between the two instructions will be serviced more
|
||||||
slowly than it normally would. The same goes for decrementing twice; use
|
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.
|
> 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
|
discussion of the matter in Chapter 4, display adapters offer only a
|
||||||
limited number of accesses to display memory during any given period of
|
limited number of accesses to display memory during any given period of
|
||||||
time. The 8088 is capable of making use of most but not all of those
|
time. The 8088 is capable of making use of most but not all of those
|
||||||
slots with **REP MOVSW**, so the number of memory accesses allowed by a
|
slots with `REP MOVSW`, so the number of memory accesses allowed by a
|
||||||
display adapter such as a standard VGA is reasonably well-matched to an
|
display adapter such as a standard VGA is reasonably well-matched to an
|
||||||
8088's memory access speed. Granted, access to a VGA slows the 8088 down
|
8088's memory access speed. Granted, access to a VGA slows the 8088 down
|
||||||
considerably—but, as we're about to find out, "considerably" is a
|
considerably—but, as we're about to find out, "considerably" is a
|
||||||
|
|
|
||||||
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
|
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
|
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
|
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
|
anything but ideal for a 286. For one thing, most display adapters are
|
||||||
8-bit devices, although newer adapters are 16-bit in nature. One
|
8-bit devices, although newer adapters are 16-bit in nature. One
|
||||||
|
|
@ -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
|
memory. Far more cycles are eaten by the inherent memory-access
|
||||||
limitations of display adapters—that is, the limited number of display
|
limitations of display adapters—that is, the limited number of display
|
||||||
memory accesses that display adapters make available to the 286. Look at
|
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
|
available accesses to display memory, then how much faster can code
|
||||||
running on a 286 or 386 possibly run when accessing display memory?
|
running on a 286 or 386 possibly run when accessing display memory?
|
||||||
|
|
||||||
|
|
@ -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.)
|
either mode, each segment is still limited to 64K.)
|
||||||
|
|
||||||
There are also a handful of 286-specific real-mode instructions, and
|
There are also a handful of 286-specific real-mode instructions, and
|
||||||
they can be quite useful. **BOUND** checks array bounds. **ENTER** and
|
they can be quite useful. `BOUND` checks array bounds. `ENTER` and
|
||||||
**LEAVE** support compact and speedy stack frame construction and
|
`LEAVE` support compact and speedy stack frame construction and
|
||||||
removal, ideal for interfacing to high-level languages such as C and
|
removal, ideal for interfacing to high-level languages such as C and
|
||||||
Pascal (although these instructions are actually relatively slow on the
|
Pascal (although these instructions are actually relatively slow on the
|
||||||
386 and its successors, and should be used with caution when performance
|
386 and its successors, and should be used with caution when performance
|
||||||
matters). **INS** and **OUTS** are new string instructions that support
|
matters). `INS` and `OUTS` are new string instructions that support
|
||||||
efficient data transfer between memory and I/O ports. Finally, **PUSHA**
|
efficient data transfer between memory and I/O ports. Finally, `PUSHA`
|
||||||
and **POPA** push and pop all eight general-purpose registers.
|
and `POPA` push and pop all eight general-purpose registers.
|
||||||
|
|
||||||
A couple of old instructions gain new features on the 286. For one, the
|
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
|
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*
|
not just 1 bit or the number of bits specified by CL, but for *any*
|
||||||
constant number of bits.
|
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
|
of simple instructions on the 486 and especially on the Pentium.) What's
|
||||||
more, both old and new instructions support 32-bit operations on the
|
more, both old and new instructions support 32-bit operations on the
|
||||||
386. For example, it's relatively simple to copy data in chunks of 4
|
386. For example, it's relatively simple to copy data in chunks of 4
|
||||||
bytes on a 386, even in real mode, by using the **MOVSD** ("move string
|
bytes on a 386, even in real mode, by using the `MOVSD` ("move string
|
||||||
double") instruction, or to negate a 32-bit value with **NEG eax**.
|
double") instruction, or to negate a 32-bit value with `NEG eax`.
|
||||||
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
|
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
|
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
|
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
|
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.
|
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)
|
improvement is the near elimination of effective address (EA)
|
||||||
calculation time. Where an 8088 takes from 5 to 12 cycles to calculate
|
calculation time. Where an 8088 takes from 5 to 12 cycles to calculate
|
||||||
an EA, a 286 or 386 usually takes no time whatsoever to perform the
|
an EA, a 286 or 386 usually takes no time whatsoever to perform the
|
||||||
calculation. If a base+index+displacement addressing mode, such as **MOV
|
calculation. If a base+index+displacement addressing mode, such as `MOV
|
||||||
AX,[WordArray+bx+si]**, is used on a 286 or 386, 1 cycle is taken to
|
AX,[WordArray+bx+si]`, is used on a 286 or 386, 1 cycle is taken to
|
||||||
perform the EA calculation, but that's both the worst case and the only
|
perform the EA calculation, but that's both the worst case and the only
|
||||||
case in which there's any EA overhead at all.
|
case in which there's any EA overhead at all.
|
||||||
|
|
||||||
The elimination of EA calculation time means that the EU execution time
|
The elimination of EA calculation time means that the EU execution time
|
||||||
of memory-addressing instructions is much closer to the EU execution
|
of memory-addressing instructions is much closer to the EU execution
|
||||||
time of register-only instructions. For instance, on the 8088 **ADD
|
time of register-only instructions. For instance, on the 8088 `ADD
|
||||||
[WordVar],100H** is a 31-cycle instruction, while **ADD DX,100H** is a
|
[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
|
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.
|
is a 3-cycle instruction—a ratio of just 2.3 to 1.
|
||||||
|
|
||||||
It would seem, then, that it's less necessary to use the registers on
|
It would seem, then, that it's less necessary to use the registers on
|
||||||
|
|
@ -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
|
memory-addressing instructions so fast that there's no spare instruction
|
||||||
prefetching time during those instructions, so the prefetch queue runs
|
prefetching time during those instructions, so the prefetch queue runs
|
||||||
dry, especially on the AT, with its one-wait-state memory. On the AT,
|
dry, especially on the AT, with its one-wait-state memory. On the AT,
|
||||||
the 6-byte instruction **ADD [WordVar],100H** is effectively at least a
|
the 6-byte instruction `ADD [WordVar],100H` is effectively at least a
|
||||||
15-cycle instruction, because 3 cycles are needed to fetch each of the
|
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.
|
and write the result back to memory.
|
||||||
|
|
||||||
Granted, the register-only instruction **ADD DX,100H** also slows
|
Granted, the register-only instruction `ADD DX,100H` also slows
|
||||||
down—to 6 cycles—because of instruction prefetching, leaving a ratio of
|
down—to 6 cycles—because of instruction prefetching, leaving a ratio of
|
||||||
2.5 to 1. Now, however, let's look at the performance of the same code
|
2.5 to 1. Now, however, let's look at the performance of the same code
|
||||||
on an 8088. The register-only code would run in 16 cycles (4 instruction
|
on an 8088. The register-only code would run in 16 cycles (4 instruction
|
||||||
|
|
@ -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
|
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
|
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
|
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.
|
AT than it does on the PC in this case.
|
||||||
|
|
||||||
Theory confirmed.
|
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}
|
#### POPF and the 286 {#Heading17}
|
||||||
|
|
||||||
We've one final 286-related item to discuss: the hardware malfunction of
|
We've one final 286-related item to discuss: the hardware malfunction of
|
||||||
**POPF** under certain circumstances on the 286.
|
`POPF` under certain circumstances on the 286.
|
||||||
|
|
||||||
The problem is this: Sometimes **POPF** permits interrupts to occur when
|
The problem is this: Sometimes `POPF` permits interrupts to occur when
|
||||||
interrupts are initially off and the setting popped into the Interrupt
|
interrupts are initially off and the setting popped into the Interrupt
|
||||||
flag from the stack keeps interrupts off. In other words, an interrupt
|
flag from the stack keeps interrupts off. In other words, an interrupt
|
||||||
can happen even though the Interrupt flag is never set to 1. Now, I
|
can happen even though the Interrupt flag is never set to 1. Now, I
|
||||||
|
|
@ -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
|
For example, a critical portion of a disk BIOS might need to retrieve
|
||||||
data from the disk controller the instant it becomes available; even a
|
data from the disk controller the instant it becomes available; even a
|
||||||
few hundred microseconds of delay could result in a sector's worth of
|
few hundred microseconds of delay could result in a sector's worth of
|
||||||
data misread. In this case, one misplaced interrupt during a **POPF**
|
data misread. In this case, one misplaced interrupt during a `POPF`
|
||||||
could result in a trashed hard disk if that interrupt occurs while the
|
could result in a trashed hard disk if that interrupt occurs while the
|
||||||
disk BIOS is reading a sector of the File Allocation Table.
|
disk BIOS is reading a sector of the File Allocation Table.
|
||||||
|
|
||||||
There is a workaround for the **POPF** bug. While the workaround is easy
|
There is a workaround for the `POPF` bug. While the workaround is easy
|
||||||
to use, it's considerably slower than **POPF**, and costs a few bytes as
|
to use, it's considerably slower than `POPF`, and costs a few bytes as
|
||||||
well, so you won't want to use it in code that can tolerate interrupts.
|
well, so you won't want to use it in code that can tolerate interrupts.
|
||||||
On the other hand, in code that truly cannot be interrupted, you should
|
On the other hand, in code that truly cannot be interrupted, you should
|
||||||
view those extra cycles and bytes as cheap insurance against mysterious
|
view those extra cycles and bytes as cheap insurance against mysterious
|
||||||
and erratic program crashes.
|
and erratic program crashes.
|
||||||
|
|
||||||
One obvious reason to discuss the **POPF** workaround is that it's
|
One obvious reason to discuss the `POPF` workaround is that it's
|
||||||
useful. Another reason is that the workaround is an excellent example of
|
useful. Another reason is that the workaround is an excellent example of
|
||||||
Zen-level assembly coding, in that there's a well-defined goal to be
|
Zen-level assembly coding, in that there's a well-defined goal to be
|
||||||
achieved but no obvious way to do so. The goal is to reproduce the
|
achieved but no obvious way to do so. The goal is to reproduce the
|
||||||
functionality of the **POPF** instruction without using **POPF**, and
|
functionality of the `POPF` instruction without using `POPF`, and
|
||||||
the place to start is by asking exactly what **POPF** does.
|
the place to start is by asking exactly what `POPF` does.
|
||||||
|
|
||||||
All **POPF** does is pop the word on top of the stack into the FLAGS
|
All `POPF` does is pop the word on top of the stack into the FLAGS
|
||||||
register, as shown in Figure 11.4. How can we do that without **POPF**?
|
register, as shown in Figure 11.4. How can we do that without `POPF`?
|
||||||
Of course, the 286's designers intended us to use **POPF** for this
|
Of course, the 286's designers intended us to use `POPF` for this
|
||||||
purpose, and didn't intentionally provide any alternative approach, so
|
purpose, and didn't intentionally provide any alternative approach, so
|
||||||
we'll have to devise an alternative approach of our own. To do that,
|
we'll have to devise an alternative approach of our own. To do that,
|
||||||
we'll have to search for instructions that contain some of the same
|
we'll have to search for instructions that contain some of the same
|
||||||
functionality as **POPF**, in the hope that one of those instructions
|
functionality as `POPF`, in the hope that one of those instructions
|
||||||
can be used in some way to replace **POPF**.
|
can be used in some way to replace `POPF`.
|
||||||
40
11-08.md
40
11-08.md
|
|
@ -10,26 +10,26 @@ chapter: '11'
|
||||||
pages: 226-231
|
pages: 226-231
|
||||||
---
|
---
|
||||||
|
|
||||||
Well, there's only one instruction other than **POPF** that loads the
|
Well, there's only one instruction other than `POPF` that loads the
|
||||||
FLAGS register directly from the stack, and that's **IRET**, which loads
|
FLAGS register directly from the stack, and that's `IRET`, which loads
|
||||||
the FLAGS register from the stack as it branches, as shown in Figure
|
the FLAGS register from the stack as it branches, as shown in Figure
|
||||||
11.5. iret has no known bugs of the sort that plague **POPF**, so it's
|
11.5. iret has no known bugs of the sort that plague `POPF`, so it's
|
||||||
certainly a candidate to replace popf in non-interruptible applications.
|
certainly a candidate to replace popf in non-interruptible applications.
|
||||||
Unfortunately, **IRET** loads the FLAGS register with the *third* word
|
Unfortunately, `IRET` loads the FLAGS register with the *third* word
|
||||||
down on the stack, not the word on top of the stack, as is the case with
|
down on the stack, not the word on top of the stack, as is the case with
|
||||||
**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
|
between the top of the stack and the word popped into the FLAGS
|
||||||
register.
|
register.
|
||||||
|
|
||||||
Obviously, the segment:offset that **IRET** expects to find on the stack
|
Obviously, the segment:offset that `IRET` expects to find on the stack
|
||||||
above the pushed flags isn't present when the stack is set up for
|
above the pushed flags isn't present when the stack is set up for
|
||||||
**POPF**, so we'll have to adjust the stack a bit before we can
|
`POPF`, so we'll have to adjust the stack a bit before we can
|
||||||
substitute **IRET** for **POPF**. What we'll have to do is push the
|
substitute `IRET` for `POPF`. What we'll have to do is push the
|
||||||
segment:offset of the instruction after our workaround code onto the
|
segment:offset of the instruction after our workaround code onto the
|
||||||
stack right above the pushed flags. **IRET** will then branch to that
|
stack right above the pushed flags. `IRET` will then branch to that
|
||||||
address and pop the flags, ending up at the instruction after the
|
address and pop the flags, ending up at the instruction after the
|
||||||
workaround code with the flags popped. That's just the result that would
|
workaround code with the flags popped. That's just the result that would
|
||||||
have occurred had we executed **POPF**—WITH the bonus that no interrupts
|
have occurred had we executed `POPF`—WITH the bonus that no interrupts
|
||||||
can accidentally occur when the Interrupt flag is 0 both before and
|
can accidentally occur when the Interrupt flag is 0 both before and
|
||||||
after the pop.
|
after the pop.
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ after the pop.
|
||||||
How can we push the segment:offset of the next instruction? Well,
|
How can we push the segment:offset of the next instruction? Well,
|
||||||
finding the offset of the next instruction by performing a near call to
|
finding the offset of the next instruction by performing a near call to
|
||||||
that instruction is a tried-and-true trick. We can do something similar
|
that instruction is a tried-and-true trick. We can do something similar
|
||||||
here, but in this case we need a far call, since **IRET** requires both
|
here, but in this case we need a far call, since `IRET` requires both
|
||||||
a segment and an offset. We'll also branch backward so that the address
|
a segment and an offset. We'll also branch backward so that the address
|
||||||
pushed on the stack will point to the instruction we want to continue
|
pushed on the stack will point to the instruction we want to continue
|
||||||
with. The code works out like this:
|
with. The code works out like this:
|
||||||
|
|
@ -66,7 +66,7 @@ popfskip:
|
||||||
|
|
||||||
The operation of this code is illustrated in Figure 11.6.
|
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
|
emulate a far call by pushing CS and performing a near call, thereby
|
||||||
shrinking the workaround code by 1 byte:
|
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
|
By the way, the flags can be popped much more quickly if you're willing
|
||||||
to alter a register in the process. For example, the following macro
|
to alter a register in the process. For example, the following macro
|
||||||
emulates **POPF** with just one branch, but wipes out AX:
|
emulates `POPF` with just one branch, but wipes out AX:
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
EMULATE_POPF_TRASH_AX macro
|
EMULATE_POPF_TRASH_AX macro
|
||||||
|
|
@ -95,11 +95,11 @@ EMULATE_POPF_TRASH_AX macro
|
||||||
endm
|
endm
|
||||||
```
|
```
|
||||||
|
|
||||||
It's not a perfect substitute for **POPF**, since **POPF** doesn't alter
|
It's not a perfect substitute for `POPF`, since `POPF` doesn't alter
|
||||||
any registers, but it's faster and shorter than **EMULATE\_POPF** when
|
any registers, but it's faster and shorter than `EMULATE_POPF` when
|
||||||
you can spare the register. If you're using 286-specific instructions,
|
you can spare the register. If you're using 286-specific instructions,
|
||||||
you can use which is shorter still, alters no registers, and branches
|
you can use which is shorter still, alters no registers, and branches
|
||||||
just once. (Of course, this version of **EMULATE\_POPF** won't work on
|
just once. (Of course, this version of `EMULATE_POPF` won't work on
|
||||||
an 8088.)
|
an 8088.)
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -114,15 +114,15 @@ EMULATE_POPFmacro
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The standard version of **EMULATE\_POPF** is 6 bytes longer than
|
The standard version of `EMULATE_POPF` is 6 bytes longer than
|
||||||
**POPF** and much slower, as you'd expect given that it involves three
|
`POPF` and much slower, as you'd expect given that it involves three
|
||||||
branches. Anyone in his/her right mind would prefer **POPF** to a
|
branches. Anyone in his/her right mind would prefer `POPF` to a
|
||||||
larger, slower, three-branch macro—given a choice. In noncode, however,
|
larger, slower, three-branch macro—given a choice. In noncode, however,
|
||||||
there's no choice here; the safer—if slower—approach is the best.
|
there's no choice here; the safer—if slower—approach is the best.
|
||||||
(Having people associate your programs with crashed computers is *not* a
|
(Having people associate your programs with crashed computers is *not* a
|
||||||
desirable situation, no matter how unfair the circumstances under which
|
desirable situation, no matter how unfair the circumstances under which
|
||||||
it occurs.)
|
it occurs.)
|
||||||
|
|
||||||
And now you know the nature of and the workaround for the **POPF** bug.
|
And now you know the nature of and the workaround for the `POPF` bug.
|
||||||
Whether you ever need the workaround or not, it's a neatly packaged
|
Whether you ever need the workaround or not, it's a neatly packaged
|
||||||
example of the tremendous flexibility of the x86 instruction set.
|
example of the tremendous flexibility of the x86 instruction set.
|
||||||
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
|
an idea of just how different the optimization rules for the 486 are
|
||||||
from what you're used to. Okay, it's not quite *that* bad—but upon
|
from what you're used to. Okay, it's not quite *that* bad—but upon
|
||||||
encountering a processor where string instructions are often to be
|
encountering a processor where string instructions are often to be
|
||||||
avoided and memory-to-register **MOV**s are frequently as fast as
|
avoided and memory-to-register `MOV`s are frequently as fast as
|
||||||
register-to-register **MOV**s, Dorothy was heard to exclaim (before she
|
register-to-register `MOV`s, Dorothy was heard to exclaim (before she
|
||||||
sank out of sight in a swirl of hopelessly mixed metaphors), "I don't
|
sank out of sight in a swirl of hopelessly mixed metaphors), "I don't
|
||||||
think we're in Kansas anymore, Toto."
|
think we're in Kansas anymore, Toto."
|
||||||
|
|
||||||
|
|
@ -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
|
making 486 optimization unique. Simple, common instructions are executed
|
||||||
in a single cycle by a RISC-like core processor, but other instructions
|
in a single cycle by a RISC-like core processor, but other instructions
|
||||||
are executed pretty much as they were on the 386, where every
|
are executed pretty much as they were on the 386, where every
|
||||||
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
|
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
|
cycle *slower* than on the 386. The floating-point execution unit inside
|
||||||
the 486 is also much faster than the 387 math coprocessor, largely
|
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
|
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
|
coprocessor built in), it is more tightly coupled. The results are
|
||||||
sometimes startling: **FMUL** (floating point multiply) is usually
|
sometimes startling: `FMUL` (floating point multiply) is usually
|
||||||
faster on the 486 than **IMUL** (integer multiply)!
|
faster on the 486 than `IMUL` (integer multiply)!
|
||||||
|
|
||||||
An encyclopedic approach to 486 optimization would take a book all by
|
An encyclopedic approach to 486 optimization would take a book all by
|
||||||
itself, so in this chapter I'm only going to hit the highlights of 486
|
itself, so in this chapter I'm only going to hit the highlights of 486
|
||||||
|
|
@ -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,
|
however. In real mode, indexed addressing means that two registers,
|
||||||
rather than one or none, are used to point to memory. (In this context,
|
rather than one or none, are used to point to memory. (In this context,
|
||||||
the use of one register to address memory is "base addressing," no
|
the use of one register to address memory is "base addressing," no
|
||||||
matter what register is used.) **MOV AX, [BX+DI]** and **MOV CL,
|
matter what register is used.) `MOV AX, [BX+DI]` and `MOV CL,
|
||||||
[BP+SI+10]** perform indexed addressing; **MOV AX,[BX]** and **MOV DL,
|
[BP+SI+10]` perform indexed addressing; `MOV AX,[BX]` and `MOV DL,
|
||||||
[SI+1]** do not.
|
[SI+1]` do not.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> Therefore, in real mode, the rule is to avoid using two registers to
|
> 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.
|
as the first example, but avoids indexed addressing.
|
||||||
|
|
||||||
In protected mode, the definition of indexed addressing is a tad more
|
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,
|
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
|
[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
|
use of 386/486 scaled addressing, as in `MOV [ECX*2],EAX`, also
|
||||||
constitutes indexed addressing, even if only one register is used to
|
constitutes indexed addressing, even if only one register is used to
|
||||||
point to memory.
|
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
|
one cycle could make. After all, on the 8088, effective address
|
||||||
calculations take a *minimum* of 5 cycles. On the 486, however, 1 cycle
|
calculations take a *minimum* of 5 cycles. On the 486, however, 1 cycle
|
||||||
is a big deal because many instructions, including most register-only
|
is a big deal because many instructions, including most register-only
|
||||||
instructions (**MOV**, **ADD**, **CMP**, and so on) execute in just 1
|
instructions (`MOV`, `ADD`, `CMP`, and so on) execute in just 1
|
||||||
cycle. In particular, **MOV**s to and from memory execute in 1 cycle—if
|
cycle. In particular, `MOV`s to and from memory execute in 1 cycle—if
|
||||||
they're not hampered by something like indexed addressing, in which case
|
they're not hampered by something like indexed addressing, in which case
|
||||||
they slow to half speed (or worse, as we will see shortly).
|
they slow to half speed (or worse, as we will see shortly).
|
||||||
|
|
||||||
|
|
@ -64,7 +64,7 @@ MOV AX,[BX]
|
||||||
```
|
```
|
||||||
|
|
||||||
there's no way that the 486 can calculate the address referenced by
|
there's no way that the 486 can calculate the address referenced by
|
||||||
**MOV AX,[BX]** until **MOV BX,OFFSET MemVar** finishes, so pipelining
|
`MOV AX,[BX]` until `MOV BX,OFFSET MemVar` finishes, so pipelining
|
||||||
that calculation ahead of time is not possible. A good workaround is
|
that calculation ahead of time is not possible. A good workaround is
|
||||||
rearranging your code so that at least one instruction lies between the
|
rearranging your code so that at least one instruction lies between the
|
||||||
loading of the memory pointer and its use. For example,
|
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
|
In 32-bit protected mode, however, the penalty is, in fact, the 1 cycle
|
||||||
that Intel .
|
that Intel .
|
||||||
|
|
||||||
Considering that **MOV** normally takes only one cycle total, that's
|
Considering that `MOV` normally takes only one cycle total, that's
|
||||||
quite a loss. For example, the postdecrement loop shown above is 2 full
|
quite a loss. For example, the postdecrement loop shown above is 2 full
|
||||||
cycles faster than the preincrement loop, resulting in a 29 percent
|
cycles faster than the preincrement loop, resulting in a 29 percent
|
||||||
improvement in the performance of the entire loop. But wait, there's
|
improvement in the performance of the entire loop. But wait, there's
|
||||||
|
|
|
||||||
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.
|
pointer when it is implicitly used to point to memory.
|
||||||
|
|
||||||
Intel states that the stack pointer is an implied destination register
|
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
|
(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
|
Intel then implies that the aforementioned addressing pipeline penalty
|
||||||
is incurred whenever the stack pointer is used as a destination by one
|
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
|
of the first set of instructions and is then immediately used to address
|
||||||
memory by one of the second set. This raises the specter of unpleasant
|
memory by one of the second set. This raises the specter of unpleasant
|
||||||
programming contortions such as intermixing **PUSH**es and **POP**s with
|
programming contortions such as intermixing `PUSH`es and `POP`s with
|
||||||
other instructions to avoid interrupting the addressing pipeline.
|
other instructions to avoid interrupting the addressing pipeline.
|
||||||
Fortunately, matters are actually not so grim as Intel's documentation
|
Fortunately, matters are actually not so grim as Intel's documentation
|
||||||
would indicate; my tests indicate that the addressing pipeline penalty
|
would indicate; my tests indicate that the addressing pipeline penalty
|
||||||
|
|
@ -52,7 +52,7 @@ et
|
||||||
|
|
||||||
to exhibit the addressing pipeline interruption phenomenon (SP is both
|
to exhibit the addressing pipeline interruption phenomenon (SP is both
|
||||||
destination and addressing register for both instructions, according to
|
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
|
the official execution times exactly. Likewise, a sequence like
|
||||||
|
|
||||||
```nasm
|
```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*
|
On the other hand, performing arithmetic directly on SP as an *explicit*
|
||||||
destination—for example, to deallocate local variables—and then using
|
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
|
pipeline. For example
|
||||||
|
|
||||||
```nasm
|
```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
|
far indicate that the stack pointer incurs the addressing pipeline
|
||||||
penalty only if (E)SP is the *explicit* destination of one instruction
|
penalty only if (E)SP is the *explicit* destination of one instruction
|
||||||
and is then used by one of the two following instructions to address
|
and is then used by one of the two following instructions to address
|
||||||
memory. So, for instance, SP isn't the explicit operand of **POP AX—**AX
|
memory. So, for instance, SP isn't the explicit operand of `POP AX`-AX
|
||||||
is—and no cycles are lost if **POP AX** is followed by **POP** or
|
is—and no cycles are lost if `POP AX` is followed by `POP` or
|
||||||
**RET**. Happily, then, we need not worry about the sequence in which we
|
`RET`. Happily, then, we need not worry about the sequence in which we
|
||||||
use **PUSH** and **POP**. However, adding to, moving to, or subtracting
|
use `PUSH` and `POP`. However, adding to, moving to, or subtracting
|
||||||
from the stack pointer should ideally be done at least two cycles before
|
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.
|
pointer to address memory.
|
||||||
|
|
||||||
#### Problems with Byte Registers {#Heading9}
|
#### 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
|
in fact exist), but it is an interesting window on the way the 486
|
||||||
works.
|
works.
|
||||||
|
|
||||||
In case you're curious, there's no such penalty for the typical **XLAT**
|
In case you're curious, there's no such penalty for the typical `XLAT`
|
||||||
sequence like
|
sequence like
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -154,20 +154,20 @@ mov al,[si]
|
||||||
xlat
|
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
|
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
|
is so slow—4 cycles—that it gives the 486 time to perform addressing
|
||||||
calculations during the course of the instruction.
|
calculations during the course of the instruction.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> While it's nice that **XLAT** doesn't suffer from the various 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
|
> 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.
|
> slow, so there's still no compelling reason to use `XLAT` on the 486.
|
||||||
|
|
||||||
In general, penalties for interrupting the 486's pipeline apply
|
In general, penalties for interrupting the 486's pipeline apply
|
||||||
primarily to the fast core instructions of the 486, most notably
|
primarily to the fast core instructions of the 486, most notably
|
||||||
register-only instructions and **MOV**, although arithmetic and logical
|
register-only instructions and `MOV`, although arithmetic and logical
|
||||||
operations that access memory are also often affected. I don't know all
|
operations that access memory are also often affected. I don't know all
|
||||||
the performance dependencies, and I don't plan to; figuring all of them
|
the performance dependencies, and I don't plan to; figuring all of them
|
||||||
out would be a big, boring job of little value. Basically, on the 486
|
out would be a big, boring job of little value. Basically, on the 486
|
||||||
|
|
|
||||||
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
|
produces no penalty; loading 2 cycles ahead is the only case that
|
||||||
normally incurs a penalty. However, think of Rule \#4 as meaning that
|
normally incurs a penalty. However, think of Rule \#4 as meaning that
|
||||||
loading a byte register disrupts the memory addressing pipeline as it
|
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,
|
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
|
pipeline is now on its first cycle: the one that loading a byte register
|
||||||
can affect.
|
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
|
under test a second time, to make sure that the instructions are in the
|
||||||
cache on the second pass, the one for which results are displayed. Also
|
cache on the second pass, the one for which results are displayed. Also
|
||||||
note that the code is less than 8K in size, so that it can all fit in
|
note that the code is less than 8K in size, so that it can all fit in
|
||||||
the 486's 8K internal cache. If I double the **REPT** value in Listing
|
the 486's 8K internal cache. If I double the `REPT` value in Listing
|
||||||
12.2 to 2,000, making the test code larger than 8K, the execution time
|
12.2 to 2,000, making the test code larger than 8K, the execution time
|
||||||
more than doubles to 224 µs, or 3.7 cycles per repetition; the extra
|
more than doubles to 224 µs, or 3.7 cycles per repetition; the extra
|
||||||
seven-tenths of a cycle comes from fetching non-cached instruction
|
seven-tenths of a cycle comes from fetching non-cached instruction
|
||||||
|
|
|
||||||
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
|
"When I looked closely as this, I realized that the two cycles for the
|
||||||
final **ADD** is just the sum of 1 cycle to load the data from memory,
|
final `ADD` is just the sum of 1 cycle to load the data from memory,
|
||||||
and 1 cycle to add it to DX, so the code could just as well have been
|
and 1 cycle to add it to DX, so the code could just as well have been
|
||||||
written as shown in Listing 13.3. The final breakthrough came when I
|
written as shown in Listing 13.3. The final breakthrough came when I
|
||||||
realized that by initializing AX to zero outside the loop, I could
|
realized that by initializing AX to zero outside the loop, I could
|
||||||
rearrange it as shown in Listing 13.4 and do the final **ADD DX,AX**
|
rearrange it as shown in Listing 13.4 and do the final `ADD DX,AX`
|
||||||
after the loop. This way there are two single-cycle instructions between
|
after the loop. This way there are two single-cycle instructions between
|
||||||
the first and the fourth line, avoiding all pipeline stalls, for a total
|
the first and the fourth line, avoiding all pipeline stalls, for a total
|
||||||
throughput of two cycles/char."
|
throughput of two cycles/char."
|
||||||
|
|
@ -52,12 +52,12 @@ Clever 486 optimization can pay off big. QED.
|
||||||
### BSWAP: More Useful Than You Might Think {#Heading4}
|
### BSWAP: More Useful Than You Might Think {#Heading4}
|
||||||
|
|
||||||
There are only 3 non-system instructions unique to the 486. None is
|
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
|
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
|
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
|
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
|
first in memory, also called *little endian*) to Motorola format (most
|
||||||
significant byte first in memory, or *big endian*), like so:
|
significant byte first in memory, or *big endian*), like so:
|
||||||
|
|
||||||
|
|
@ -67,16 +67,16 @@ bswap
|
||||||
stosd
|
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
|
a bitmap so that they can be rotated 32 bits at a time with an
|
||||||
instruction such as **ROR EAX,1**. Intel's byte ordering for multiword
|
instruction such as `ROR EAX,1`. Intel's byte ordering for multiword
|
||||||
values (least-significant byte first) loads pixels in the wrong order,
|
values (least-significant byte first) loads pixels in the wrong order,
|
||||||
so far as word rotation is concerned, but **BSWAP** can take care of
|
so far as word rotation is concerned, but `BSWAP` can take care of
|
||||||
that.
|
that.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
having to do with making efficient use of the upper half of 32-bit
|
||||||
registers. As any assembly language programmer knows, the x86 register
|
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
|
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
|
instructions of the 486, taking 2 to 3 cycles to execute. Thus, it takes
|
||||||
2 cycles to rotate the skip value into CX in Listing 13.5, and 2 more
|
2 cycles to rotate the skip value into CX in Listing 13.5, and 2 more
|
||||||
cycles to rotate it back to the upper half of ECX. I'd say four cycles
|
cycles to rotate it back to the upper half of ECX. I'd say four cycles
|
||||||
is a pretty steep price to pay, especially considering that a **MOV** to
|
is a pretty steep price to pay, especially considering that a `MOV` to
|
||||||
or from memory takes only one cycle. Basically, using **ROR** to access
|
or from memory takes only one cycle. Basically, using `ROR` to access
|
||||||
a 16-bit value in the upper half of a 16-bit register is a pretty
|
a 16-bit value in the upper half of a 16-bit register is a pretty
|
||||||
marginal technique, unless for some reason you can't access memory at
|
marginal technique, unless for some reason you can't access memory at
|
||||||
all (for example, if you're using BP as a working register, temporarily
|
all (for example, if you're using BP as a working register, temporarily
|
||||||
|
|
|
||||||
48
13-03.md
48
13-03.md
|
|
@ -10,11 +10,11 @@ chapter: '13'
|
||||||
pages: 254-256
|
pages: 254-256
|
||||||
---
|
---
|
||||||
|
|
||||||
On the 386, **ROR** was the only way to split a 32-bit register into two
|
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
|
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.
|
job, but can do it better, because `BSWAP` executes in just one cycle.
|
||||||
**BSWAP** has the added benefit of not affecting any flags, unlike
|
`BSWAP` has the added benefit of not affecting any flags, unlike
|
||||||
**ROR**. With **BSWAP**-based code like that in Listing 13.6, the upper
|
`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
|
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
|
without altering any flags, making the technique of packing two 16-bit
|
||||||
registers into one 32-bit register much more useful.
|
registers into one 32-bit register much more useful.
|
||||||
|
|
@ -37,8 +37,8 @@ looptop:
|
||||||
|
|
||||||
### Pushing and Popping Memory {#Heading5}
|
### Pushing and Popping Memory {#Heading5}
|
||||||
|
|
||||||
Pushing or popping a memory location, as in **PUSH WORD PTR [BX]** or
|
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
|
`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
|
the stack, especially when pushing parameters for calling a C-compatible
|
||||||
function. However, on a 486, these are unattractive instructions from a
|
function. However, on a 486, these are unattractive instructions from a
|
||||||
performance perspective. Pushing a memory location takes four cycles; by
|
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
|
executed in a single cycle by the 486's RISC core, are fast; whereas
|
||||||
complex operations, which must be carried out in microcode just as they
|
complex operations, which must be carried out in microcode just as they
|
||||||
were on the 386, are almost all relatively slow. Slow, complex
|
were on the 386, are almost all relatively slow. Slow, complex
|
||||||
operations include all the string instructions except **REP MOVS,** as
|
operations include all the string instructions except `REP MOVS`, as
|
||||||
well as **XLAT, LOOP,** and, of course, **PUSH *mem*** and **POP
|
well as `XLAT`, `LOOP`, and, of course, `PUSH *mem*` and `POP
|
||||||
*mem.***
|
*mem*`.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> Whenever possible, try to use the 486's 1-cycle instructions, including
|
> 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
|
> `MOV`, `ADD`, `SUB`, `CMP`, `ADC`, `SBB`, `XOR`, `AND`, `OR`, `TEST`,
|
||||||
> reg** and **POP reg**. These instructions have an added benefit in that
|
> `LEA`, and `PUSH reg` and `POP reg`. These instructions have an added
|
||||||
> it's often possible to rearrange them for maximum pipeline efficiency,
|
> benefit in that it's often possible to rearrange them for maximum
|
||||||
> as is the case with Terje's optimization described earlier in this
|
> pipeline efficiency, as is the case with Terje's optimization described
|
||||||
> chapter.
|
> earlier in this chapter.
|
||||||
|
|
||||||
### Optimal 1-Bit Shifts and Rotates {#Heading6}
|
### Optimal 1-Bit Shifts and Rotates {#Heading6}
|
||||||
|
|
||||||
On a 486, the n-bit forms of the shift and rotate instructions—as in
|
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
|
`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
|
forms—as in `ROR AX,1` and `SHL BX,1`-are *3-cycle* instructions. Go
|
||||||
figure.
|
figure.
|
||||||
|
|
||||||
Assemblers default to the 1-bit instruction for 1-bit shifts and
|
Assemblers default to the 1-bit instruction for 1-bit shifts and
|
||||||
|
|
@ -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
|
however, it might be worth hand-assembling the n-bit form of a
|
||||||
single-bit shift or rotate in order to save that cycle. The easiest way
|
single-bit shift or rotate in order to save that cycle. The easiest way
|
||||||
to do this is to assemble a 2-bit form of the desired instruction, as in
|
to do this is to assemble a 2-bit form of the desired instruction, as in
|
||||||
**SHL AX,2,** then look at the hex codes that the assembler generates
|
`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
|
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
|
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
|
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
|
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:
|
code as follows:
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -111,12 +111,12 @@ mov dx,ax
|
||||||
```
|
```
|
||||||
|
|
||||||
At the end of this sequence, DX will contain 2, and the fast n-bit
|
At the end of this sequence, DX will contain 2, and the fast n-bit
|
||||||
version of **SHL AX,1** will have executed. If you use this approach,
|
version of `SHL AX,1` will have executed. If you use this approach,
|
||||||
I'd recommend using a macro, rather than sticking DBs in the middle of
|
I'd recommend using a macro, rather than sticking DBs in the middle of
|
||||||
your code.
|
your code.
|
||||||
|
|
||||||
Again, this technique is advantageous *only* on a 486. It also doesn't
|
Again, this technique is advantageous *only* on a 486. It also doesn't
|
||||||
apply to **RCL** and **RCR,** where you definitely want to use the 1-bit
|
apply to `RCL` and `RCR`, where you definitely want to use the 1-bit
|
||||||
versions whenever you can, because the n-bit versions are horrendously
|
versions whenever you can, because the n-bit versions are horrendously
|
||||||
slow. But if you're optimizing for the 486, these tidbits can save a few
|
slow. But if you're optimizing for the 486, these tidbits can save a few
|
||||||
critical cycles—and Lord knows that if you're optimizing for the
|
critical cycles—and Lord knows that if you're optimizing for the
|
||||||
|
|
|
||||||
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
|
uses a perfectly valid 32-bit address, with the byte accessed being the
|
||||||
one at the offset in DS pointed to by the sum of EDX times 4 plus the
|
one at the offset in DS pointed to by the sum of EDX times 4 plus the
|
||||||
offset of **BaseTable** plus ECX. This is a very powerful memory
|
offset of `BaseTable` plus ECX. This is a very powerful memory
|
||||||
addressing scheme, far superior to 8088-style 16-bit addressing, but
|
addressing scheme, far superior to 8088-style 16-bit addressing, but
|
||||||
it's not without its quirks and costs, so let's take a quick look at
|
it's not without its quirks and costs, so let's take a quick look at
|
||||||
32-bit addressing. (By the way, 32-bit addressing is not limited to
|
32-bit addressing. (By the way, 32-bit addressing is not limited to
|
||||||
|
|
@ -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
|
is a 1-cycle penalty imposed for using an index register. In this
|
||||||
context, you use an index register when you use a register that's
|
context, you use an index register when you use a register that's
|
||||||
scaled, or when you use the sum of two registers to point to memory.
|
scaled, or when you use the sum of two registers to point to memory.
|
||||||
**MOV BL,[EBX\*2]** uses an index register and takes an extra cycle, as
|
`MOV BL,[EBX*2]` uses an index register and takes an extra cycle, as
|
||||||
does **MOV CL,[EAX+EDX]; MOV CL,[EAX+100H]** is not indexed, however.
|
does `MOV CL,[EAX+EDX]; MOV CL,[EAX+100H]` is not indexed, however.
|
||||||
|
|
||||||
The other cost of 32-bit addressing is in instruction size. Old-style
|
The other cost of 32-bit addressing is in instruction size. Old-style
|
||||||
16-bit addressing usually (except in a few special cases) uses one extra
|
16-bit addressing usually (except in a few special cases) uses one extra
|
||||||
|
|
@ -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
|
continues to use the Mod-R/M byte, albeit with a different
|
||||||
interpretation; in these cases, 32-bit addressing is no larger than
|
interpretation; in these cases, 32-bit addressing is no larger than
|
||||||
16-bit addressing, except when a 32-bit displacement is involved. For
|
16-bit addressing, except when a 32-bit displacement is involved. For
|
||||||
example, **MOV AL, [EBX]** is a 2-byte instruction; **MOV AL,
|
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
|
[EBX+10H]` is a 3-byte instruction; and `MOV AL, [EBX+10000H]` is a
|
||||||
6-byte instruction.
|
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
|
in the excellent *Algorithms in C,* by Robert Sedgewick
|
||||||
(Addison-Wesley), which served as the primary reference for this
|
(Addison-Wesley), which served as the primary reference for this
|
||||||
chapter. (If you look at Sedgewick, be aware that in the Boyer-Moore
|
chapter. (If you look at Sedgewick, be aware that in the Boyer-Moore
|
||||||
listing on page 288, there is a mistake: "j \> 0" in the **for** loop
|
listing on page 288, there is a mistake: "j \> 0" in the `for` loop
|
||||||
should be "j \>= 0," unless I'm missing something.)
|
should be "j \>= 0," unless I'm missing something.)
|
||||||
|
|
||||||
String searching is the simple matter of finding the first occurrence of
|
String searching is the simple matter of finding the first occurrence of
|
||||||
|
|
@ -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
|
possible match location, starting at the beginning of the buffer and
|
||||||
advancing one position after each mismatch, until either a match is
|
advancing one position after each mismatch, until either a match is
|
||||||
found or the buffer is exhausted. There's even a nifty string
|
found or the buffer is exhausted. There's even a nifty string
|
||||||
instruction, **REPZ CMPS,** that's perfect for comparing the pattern to
|
instruction, `REPZ CMPS`, that's perfect for comparing the pattern to
|
||||||
the contents of the buffer at each location. What could be simpler?
|
the contents of the buffer at each location. What could be simpler?
|
||||||
|
|
||||||
We have some important information that we're not yet using, though.
|
We have some important information that we're not yet using, though.
|
||||||
|
|
@ -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
|
buffer, or anything close. A reasonable conclusion is that the first
|
||||||
character of the pattern will rarely match the first character of the
|
character of the pattern will rarely match the first character of the
|
||||||
buffer location currently being checked. This allows us to use the
|
buffer location currently being checked. This allows us to use the
|
||||||
speedy **REPNZ SCASB** to whiz through the buffer, eliminating most
|
speedy `REPNZ SCASB` to whiz through the buffer, eliminating most
|
||||||
potential match locations with single repetitions of **SCASB.** Only
|
potential match locations with single repetitions of `SCASB`. Only
|
||||||
when that first character does (infrequently) match must we drop back to
|
when that first character does (infrequently) match must we drop back to
|
||||||
the slower **REPZ CMPS** approach.
|
the slower `REPZ CMPS` approach.
|
||||||
|
|
||||||
It's important to understand that we're assuming that the buffer is
|
It's important to understand that we're assuming that the buffer is
|
||||||
typical text. That's what I meant at the outset, when I said that the
|
typical text. That's what I meant at the outset, when I said that the
|
||||||
|
|
@ -106,26 +106,26 @@ information you need may be under your nose.
|
||||||
> a great deal of useful, if somewhat imprecise, information.
|
> a great deal of useful, if somewhat imprecise, information.
|
||||||
|
|
||||||
If the buffer contains the letter ‘A' repeated 1,000 times, followed by
|
If the buffer contains the letter ‘A' repeated 1,000 times, followed by
|
||||||
the letter ‘B,' then the **REPNZ SCASB/REPZ CMPS** approach will be much
|
the letter ‘B,' then the `REPNZ SCASB/REPZ CMPS` approach will be much
|
||||||
slower than the brute-force **REPZ CMPS** approach when searching for
|
slower than the brute-force `REPZ CMPS` approach when searching for
|
||||||
the pattern "AB," because **REPNZ SCASB** would match at every buffer
|
the pattern "AB," because `REPNZ SCASB` would match at every buffer
|
||||||
location. You could construct a horrendous worst-case scenario for
|
location. You could construct a horrendous worst-case scenario for
|
||||||
almost any good optimization; the key is understanding the usual
|
almost any good optimization; the key is understanding the usual
|
||||||
conditions under which your code will work.
|
conditions under which your code will work.
|
||||||
|
|
||||||
As discussed in Chapter 9, we also know that certain characters have
|
As discussed in Chapter 9, we also know that certain characters have
|
||||||
lower probabilities of matching than others. In a normal buffer, ‘T'
|
lower probabilities of matching than others. In a normal buffer, ‘T'
|
||||||
will match far more often than ‘X.' Therefore, if we use **REPNZ SCASB**
|
will match far more often than ‘X.' Therefore, if we use `REPNZ SCASB`
|
||||||
to scan for the least common letter in the search string, rather than
|
to scan for the least common letter in the search string, rather than
|
||||||
the first letter, we'll greatly decrease the number of times we have to
|
the first letter, we'll greatly decrease the number of times we have to
|
||||||
drop back to **REPZ CMPS,** and the search time will become very close
|
drop back to `REPZ CMPS`, and the search time will become very close
|
||||||
to the time it takes **REPNZ SCASB** to go from the start of the buffer
|
to the time it takes `REPNZ SCASB` to go from the start of the buffer
|
||||||
to the match location. If the distance to the first match is N bytes,
|
to the match location. If the distance to the first match is N bytes,
|
||||||
the least-common **REPNZ SCASB** approach will take about as long as N
|
the least-common `REPNZ SCASB` approach will take about as long as N
|
||||||
repetitions of **REPNZ SCASB.**
|
repetitions of `REPNZ SCASB`.
|
||||||
|
|
||||||
At this point, we're pretty much searching at the speed of **REPNZ
|
At this point, we're pretty much searching at the speed of `REPNZ
|
||||||
SCASB.** On the x86, there simply is no faster way to test each
|
SCASB`. On the x86, there simply is no faster way to test each
|
||||||
character in turn. In order to get any faster, we'd have to check fewer
|
character in turn. In order to get any faster, we'd have to check fewer
|
||||||
characters—but we can't do that and still be sure of finding all
|
characters—but we can't do that and still be sure of finding all
|
||||||
matches. Can we?
|
matches. Can we?
|
||||||
|
|
|
||||||
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
|
Look at it differently, though: What if we compare the pattern starting
|
||||||
with the last (rightmost) byte, rather than the first (leftmost) byte?
|
with the last (rightmost) byte, rather than the first (leftmost) byte?
|
||||||
In other words, what if we compare from high memory toward low, in the
|
In other words, what if we compare from high memory toward low, in the
|
||||||
direction in which string instructions go after the **STD** instruction?
|
direction in which string instructions go after the `STD` instruction?
|
||||||
After all, we're comparing one set of bytes (the pattern) to another set
|
After all, we're comparing one set of bytes (the pattern) to another set
|
||||||
of bytes (a portion of the buffer); it doesn't matter in the least in
|
of bytes (a portion of the buffer); it doesn't matter in the least in
|
||||||
what order we compare them, so long as all the bytes in one set are
|
what order we compare them, so long as all the bytes in one set are
|
||||||
|
|
@ -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
|
*might* have matched, but have just been shown not to, because they
|
||||||
overlap the mismatched character that doesn't belong in the pattern. In
|
overlap the mismatched character that doesn't belong in the pattern. In
|
||||||
this case, we can skip ahead by the full pattern length in the buffer!
|
this case, we can skip ahead by the full pattern length in the buffer!
|
||||||
This is how we can outperform even **REPNZ SCASB; REPNZ SCASB** has to
|
This is how we can outperform even `REPNZ SCASB; REPNZ SCASB` has to
|
||||||
check every byte in the buffer, but Boyer-Moore doesn't.
|
check every byte in the buffer, but Boyer-Moore doesn't.
|
||||||
|
|
||||||
Figure 14.1 illustrates the operation of a Boyer-Moore search when the
|
Figure 14.1 illustrates the operation of a Boyer-Moore search when the
|
||||||
|
|
|
||||||
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
|
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
|
measured with Turbo Profiler on a 20 MHz cached 386, searching a
|
||||||
modified version of the text of this chapter) shows that this
|
modified version of the text of this chapter) shows that this
|
||||||
implementation is generally much slower than **REPNZ SCASB,** although
|
implementation is generally much slower than `REPNZ SCASB`, although
|
||||||
it does come close when searching for long patterns. Listing 14.1 is
|
it does come close when searching for long patterns. Listing 14.1 is
|
||||||
designed primarily to make later assembly implemenmore comprehensible,
|
designed primarily to make later assembly implemenmore comprehensible,
|
||||||
rather than faster; Sedge's implementation uses arrays rather than
|
rather than faster; Sedge's implementation uses arrays rather than
|
||||||
pointers, is a great deal more compact and very clever, and may be
|
pointers, is a great deal more compact and very clever, and may be
|
||||||
somewhat faster. Regardless, the far superior performance of **REPNZ
|
somewhat faster. Regardless, the far superior performance of `REPNZ
|
||||||
SCASB** clearly indicates that assembly language is in order at this
|
SCASB` clearly indicates that assembly language is in order at this
|
||||||
point.
|
point.
|
||||||
|
|
||||||
| | "g;" | "Yogi" | "igoY" | "Adrian" | "Conclusion" | "You don't know what you know" |
|
| | "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
|
The entry "Standard Boyer-Moore in ASM" in Table 14.1 refers to
|
||||||
straight-forward hand optimization of Listing 14.1, code that is not
|
straight-forward hand optimization of Listing 14.1, code that is not
|
||||||
included in this chapter for the perfectly good reason that it is slower
|
included in this chapter for the perfectly good reason that it is slower
|
||||||
in most cases than **REPNZ SCASB.** I say this casually now, but not so
|
in most cases than `REPNZ SCASB`. I say this casually now, but not so
|
||||||
yesterday, when I had all but concluded that Boyer-Moore was simply
|
yesterday, when I had all but concluded that Boyer-Moore was simply
|
||||||
inferior on the x86, due to two architectural quirks: the string
|
inferior on the x86, due to two architectural quirks: the string
|
||||||
instructions and slow branch. I had even coined a neat phrase for it:
|
instructions and slow branch. I had even coined a neat phrase for it:
|
||||||
|
|
|
||||||
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
|
Well, architecture carries a lot of weight, but it sure as heck isn't
|
||||||
destiny. I had simply fallen into the trap of figuring that the
|
destiny. I had simply fallen into the trap of figuring that the
|
||||||
algorithm was so clever that I didn't have to do any thinking myself.
|
algorithm was so clever that I didn't have to do any thinking myself.
|
||||||
The path leading to **REPNZ SCASB** from the original brute-force
|
The path leading to `REPNZ SCASB` from the original brute-force
|
||||||
approach of **REPZ CMPSB** at every location had been based on my
|
approach of `REPZ CMPSB` at every location had been based on my
|
||||||
observation that the first character comparison at each buffer location
|
observation that the first character comparison at each buffer location
|
||||||
usually fails. Why not apply the same concept to Boyer-Moore? Listing
|
usually fails. Why not apply the same concept to Boyer-Moore? Listing
|
||||||
14.3 is just like the standard implementation—except that it's optimized
|
14.3 is just like the standard implementation—except that it's optimized
|
||||||
to handle a first-comparison mismatch as quickly as possible in the loop
|
to handle a first-comparison mismatch as quickly as possible in the loop
|
||||||
at **QuickSearchLoop**, much as **REPNZ SCASB** optimizes
|
at `QuickSearchLoop`, much as `REPNZ SCASB` optimizes
|
||||||
first-comparison mismatches for the brute-force approach. The results in
|
first-comparison mismatches for the brute-force approach. The results in
|
||||||
Table 14.1 speak for themselves; Listing 14.3 is more than twice as fast
|
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
|
as what I assure you was already a nice, tight assembly implementation
|
||||||
(and unrolling **QuickSearchLoop** could boost performance by up to 10
|
(and unrolling `QuickSearchLoop` could boost performance by up to 10
|
||||||
percent more). Listing 14.3 is also *four times* faster than **REPNZ
|
percent more). Listing 14.3 is also *four times* faster than `REPNZ
|
||||||
SCASB** in one case.
|
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
|
searching techniques. Nonetheless, the overall trend is clear: For all
|
||||||
but the shortest patterns, well-implemented Boyer-Moore is generally as
|
but the shortest patterns, well-implemented Boyer-Moore is generally as
|
||||||
good as or better than—sometimes *much* better than—brute-force
|
good as or better than—sometimes *much* better than—brute-force
|
||||||
searching. (For short patterns, you might want to use **REPNZ SCASB,**
|
searching. (For short patterns, you might want to use `REPNZ SCASB`,
|
||||||
thereby getting the best of both worlds.)
|
thereby getting the best of both worlds.)
|
||||||
|
|
||||||
Know your data and use your smarts. Don't stop thinking just because
|
Know your data and use your smarts. Don't stop thinking just because
|
||||||
|
|
@ -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
|
We can do substantially better yet than Listing 14.3 if we're willing to
|
||||||
accept tighter limits on the data. Limiting the length of the
|
accept tighter limits on the data. Limiting the length of the
|
||||||
searched-for pattern to a maximum of 255 bytes allows us to use the
|
searched-for pattern to a maximum of 255 bytes allows us to use the
|
||||||
**XLAT** instruction and generally tighten the critical loop. (Be aware,
|
`XLAT` instruction and generally tighten the critical loop. (Be aware,
|
||||||
however, that **XLAT** is a relatively expensive instruction on the 486
|
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
|
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
|
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
|
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
|
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
|
all but the first search when repeatedly searching for a particular
|
||||||
pattern, by building the skip table externally and passing a pointer to
|
pattern, by building the skip table externally and passing a pointer to
|
||||||
it as a parameter.
|
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
|
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,
|
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
|
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
|
Conceptually, nothing could be simpler. From an implementation
|
||||||
perspective, however, there are serious flaws with this model.
|
perspective, however, there are serious flaws with this model.
|
||||||
|
|
||||||
The fundamental problem is that the model of Figure 15.1 unnecessarily
|
The fundamental problem is that the model of Figure 15.1 unnecessarily
|
||||||
complicates link manipulation. In order to delete a node, for example,
|
complicates link manipulation. In order to delete a node, for example,
|
||||||
you must change the preceding node's **NextNode** pointer to point to
|
you must change the preceding node's `NextNode` pointer to point to
|
||||||
the following node, as shown in Listing 15.1. (Listing 15.2 is the
|
the following node, as shown in Listing 15.1. (Listing 15.2 is the
|
||||||
header file LLIST.H, which is **\#include**d by all the linked list
|
header file LLIST.H, which is `#include`d by all the linked list
|
||||||
listings in this chapter.) Easy enough—unless the preceding node happens
|
listings in this chapter.) Easy enough—unless the preceding node happens
|
||||||
to be the head pointer, which doesn't *have* a **NextNode** field,
|
to be the head pointer, which doesn't *have* a `NextNode` field,
|
||||||
because it's not a node, so Listing 15.1 won't work. Cumbersome special
|
because it's not a node, so Listing 15.1 won't work. Cumbersome special
|
||||||
code and extra information (a pointer to the head of the list) are
|
code and extra information (a pointer to the head of the list) are
|
||||||
required to handle the head-pointer case, as shown in Listing 15.3.
|
required to handle the head-pointer case, as shown in Listing 15.3.
|
||||||
(I'll grant you that if you make the next-node pointer the first field
|
(I'll grant you that if you make the next-node pointer the first field
|
||||||
in the **LinkNode** structure, at offset 0, then you could successfully
|
in the `LinkNode` structure, at offset 0, then you could successfully
|
||||||
point to the head pointer and pretend it was a **LinkNode**
|
point to the head pointer and pretend it was a `LinkNode`
|
||||||
structure—but that's an ugly and potentially dangerous trick, and we'll
|
structure—but that's an ugly and potentially dangerous trick, and we'll
|
||||||
see a better approach next.)
|
see a better approach next.)
|
||||||
|
|
||||||
|
|
|
||||||
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
|
However, it is true that if you're going to store a variety of types of
|
||||||
structures in your linked lists, you should start each node with the
|
structures in your linked lists, you should start each node with the
|
||||||
**LinkNode** field. That way, the link pointer is in the same place in
|
`LinkNode` field. That way, the link pointer is in the same place in
|
||||||
*every* structure, and the same linked list code can handle all of the
|
*every* structure, and the same linked list code can handle all of the
|
||||||
structure types by casting them to the base link-node structure type.
|
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
|
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
|
Note that Listings 15.1 and 15.3 have to specify the linked-list delete
|
||||||
operation as "delete the *next* node," rather than "delete this node,"
|
operation as "delete the *next* node," rather than "delete this node,"
|
||||||
because in order to relink it's necessary to access the **NextNode**
|
because in order to relink it's necessary to access the `NextNode`
|
||||||
field of the node preceding the node to be deleted, and it's impossible
|
field of the node preceding the node to be deleted, and it's impossible
|
||||||
to backtrack in a singly linked list. For this reason, singly-linked
|
to backtrack in a singly linked list. For this reason, singly-linked
|
||||||
list operations tend to work with the structure preceding the one of
|
list operations tend to work with the structure preceding the one of
|
||||||
|
|
@ -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
|
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
|
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
|
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
|
pointer. This way, a deletion operation on an empty list will have no
|
||||||
effect—quite unlike the same operation performed on a list terminated
|
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
|
can be detected because it will be the only node for which the next-node
|
||||||
pointer equals the current-node pointer.
|
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
|
Listings 15.1 and 15.6 together form a suite of C functions for
|
||||||
maintaining a circular linked list sorted by ascending value. (Listing
|
maintaining a circular linked list sorted by ascending value. (Listing
|
||||||
15.5 requires modification before it will work with circular lists.)
|
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
|
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
|
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
|
illustrates the use of the linked-list functions in Listings 15.1 and
|
||||||
15.6.
|
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
|
In one of my *PC TECHNIQUES* "Pushing the Envelope" columns, I passed
|
||||||
along one of David Stafford's fiendish programming puzzles: Write a
|
along one of David Stafford's fiendish programming puzzles: Write a
|
||||||
C-callable function to find the greatest or smallest unsigned **int**.
|
C-callable function to find the greatest or smallest unsigned `int`.
|
||||||
Not a big deal—except that David had *already* done it in 24 bytes, so
|
Not a big deal—except that David had *already* done it in 24 bytes, so
|
||||||
the challenge was to do it in 24 bytes or less.
|
the challenge was to do it in 24 bytes or less.
|
||||||
|
|
||||||
|
|
@ -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
|
hate to say this because some of my correspondents were *very* pleased
|
||||||
with the thought that they had bested David), no one has yet met the
|
with the thought that they had bested David), no one has yet met the
|
||||||
challenge—because most of you folks missed a key point. When David said,
|
challenge—because most of you folks missed a key point. When David said,
|
||||||
"Write a function to find the greatest or smallest unsigned **int** in
|
"Write a function to find the greatest or smallest unsigned `int` in
|
||||||
24 bytes or less," he meant, "Write the **hi** and the **lo** functions
|
24 bytes or less," he meant, "Write the `hi` and the `lo` functions
|
||||||
in 24 bytes or less—*combined*."
|
in 24 bytes or less—*combined*."
|
||||||
|
|
||||||
Oh.
|
Oh.
|
||||||
|
|
|
||||||
14
16-03.md
14
16-03.md
|
|
@ -106,16 +106,16 @@ _ScanBuffer endp
|
||||||
```
|
```
|
||||||
|
|
||||||
Listing 16.4 features several interesting tricks. First, it uses
|
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
|
pointed-to byte, advance the pointer, and look up the value indexed by
|
||||||
the byte in a table, all with just two instruction bytes.
|
the byte in a table, all with just two instruction bytes.
|
||||||
(Interestingly, Listing 16.4 would probably run quite a bit better still
|
(Interestingly, Listing 16.4 would probably run quite a bit better still
|
||||||
on an 8088, where **LODSB** and **XLAT** have a greater advantage over
|
on an 8088, where `LODSB` and `XLAT` have a greater advantage over
|
||||||
conventional instructions. On the 486 and Pentium, however, **LODSB**
|
conventional instructions. On the 486 and Pentium, however, `LODSB`
|
||||||
and **XLAT** lose much of their appeal, and should be replaced with
|
and `XLAT` lose much of their appeal, and should be replaced with
|
||||||
**MOV** instructions.) Better yet, **LODSB** and **XLAT** don't alter
|
`MOV` instructions.) Better yet, `LODSB` and `XLAT` don't alter
|
||||||
the flags, so the Zero flag status set before **LODSB** is still around
|
the flags, so the Zero flag status set before `LODSB` is still around
|
||||||
to be tested after **XLAT** .
|
to be tested after `XLAT` .
|
||||||
|
|
||||||
Finally, if you look closely, you will see that Listing 16.4 jumps out
|
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
|
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
|
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
|
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
|
out), so it was superfluous as a memory-addressing component; this made
|
||||||
it possible to use base-only addressing (**[EAX]**) rather than
|
it possible to use base-only addressing (`[EAX]`) rather than
|
||||||
base+index addressing (**[EBX+EAX]**), which saves a cycle on the 386.
|
base+index addressing (`[EBX+EAX]`), which saves a cycle on the 386.
|
||||||
Second: Changing the instruction to **CMP [EAX],DH** saved 2 cycles—just
|
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.
|
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**
|
> `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
|
> takes only 5 cycles; you should always perform`CMP` with the memory
|
||||||
> operand on the left on the 386.
|
> operand on the left on the 386.
|
||||||
|
|
||||||
(Granted, **CMP [*mem*],*reg*** is 1 cycle slower than **CMP
|
(Granted, `CMP [*mem*],*reg*` is 1 cycle slower than `CMP
|
||||||
*reg*,[*mem*]** on the 286, and they're both the same on the 8088; in
|
*reg*,[*mem*]` on the 286, and they're both the same on the 8088; in
|
||||||
this case, though, the code was specific to the 386. In case you're
|
this case, though, the code was specific to the 386. In case you're
|
||||||
curious, both forms take 2 cycles on the 486; quite a lot faster, eh?)
|
curious, both forms take 2 cycles on the 486; quite a lot faster, eh?)
|
||||||
|
|
||||||
|
|
@ -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
|
of 386 native-mode programming. The second lesson is that after you've
|
||||||
slaved over your code for a while, you're in no shape to see its flaws,
|
slaved over your code for a while, you're in no shape to see its flaws,
|
||||||
or to be able to get the new perspectives needed to speed it up. I'll
|
or to be able to get the new perspectives needed to speed it up. I'll
|
||||||
bet Terje looked at that **[EBX+EAX]** addressing a hundred times while
|
bet Terje looked at that `[EBX+EAX]` addressing a hundred times while
|
||||||
trying to speed up his code, but he didn't really see what it did;
|
trying to speed up his code, but he didn't really see what it did;
|
||||||
instead, he saw what it was supposed to do. Mental shortcuts like this
|
instead, he saw what it was supposed to do. Mental shortcuts like this
|
||||||
are what enable us to deal with the complexities of assembly language
|
are what enable us to deal with the complexities of assembly language
|
||||||
|
|
|
||||||
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
|
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
|
overall counting time, including time spent in the main program, disk
|
||||||
I/O, and everything else; the second value is the time actually spent
|
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
|
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
|
quality of the optimization in each entry, since the rest of the overall
|
||||||
execution time is fixed.
|
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),
|
(which in turn is basically the same as that of the original C code),
|
||||||
but that framework is implemented more efficiently.
|
but that framework is implemented more efficiently.
|
||||||
|
|
||||||
One obvious level 1 optimization is using a **word** rather than
|
One obvious level 1 optimization is using a `word` rather than
|
||||||
**dword** counter. **ScanBuffer** can never be called upon to handle
|
`dword` counter. `ScanBuffer` can never be called upon to handle
|
||||||
more than 64K bytes at a time, so no more than 32K words can ever be
|
more than 64K bytes at a time, so no more than 32K words can ever be
|
||||||
found. Given that, it's a logical step to use **INC** rather than
|
found. Given that, it's a logical step to use `INC` rather than
|
||||||
**ADD/ADC** to keep count, adding the tally into the full 32-bit count
|
`ADD/ADC` to keep count, adding the tally into the full 32-bit count
|
||||||
only upon exiting the function. Another useful optimization is aligning
|
only upon exiting the function. Another useful optimization is aligning
|
||||||
loop tops and other branch destinations to **word** , or better yet
|
loop tops and other branch destinations to `word` , or better yet
|
||||||
**dword** , boundaries.
|
`dword` , boundaries.
|
||||||
|
|
||||||
Eliminating branches was very popular, as it should be on x86
|
Eliminating branches was very popular, as it should be on x86
|
||||||
processors. Branches were eliminated in a remarkable variety of ways.
|
processors. Branches were eliminated in a remarkable variety of ways.
|
||||||
Many of you unrolled the loop, a technique that does pay off nicely. A
|
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
|
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
|
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
|
the point of unrolling a loop is to reduce the number of times you have
|
||||||
to check for the end of the buffer! The trick to this is to set CX to
|
to check for the end of the buffer! The trick to this is to set CX to
|
||||||
|
|
@ -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
|
whole thing come out right. Listing 16.5 (QSCAN3.ASM) illustrates this
|
||||||
technique.
|
technique.
|
||||||
|
|
||||||
Another effective optimization is the use of **LODSW** rather than
|
Another effective optimization is the use of `LODSW` rather than
|
||||||
**LODSB** , thereby processing two bytes per memory access. This has the
|
`LODSB` , thereby processing two bytes per memory access. This has the
|
||||||
effect of unrolling the loop one time, since with **LODSW** , looping is
|
effect of unrolling the loop one time, since with `LODSW` , looping is
|
||||||
performed at most only once every two bytes.
|
performed at most only once every two bytes.
|
||||||
|
|
||||||
Cutting down the branches used to loop is only part of the branching
|
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,
|
Listing 16.6, contributed by Willem Clements, of Granada, Spain,
|
||||||
illustrates a variety of level 1 optimizations: the two-loop approach,
|
illustrates a variety of level 1 optimizations: the two-loop approach,
|
||||||
the use of a 16- rather than 32-bit counter, and the use of **LODSW** .
|
the use of a 16- rather than 32-bit counter, and the use of `LODSW` .
|
||||||
Together, these optimizations made Willem's code nearly twice as fast as
|
Together, these optimizations made Willem's code nearly twice as fast as
|
||||||
mine in Listing 16.4. A few details could stand improvement; for
|
mine in Listing 16.4. A few details could stand improvement; for
|
||||||
example, **AND AX,AX** is a shorter way to test for zero than **CMP
|
example, `AND AX,AX` is a shorter way to test for zero than `CMP
|
||||||
AX,0** , and **ALIGN 2** could be used. Nonetheless, this is good code,
|
AX,0` , and `ALIGN 2` could be used. Nonetheless, this is good code,
|
||||||
and it's also fairly compact and reasonably easy to understand. In
|
and it's also fairly compact and reasonably easy to understand. In
|
||||||
short, this is an excellent example of how an hour or so of
|
short, this is an excellent example of how an hour or so of
|
||||||
hand-optimization might accomplish significantly improved performance at
|
hand-optimization might accomplish significantly improved performance at
|
||||||
|
|
|
||||||
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
|
needed to do, rather than just improving how it already worked, and
|
||||||
implemented code that sprang from that new perspective.
|
implemented code that sprang from that new perspective.
|
||||||
|
|
||||||
You can see one example of this in Listing 16.6, where Willem uses **CMP
|
You can see one example of this in Listing 16.6, where Willem uses `CMP
|
||||||
AX,0101H** to check two bytes at once. While you might think of this as
|
AX,0101H` to check two bytes at once. While you might think of this as
|
||||||
nothing more than a doubling up of tests, it's a little more than that,
|
nothing more than a doubling up of tests, it's a little more than that,
|
||||||
especially when taken together with the use of two loops. This is a
|
especially when taken together with the use of two loops. This is a
|
||||||
break with the serial nature of the C code, a recognition that word
|
break with the serial nature of the C code, a recognition that word
|
||||||
|
|
@ -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
|
characters. Thought of this way, the code would have counted the word
|
||||||
when it first detected the group. Had you done this, your main program
|
when it first detected the group. Had you done this, your main program
|
||||||
would not have needed to look for the possible last transition or deal
|
would not have needed to look for the possible last transition or deal
|
||||||
with the semantics of the value in **CharValue**."*
|
with the semantics of the value in `CharValue`."*
|
||||||
|
|
||||||
John Richardson, of New York, contributed a good example of the benefits
|
John Richardson, of New York, contributed a good example of the benefits
|
||||||
of a different perspective (in this case, a hardware perspective). John
|
of a different perspective (in this case, a hardware perspective). John
|
||||||
|
|
|
||||||
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.
|
John later divides the transition count by two to get the word count.
|
||||||
(Food for thought: It's also possible to use **CMP** and **ADC** to
|
(Food for thought: It's also possible to use `CMP` and `ADC` to
|
||||||
detect words without branching.)
|
detect words without branching.)
|
||||||
|
|
||||||
John's approach makes it clear that word-counting is nothing more than a
|
John's approach makes it clear that word-counting is nothing more than a
|
||||||
|
|
@ -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
|
word/not status for each pair of bytes to the main word count simply by
|
||||||
getting the two bytes, working in the carry status from the last byte,
|
getting the two bytes, working in the carry status from the last byte,
|
||||||
and using the resulting value to index into the 64K table, adding in the
|
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
|
to perform all word-counting tasks for a pair of bytes. Three
|
||||||
instructions, no branches—pretty nearly perfect code.
|
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
|
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
|
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,
|
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
|
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
|
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
|
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
|
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
|
profiler, or risk making a fool of yourself. Consider Listings 17.1 and
|
||||||
17.2. Where do you think the potential for significant speed-up lies?
|
17.2. Where do you think the potential for significant speed-up lies?
|
||||||
I'll tell you one place where I thought there was considerable
|
I'll tell you one place where I thought there was considerable
|
||||||
potential—in **draw\_pixel()**. As a programmer of high-speed graphics,
|
potential—in `draw_pixel()`. As a programmer of high-speed graphics,
|
||||||
I figured any drawing function that was not only written in C/C++ but
|
I figured any drawing function that was not only written in C/C++ but
|
||||||
also recalculated the target address from scratch for each pixel would
|
also recalculated the target address from scratch for each pixel would
|
||||||
be among the first optimization targets. I also expected to get major
|
be among the first optimization targets. I also expected to get major
|
||||||
gains out of going to a Ping-Pong arrangement so that I didn't have to
|
gains out of going to a Ping-Pong arrangement so that I didn't have to
|
||||||
copy the new cellmap back to **current\_map** after calculating the next
|
copy the new cellmap back to `current_map` after calculating the next
|
||||||
generation.
|
generation.
|
||||||
|
|
||||||
| | Listing 17.1 | Listing 17.3 | Listing 17.4 |
|
| | Listing 17.1 | Listing 17.3 | Listing 17.4 |
|
||||||
|--------------------------|--------------|--------------|--------------|
|
|--------------------------|--------------|--------------|--------------|
|
||||||
| **Total execution time** | 340 secs | 94 secs | 45 secs |
|
| **Total execution time** | 340 secs | 94 secs | 45 secs |
|
||||||
| **cell\_state()** | 275 | 21 | — |
|
| `cell_state()` | 275 | 21 | — |
|
||||||
| **next\_generation()** | 60 | 14 | 40 |
|
| `next_generation()` | 60 | 14 | 40 |
|
||||||
| **count\_neighbors()** | — | 54 | — |
|
| `count_neighbors()` | — | 54 | — |
|
||||||
| **draw\_pixel()** | 2 | 2 | 2 |
|
| `draw_pixel()` | 2 | 2 | 2 |
|
||||||
| **set\_cell()** | <1 | <1 | <1 |
|
| `set_cell()` | <1 | <1 | <1 |
|
||||||
| **clear\_cell()** | <1 | <1 | <1 |
|
| `clear_cell()` | <1 | <1 | <1 |
|
||||||
| **copy\_cells()** | <1 | <1 | <1 |
|
| `copy_cells()` | <1 | <1 | <1 |
|
||||||
|
|
||||||
Table: Table 17.1 Execution times for the game of life.
|
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
|
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
|
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,
|
where the time actually goes in Listings 17.1 and 17.2. As you can see,
|
||||||
the time taken by **draw\_pixel(),** **copy\_cells(),** and *everything*
|
the time taken by `draw_pixel()`, `copy_cells()`, and *everything*
|
||||||
other than calculating the next generation is nothing more than noise.
|
other than calculating the next generation is nothing more than noise.
|
||||||
We could optimize these routines right down to executing
|
We could optimize these routines right down to executing
|
||||||
*instantaneously,* and you know what? It wouldn't make the slightest
|
*instantaneously,* and you know what? It wouldn't make the slightest
|
||||||
perceptible difference in how fast the program runs. Given the present
|
perceptible difference in how fast the program runs. Given the present
|
||||||
state of our Game of Life implementation, the only areas worth looking
|
state of our Game of Life implementation, the only areas worth looking
|
||||||
at for possible optimizations are **cell\_state()** and
|
at for possible optimizations are `cell_state()` and
|
||||||
**next\_generation().**
|
`next_generation().`
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> 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
|
> 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
|
> redraw pixels only when their states change, rather than during every
|
||||||
> generation. Detecting and eliminating redundant operations is part of
|
> 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}
|
### The Hazards and Advantages of Abstraction {#Heading6}
|
||||||
|
|
||||||
How can we speed up **cell\_state()** and **next\_generation()**? I'll
|
How can we speed up `cell_state()` and `next_generation()`? I'll
|
||||||
tell you how *not* to do it: By writing those member functions in
|
tell you how *not* to do it: By writing those member functions in
|
||||||
assembly. It's tempting to say that **cell\_state()** is taking all the
|
assembly. It's tempting to say that `cell_state()` is taking all the
|
||||||
time, so we need to speed it up with assembly, but what we really need
|
time, so we need to speed it up with assembly, but what we really need
|
||||||
to do is figure out *why* **cell\_state()** is taking all the time, then
|
to do is figure out *why* `cell_state()` is taking all the time, then
|
||||||
address that aspect of the program directly.
|
address that aspect of the program directly.
|
||||||
|
|
||||||
Once you know where you need to optimize, the one word to keep in mind
|
Once you know where you need to optimize, the one word to keep in mind
|
||||||
|
|
@ -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
|
almost certainly end up with a slow program. In Listing 17.1, the
|
||||||
abstraction that's killing us is that of looking at the eight neighbors
|
abstraction that's killing us is that of looking at the eight neighbors
|
||||||
with eight completely independent operations, requiring eight calls to
|
with eight completely independent operations, requiring eight calls to
|
||||||
**cell\_state()** and eight calculations of cell address and cell mask.
|
`cell_state()` and eight calculations of cell address and cell mask.
|
||||||
In fact, given the nature of cell storage, the eight neighbors are in a
|
In fact, given the nature of cell storage, the eight neighbors are in a
|
||||||
fixed relationship to one another, and the addresses and masks of all
|
fixed relationship to one another, and the addresses and masks of all
|
||||||
eight can generally be found very easily via hard-wired offsets and
|
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
|
But doesn't that extra copying of the edges take time? Sure, but only a
|
||||||
little; we can build it into the cellmap copying function, and then
|
little; we can build it into the cellmap copying function, and then
|
||||||
frankly we won't even notice it. Avoiding tens or hundreds of thousands
|
frankly we won't even notice it. Avoiding tens or hundreds of thousands
|
||||||
of calls to **cell\_state(),** on the other hand, will be *very*
|
of calls to `cell_state()`, on the other hand, will be *very*
|
||||||
noticeable. Listing 17.3 shows the alterations to Listing 17.1 required
|
noticeable. Listing 17.3 shows the alterations to Listing 17.1 required
|
||||||
to implement a hard-wired neighbor-counting function. This is a minor
|
to implement a hard-wired neighbor-counting function. This is a minor
|
||||||
change, in truth, implemented in about half an hour and not making the
|
change, in truth, implemented in about half an hour and not making the
|
||||||
|
|
|
||||||
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
|
implementing only the level of generality you need, but at the same time
|
||||||
decreased generality makes it more difficult to change or port the
|
decreased generality makes it more difficult to change or port the
|
||||||
program at some later date. A Game of Life implementation, such as
|
program at some later date. A Game of Life implementation, such as
|
||||||
Listing 17.1, that's built on **set\_cell()**, **clear\_cell()**, and
|
Listing 17.1, that's built on `set_cell()`, `clear_cell()`, and
|
||||||
**get\_cell()** is completely general; you can change the cell storage
|
`get_cell()` is completely general; you can change the cell storage
|
||||||
format simply by changing the constructor and those three functions.
|
format simply by changing the constructor and those three functions.
|
||||||
Listing 17.3 is harder to change because **count\_neighbors()** would
|
Listing 17.3 is harder to change because `count_neighbors()` would
|
||||||
also have to be altered, and it's more complex than any of the other
|
also have to be altered, and it's more complex than any of the other
|
||||||
functions.
|
functions.
|
||||||
|
|
||||||
So, in Listing 17.3, we've gotten under the hood and changed the cellmap
|
So, in Listing 17.3, we've gotten under the hood and changed the cellmap
|
||||||
format a little, and gotten impressive results. But now
|
format a little, and gotten impressive results. But now
|
||||||
**count\_neighbors()** is hard-wired for optimized counting, and it's
|
`count_neighbors()` is hard-wired for optimized counting, and it's
|
||||||
still taking up more than half the time. Maybe now it's time to go to
|
still taking up more than half the time. Maybe now it's time to go to
|
||||||
assembly?
|
assembly?
|
||||||
|
|
||||||
|
|
@ -135,12 +135,12 @@ neighbor_count++;
|
||||||
```
|
```
|
||||||
|
|
||||||
Listing 17.4 and Listing 17.3 are functionally the same; the only
|
Listing 17.4 and Listing 17.3 are functionally the same; the only
|
||||||
difference lies in how **next\_generation()** is implemented. (Only
|
difference lies in how `next_generation()` is implemented. (Only
|
||||||
**next\_generation()** is shown in Listing 17.4; the program is
|
`next_generation()` is shown in Listing 17.4; the program is
|
||||||
otherwise identical to Listing 17.3.) Listing 17.4 applies the following
|
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
|
eliminating many function calls and from-scratch address/mask
|
||||||
calculations; all multiplies are eliminated by using pointers and
|
calculations; all multiplies are eliminated by using pointers and
|
||||||
addition; and all cells are accessed directly via pointers and masks,
|
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
|
The net effect of these optimizations is that Listing 17.4 is more than
|
||||||
twice as fast as Listing 17.3; we've achieved the desired 18 generations
|
twice as fast as Listing 17.3; we've achieved the desired 18 generations
|
||||||
per second, albeit only on a 486, and only at 96x96. (The **\#define**
|
per second, albeit only on a 486, and only at 96x96. (The `#define`
|
||||||
that enables code limiting the speed to 18 Hz, which seemed ridiculous
|
that enables code limiting the speed to 18 Hz, which seemed ridiculous
|
||||||
in Listing 17.1, is actually useful for keeping the generations from
|
in Listing 17.1, is actually useful for keeping the generations from
|
||||||
iterating too quickly when Listing 17.4 is running on a 486, especially
|
iterating too quickly when Listing 17.4 is running on a 486, especially
|
||||||
|
|
|
||||||
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
|
can skip over scads of unchanged cells at a pop simply by scanning for
|
||||||
non-zero bytes. This is much faster than explicitly testing cell states
|
non-zero bytes. This is much faster than explicitly testing cell states
|
||||||
and neighbor counts, and lends itself beautifully to assembly language
|
and neighbor counts, and lends itself beautifully to assembly language
|
||||||
implementation as **REPZ SCASB** or (with a little cleverness) **REPZ
|
implementation as `REPZ SCASB` or (with a little cleverness) `REPZ
|
||||||
SCASW.** (Unfortunately, there's no C library function that can scan
|
SCASW`. (Unfortunately, there's no C library function that can scan
|
||||||
memory for the next byte that's non-zero.)
|
memory for the next byte that's non-zero.)
|
||||||
|
|
||||||
Listing 17.5 is a Game of Life implementation that uses the
|
Listing 17.5 is a Game of Life implementation that uses the
|
||||||
|
|
|
||||||
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*
|
complex mask and pointer management, and the only thing that *really*
|
||||||
needs to be optimized is scanning for zero bytes. (And, in fact, I
|
needs to be optimized is scanning for zero bytes. (And, in fact, I
|
||||||
haven't optimized even that because it's done in a C++ loop; it should
|
haven't optimized even that because it's done in a C++ loop; it should
|
||||||
really be **REPZ SCASB.**)
|
really be `REPZ SCASB`.)
|
||||||
|
|
||||||
In truth, none of the code in Listing 17.5 is particularly
|
In truth, none of the code in Listing 17.5 is particularly
|
||||||
well-optimized, and, as I noted, the program must be compiled with the
|
well-optimized, and, as I noted, the program must be compiled with the
|
||||||
|
|
|
||||||
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.
|
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
|
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
|
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
|
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
|
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
|
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
|
execute two instructions, even instructions that access memory, in a
|
||||||
single clock. The cycle times for instruction execution in a given pipe
|
single clock. The cycle times for instruction execution in a given pipe
|
||||||
(both pipes process instructions at the same speed) are comparable to
|
(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
|
repeated string instructions, and some of the shifts and rotates—have
|
||||||
gotten faster.
|
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
|
I'll spend the rest of this chapter covering a variety of Pentium
|
||||||
optimization tips. For starters, effective address calculations (that
|
optimization tips. For starters, effective address calculations (that
|
||||||
is, the addition and scaling required to calculate a memory operand's
|
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
|
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
|
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,
|
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
|
both of the latter cases cause a 1-cycle penalty. The faster effective
|
||||||
address calculations have the side effect of making **LEA** very
|
address calculations have the side effect of making `LEA` very
|
||||||
attractive as an arithmetic instruction. **LEA** can add any two
|
attractive as an arithmetic instruction. `LEA` can add any two
|
||||||
registers, one of which can be multiplied by one, two, four, or eight,
|
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
|
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
|
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
|
chapter, `LEA` can go through either pipe, whereas `SHL` can only go
|
||||||
through the U-pipe, so **LEA** is often a superior choice for
|
through the U-pipe, so `LEA` is often a superior choice for
|
||||||
multiplication by three, four, five, eight, or nine. (**ADD** is the
|
multiplication by three, four, five, eight, or nine. (`ADD` is the
|
||||||
best choice for multiplication by two.) If you use **LEA** for
|
best choice for multiplication by two.) If you use `LEA` for
|
||||||
arithmetic, do remember that unlike **ADD** and **SHL**, it doesn't
|
arithmetic, do remember that unlike `ADD` and `SHL`, it doesn't
|
||||||
modify any flags.
|
modify any flags.
|
||||||
|
|
||||||
As on the 486, memory operands should not cross any more alignment
|
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.
|
Instruction prefixes are awfully expensive; avoid them if you can.
|
||||||
(These include size and addressing prefixes, segment overrides,
|
(These include size and addressing prefixes, segment overrides,
|
||||||
**LOCK**, and the 0FH prefixes that extend the instruction set with
|
`LOCK`, and the 0FH prefixes that extend the instruction set with
|
||||||
instructions such as **MOVSX**. The exceptions are conditional jumps, a
|
instructions such as `MOVSX`. The exceptions are conditional jumps, a
|
||||||
fast special case.) At a minimum, a prefix byte generally takes an extra
|
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
|
cycle and shuts down the V-pipe for that cycle, effectively costing as
|
||||||
much as two normal instructions (although prefix cycles can overlap with
|
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
|
if possible avoid putting data in the code segment and referring to it
|
||||||
with a CS: prefix, or otherwise using segment overrides.
|
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
|
multiprocessor machines, because it locks the bus and requires that the
|
||||||
hardware be brought into a synchronized state. The cost varies depending
|
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
|
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
|
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.
|
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
|
> 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
|
> 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
|
> 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
|
> indicate a five-cycle tax for `LOCK` in general, and a nine-cycle
|
||||||
> execution time for **XCHG** with memory. Avoid **XCHG** with memory if
|
> execution time for `XCHG` with memory. Avoid `XCHG` with memory if
|
||||||
> you possibly can.
|
> you possibly can.
|
||||||
|
|
||||||
As with the 486, don't use **ENTER** or **LEAVE**, which are slower than
|
As with the 486, don't use `ENTER` or `LEAVE`, which are slower than
|
||||||
the equivalent discrete instructions. Also, start using **TEST
|
the equivalent discrete instructions. Also, start using `TEST
|
||||||
*reg,reg*** instead of **AND *reg,reg*** or **OR *reg,reg*** to 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
|
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
|
that `TEST`, unlike `AND` and `OR`, never modifies the target
|
||||||
register. Although in this particular case **AND** and **OR** don't
|
register. Although in this particular case `AND` and `OR` don't
|
||||||
modify the target register either, the Pentium has no way of knowing
|
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
|
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.
|
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.
|
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
|
One much-needed instruction that has finally made it into the
|
||||||
instruction set is **CPUID**, which allows your code to determine what
|
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
|
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
|
finally here. Another new instruction is `CMPXCHG8B`, which does a
|
||||||
compare and conditional exchange on a qword. **CMPXCHG8B** doesn't seem
|
compare and conditional exchange on a qword. `CMPXCHG8B` doesn't seem
|
||||||
to me to be a particularly useful instruction, but I'm sure Intel
|
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,
|
wouldn't have added it without a reason; if you know of a use for it,
|
||||||
please pass it along to me.
|
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
|
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
|
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
|
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
|
expensive on the 486.) So discard your x86 preconceptions as we delve
|
||||||
into superscalar optimization for this one-of-a-kind processor.
|
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
|
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
|
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
|
instruction cycle counts. The V-pipe is a 486 subset, able to execute
|
||||||
simple instructions such as **MOV** and **ADD**, but unable to handle
|
simple instructions such as `MOV` and `ADD`, but unable to handle
|
||||||
**MUL, DIV**, string instructions, any sort of rotation or shift, or
|
`MUL, DIV`, string instructions, any sort of rotation or shift, or
|
||||||
even **ADC** or **SBB**.
|
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
|
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
|
relatively small subset of the Pentium's instruction set, but also
|
||||||
because those instructions that the V-pipe can handle are able to pair
|
because those instructions that the V-pipe can handle are able to pair
|
||||||
only with certain U-pipe instructions. For example, **MOVSD** uses both
|
only with certain U-pipe instructions. For example, `MOVSD` uses both
|
||||||
pipes, so no instruction can be executed in parallel with **MOVSD**.
|
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
|
> Pentium as on the 486, but it's nonetheless slower than using equivalent
|
||||||
> simpler instructions that allow for superscalar execution. Stick to the
|
> simpler instructions that allow for superscalar execution. Stick to the
|
||||||
> Pentium's RISC-like instructions—the pairable instructions I'll discuss
|
> Pentium's RISC-like instructions—the pairable instructions I'll discuss
|
||||||
> next—when you're seeking maximum performance, with just a few exceptions
|
> 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
|
Trickier yet, register contention can shut down the V-pipe on any given
|
||||||
cycle, and Address Generation Interlocks (AGIs) can stall either pipe at
|
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
|
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
|
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
|
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 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
|
cycle to execute in the U-pipe, and another instruction can potentially
|
||||||
execute concurrently in the V-pipe. (As you can see, similar instruction
|
execute concurrently in the V-pipe. (As you can see, similar instruction
|
||||||
sequences can have vastly different performance characteristics on the
|
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.
|
to some reports.
|
||||||
|
|
||||||
Besides, almost all operations can be performed by combinations of
|
Besides, almost all operations can be performed by combinations of
|
||||||
pairable instructions. For example, **PUSH [*mem*]** is not on either
|
pairable instructions. For example, `PUSH [*mem*]` is not on either
|
||||||
list, but both **MOV *reg*,[*mem*]** and **PUSH *reg*** are, and those
|
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,
|
two instructions can be used to push a value stored in memory. In fact,
|
||||||
given the proper instruction stream, the discrete instructions can
|
given the proper instruction stream, the discrete instructions can
|
||||||
perform this operation effectively in just 1 cycle (taking one-half of
|
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
|
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.
|
2 cycles.
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -189,7 +189,7 @@ mov [MemVar],edx
|
||||||
|
|
||||||
The single complex instruction takes 3 cycles and is 6 bytes long; with
|
The single complex instruction takes 3 cycles and is 6 bytes long; with
|
||||||
proper sequencing, interleaving the simple instructions with other
|
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.
|
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}
|
### 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
|
into three instructions, given that this instruction can go through
|
||||||
either pipe with equal ease. The answer is that while the
|
either pipe with equal ease. The answer is that while the
|
||||||
memory-accessing instructions other than **MOV, PUSH**, and **POP**
|
memory-accessing instructions other than `MOV, PUSH`, and `POP`
|
||||||
listed in Table 20.1 (that is, **INC/DEC [*mem*],
|
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 *reg*,[*mem*]`, and
|
||||||
**ADD/SUB/XOR/AND/OR/CMP/ADC/SBB [*mem*],*reg/immed***) can be paired,
|
`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
|
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
|
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
|
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
|
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,
|
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
|
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
|
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
|
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.
|
can be from everything we've learned about its predecessors.
|
||||||
|
|
||||||
The problem with pairing non-single-cycle instructions arises when a
|
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
|
memory operand. (I'll call these *complex memory instructions*. They're
|
||||||
the only pairable instructions, other than branches, that take more than
|
the only pairable instructions, other than branches, that take more than
|
||||||
one cycle.) We've already seen that, because instructions go through the
|
one cycle.) We've already seen that, because instructions go through the
|
||||||
pipes in lockstep, if one pipe executes a complex memory instruction
|
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
|
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
|
of the time, wasting cycles. You might think that if both pipes execute
|
||||||
complex instructions of the same length, then neither would lie idle,
|
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
|
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
|
instructions with explicit memory destination operands, such as **ADD
|
||||||
[EBX],EAX**. The Pentium executes only one such memory instruction at a
|
[EBX],EAX**. The Pentium executes only one such memory instruction at a
|
||||||
time; if two memory-destination complex instructions get paired, first
|
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
|
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
|
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.
|
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
|
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
|
time—2 in the U-pipe and 2 in the V-pipe, out of 10 cycles in all—are
|
||||||
incurred in the process.
|
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
|
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
|
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
|
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
|
DL,[EBX]` executes. Likewise, the result can't be stored until the
|
||||||
cycle after **AND DL,AL** has finished. This means that these
|
cycle after `AND DL,AL` has finished. This means that these
|
||||||
instructions, as written, can't possibly pair, so the sequence takes the
|
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
|
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
|
[EBX]`, AL takes 3 cycles.) Consequently, it's necessary to interleave
|
||||||
these instructions with instructions that use other registers, so this
|
these instructions with instructions that use other registers, so this
|
||||||
set of operations can execute in one pipe while the other, unrelated set
|
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.
|
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
|
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
|
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
|
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
|
this can help you understand why sometimes, timing reveals that your
|
||||||
code runs slower than it seems it should, although this will generally
|
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
|
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]
|
;V-pipe cycle 4 mov edx,[ebp-8]
|
||||||
```
|
```
|
||||||
|
|
||||||
This commonplace code loses a U-pipe cycle to the AGI caused by **AND
|
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
|
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
|
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
|
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
|
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
|
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
|
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.
|
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
|
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
|
are special-cased so you don't have to worry about AGIs. However, if you
|
||||||
explicitly modify ESP with this instruction
|
explicitly modify ESP with this instruction
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ either explicitly with instructions like this one
|
||||||
moveax,[esp+20h]
|
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.
|
as an addressing register.
|
||||||
|
|
||||||
On the 486, any instruction that had both a constant value and an
|
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
|
suffered a 1-cycle penalty, taking a total of 2 cycles. Such
|
||||||
instructions take only one cycle on the Pentium, but they cannot pair,
|
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
|
speed up something as simple as zeroing two memory variables, as in
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -67,7 +67,7 @@ mov [MemVar2],0 ;U-pipe 2
|
||||||
Note, however, that my experiments thus far indicate that the two writes
|
Note, however, that my experiments thus far indicate that the two writes
|
||||||
in the first case don't actually pair (possibly because the memory
|
in the first case don't actually pair (possibly because the memory
|
||||||
variables have never been read into the internal cache), so you might
|
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
|
this is yet another reason why you should always measure your code's
|
||||||
actual performance.
|
actual performance.
|
||||||
|
|
||||||
|
|
@ -85,9 +85,9 @@ inc eax ;U-pipe cycle 1
|
||||||
and ebx,eax ;U-pipe cycle 2
|
and ebx,eax ;U-pipe cycle 2
|
||||||
```
|
```
|
||||||
|
|
||||||
in a single cycle; **AND EBX,EAX** can't execute until the value in EAX
|
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,
|
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
|
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
|
in the last chapter when we discussed splitting instructions into simple
|
||||||
instructions, and it is by far the most common sort of register
|
instructions, and it is by far the most common sort of register
|
||||||
contention, known as read-after-write register contention.
|
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
|
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
|
same cycle. Write-after-write contention implies that the two
|
||||||
instructions comprising the above substitute for **MOVZX** should have
|
instructions comprising the above substitute for `MOVZX` should have
|
||||||
at least one unrelated instruction between them when **SUB EAX,EAX**
|
at least one unrelated instruction between them when `SUB EAX,EAX`
|
||||||
executes in the V-pipe.
|
executes in the V-pipe.
|
||||||
|
|
||||||
#### Exceptions to Register Contention {#Heading5}
|
#### Exceptions to Register Contention {#Heading5}
|
||||||
|
|
@ -129,10 +129,10 @@ are free of charge.
|
||||||
Also, stack-related instructions that modify ESP only implicitly
|
Also, stack-related instructions that modify ESP only implicitly
|
||||||
(without ESP as part of any explicit operand) do not cause AGIs, and
|
(without ESP as part of any explicit operand) do not cause AGIs, and
|
||||||
neither do they cause register contention with other instructions that
|
neither do they cause register contention with other instructions that
|
||||||
use ESP only implicitly; such instructions include **PUSH *reg/immed*,
|
use ESP only implicitly; such instructions include `PUSH *reg/immed*,
|
||||||
POP *reg***, and **CALL**. (However, these instructions do cause
|
POP *reg*`, and `CALL`. (However, these instructions do cause
|
||||||
register contention on ESP—but not AGIs—with instructions that use ESP
|
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:
|
following sequence would hardly use the V-pipe at all:
|
||||||
|
|
||||||
```nasm
|
```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 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
|
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
|
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.
|
can't pair.
|
||||||
|
|
||||||
### Who's in First? {#Heading6}
|
### 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
|
through. Instructions with prefix bytes are generally good U-pipe
|
||||||
markers, although they're expensive instructions that should be avoided
|
markers, although they're expensive instructions that should be avoided
|
||||||
whenever possible, and have at least one aberration with regard to pipe
|
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
|
instructions not listed in Table 20.1 in the last chapter are likewise
|
||||||
U-pipe markers.
|
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
|
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
|
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
|
"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
|
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]**.
|
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
|
Then, too, even though `DEC` and `JNZ` can pair and the branch
|
||||||
prediction for **JNZ** is presumably correct virtually all the time,
|
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
|
they do take a full cycle, and maybe we can do something about that as
|
||||||
well.
|
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
|
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
|
surprise, that the code actually takes only five cycles per checksum
|
||||||
word processed, not six. A little more experimentation revealed that
|
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
|
doesn't cost anything, certainly not the one full cycle in each pipe
|
||||||
that a prefix is supposed to take. More experimentation showed that
|
that a prefix is supposed to take. More experimentation showed that
|
||||||
prefix bytes do cost the documented extra cycle when used with one-cycle
|
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
|
is that prefixes can pair with the first cycle of at least some
|
||||||
multiple-cycle instructions. Determining exactly why this happens will
|
multiple-cycle instructions. Determining exactly why this happens will
|
||||||
take further research on my part, but the most important conclusion is
|
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
|
pages: 407-409
|
||||||
---
|
---
|
||||||
|
|
||||||
The first, obvious thing we can do to Listing 21.1 is change **ADC
|
The first, obvious thing we can do to Listing 21.1 is change `ADC AX,0` to
|
||||||
AX,0** to **ADC EAX,0**, eliminating a prefix byte and saving a full
|
`ADC EAX,0`, eliminating a prefix byte and saving a full cycle. Now we're
|
||||||
cycle. Now we're down from five to four cycles. What next?
|
down from five to four cycles. What next?
|
||||||
|
|
||||||
Listing 21.2 shows one interesting alternative that doesn't really buy
|
Listing 21.2 shows one interesting alternative that doesn't really buy
|
||||||
us anything. Here, we've eliminated all size prefixes by doing
|
us anything. Here, we've eliminated all size prefixes by doing
|
||||||
byte-sized **MOVs** and **ADDs**, but because the size prefix on **ADD
|
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,
|
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
|
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
|
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 which the code is broken into simple instructions and reordered so as
|
||||||
to avoid size prefixes, register contention, AGIs, and data bank
|
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).
|
same cache data bank, as discussed in the last chapter).
|
||||||
|
|
||||||
**LISTING 21.2 L21-2.ASM**
|
**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
|
byte-sized reads to avoid AGIs, register contention, and cache bank
|
||||||
collisions, in particular the way in which the byte reads of memory are
|
collisions, in particular the way in which the byte reads of memory are
|
||||||
interspersed with the additions to avoid register contention, and the
|
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**
|
**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
|
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
|
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
|
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
|
ESI (neither `LEA` nor `DEC` affects the carry flag, allowing Terje
|
||||||
to add the carry from the previous loop iteration into the next
|
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
|
percent faster. Nonetheless, Listings 21.1 through 21.5 illustrate many
|
||||||
of the techniques and considerations in Pentium optimization.
|
of the techniques and considerations in Pentium optimization.
|
||||||
Hand-optimization for the Pentium isn't simple, and requires careful
|
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
|
ClearS endp
|
||||||
```
|
```
|
||||||
|
|
||||||
The first thing you'll notice about Listing 22.1 is that **ClearS** uses
|
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
|
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
|
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 one repeated string instruction, and there's no way to improve on
|
||||||
that.
|
that.
|
||||||
|
|
||||||
Does that mean that Listing 22.1 is as good as it can be? Hardly. While
|
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
|
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
|
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.
|
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
|
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
|
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
|
to be equal to BP at that point anyway, so why reload it with the same
|
||||||
value? Removing that instruction saves us two bytes.
|
value? Removing that instruction saves us two bytes.
|
||||||
|
|
||||||
Well, that was certainly easy enough! We're not going to find any more
|
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
|
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.
|
of better instructions for particular tasks than those that were chosen.
|
||||||
For example, there's no need to load any register, whether segment or
|
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
|
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
|
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
|
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?
|
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.
|
loading ES and DI as shown in Listing 22.3.
|
||||||
|
|
||||||
**LISTING 22.3 L22-3.ASM**
|
**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.
|
counting.
|
||||||
|
|
||||||
We can save 3 more bytes by clearing the low and high bytes of AX and
|
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.
|
values as shown in Listing 22.4.
|
||||||
|
|
||||||
**LISTING 22.4 L22-4.ASM**
|
**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.)
|
that's not part of the specification for this particular routine.)
|
||||||
|
|
||||||
Another nifty instruction-rearrangement trick saves 6 more bytes.
|
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
|
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
|
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
|
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
|
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.
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
*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.
|
shorter than that.
|
||||||
|
|
||||||
There is one detail to be taken care of before we can put our plan into
|
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
|
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
|
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
|
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
|
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.
|
discard them when it's time to return.
|
||||||
|
|
||||||
With that problem dealt with, Listing 22.7 shows the Zenned version of
|
With that problem dealt with, Listing 22.7 shows the Zenned version of
|
||||||
**ClearS**.
|
`ClearS`.
|
||||||
|
|
||||||
**LISTING 22.7 L22-7.ASM**
|
**LISTING 22.7 L22-7.ASM**
|
||||||
|
|
||||||
|
|
@ -126,13 +126,13 @@ Bye:
|
||||||
ClearS endp
|
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
|
is just 19 bytes long. That's just 37 percent as long as the original
|
||||||
version, *without any change whatsoever in the functionality that
|
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
|
run a bit faster too, given that there are far fewer instruction bytes
|
||||||
and fewer memory accesses.
|
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
|
the original. Probably not the best possible implementation—*never say
|
||||||
never!*—but an awfully good one.
|
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
|
and GC Data registers are located at the addresses of their respective
|
||||||
Index registers plus one. However, the AC Index and Data registers are
|
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
|
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
|
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
|
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,
|
the Input Status 1 register (3DAH when the VGA is in a color mode,
|
||||||
|
|
||||||
| Register | Address |
|
| Register | Address |
|
||||||
|
|
@ -76,27 +76,27 @@ addressing of the now-vanished Color/Graphics Adapter and Monochrome
|
||||||
Display Adapter.
|
Display Adapter.
|
||||||
|
|
||||||
The method used in the VGA BIOS to set registers is to point DX to the
|
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
|
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
|
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
|
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.
|
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
|
However, this technique does not work for programming the AC Index and
|
||||||
Data registers; both AC registers are addressed at 3C0H, so two separate
|
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
|
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
|
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.
|
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
|
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
|
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, so any computer or VGA that doesn't properly support word
|
||||||
**OUT**s could scarcely be considered a clone at all.
|
`OUT`s could scarcely be considered a clone at all.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> A speed tip: The setting of each chip's Index register remains the same
|
> 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>
|
> INC DX ;point to GC Data register>
|
||||||
>
|
>
|
||||||
> and then the Bit Mask register could be set repeatedly with the
|
> 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
|
> 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
|
> 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
|
> course, this method only works if the GC Index register remains
|
||||||
> unchanged throughout the loop.
|
> 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
|
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
|
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
|
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
|
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
|
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
|
causes unacceptably slow performance, all the more so because the wait
|
||||||
states that occur on accesses to VGA memory make it very desirable to
|
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.
|
slow.
|
||||||
|
|
||||||
The solution is to take advantage of the VGA's write mode 1, which is
|
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
|
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
|
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
|
addressed byte from all four planes into the VGA's four internal
|
||||||
latches, and a single **CPU** write writes the contents of the latches
|
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
|
to the four planes. During the write, the byte written by the `CPU` is
|
||||||
irrelevant.
|
irrelevant.
|
||||||
|
|
||||||
The sample program uses write mode 1 to copy the images that were
|
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
|
size, page flipping, and image storage must always be kept in mind when
|
||||||
designing programs for the VGA.
|
designing programs for the VGA.
|
||||||
|
|
||||||
To see the program run in 640x200 16-color mode, comment out the **EQU**
|
To see the program run in 640x200 16-color mode, comment out the `EQU`
|
||||||
line for **MEDRES\_VIDEO\_MODE**.
|
line for `MEDRES_VIDEO_MODE`.
|
||||||
|
|
||||||
### The Hazards of VGA Clones {#Heading10}
|
### 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
|
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
|
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
|
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.
|
performance would slow to a crawl.
|
||||||
|
|
||||||
Figure 24.1 is a simplified depiction of data flow around the ALUs. Each
|
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
|
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
|
bit of a nuisance, since it means that the value of some 8-bit register
|
||||||
must be destroyed. Under certain circumstances, a single logical
|
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
|
read to load the latches and then write to modify display memory without
|
||||||
affecting any CPU registers, as we'll see later on.
|
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
|
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 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 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
|
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.
|
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
|
which requires four instruction bytes. By shifting the value destined
|
||||||
for the high byte into the high byte with MASM's shift-left operator,
|
for the high byte into the high byte with MASM's shift-left operator,
|
||||||
**SHL** (\*100H would work also), and then logically combining the
|
`SHL` (\*100H would work also), and then logically combining the
|
||||||
values with MASM's **OR** operator (or the **ADD** operator), both
|
values with MASM's `OR` operator (or the `ADD` operator), both
|
||||||
halves of DX can be loaded with a single instruction, as in
|
halves of DX can be loaded with a single instruction, as in
|
||||||
|
|
||||||
```nasm
|
```nasm
|
||||||
|
|
@ -106,7 +106,7 @@ MOV DX,(VALUE2 SHL 8) OR VALUE1
|
||||||
|
|
||||||
which takes only three bytes and is faster, being a single instruction.
|
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
|
(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
|
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
|
ideal place to use this technique; the macro invocation can refer to two
|
||||||
separate byte values, making matters easier for the programmer, while
|
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.
|
constant.
|
||||||
|
|
||||||
> 
|
> 
|
||||||
> A minor optimization tip illustrated in the listing is the use of **INC
|
> A minor optimization tip illustrated in the listing is the use of `INC
|
||||||
> AX** and **DEC AX** in the **DrawVerticalBox** subroutine when only AL
|
> AX` and `DEC AX` in the `DrawVerticalBox` subroutine when only AL
|
||||||
> actually needs to be modified. Word-sized register increment and
|
> actually needs to be modified. Word-sized register increment and
|
||||||
> decrement instructions (or dword-sized instructions in 32-bit protected
|
> decrement instructions (or dword-sized instructions in 32-bit protected
|
||||||
> mode) are only one byte long, while byte-size register increment and
|
> mode) are only one byte long, while byte-size register increment and
|
||||||
> decrement instructions are two bytes long. Consequently, when size
|
> decrement instructions are two bytes long. Consequently, when size
|
||||||
> counts, it is worth using a whole 16-bit (or 32-bit) register instead of
|
> 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
|
> 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 register.
|
||||||
|
|
||||||
The latches and ALUs are central to high-performance VGA code, since
|
The latches and ALUs are central to high-performance VGA code, since
|
||||||
they allow programs to process across all four memory planes without a
|
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
|
arrange a program to exploit this power, however, because the ALUs are
|
||||||
far more limited than a CPU. In many instances, however, additional
|
far more limited than a CPU. In many instances, however, additional
|
||||||
hardware in the VGA, including the bit mask, the set/reset features, and
|
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
|
The barrel shifter is powerful, but (as sometimes happens in this
|
||||||
business) it sounds more useful than it really is. This is because the
|
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
|
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
|
rotation: one to set the GC Index register, and one to set the Data
|
||||||
Rotate register. However, with careful programming it's sometimes
|
Rotate register. However, with careful programming it's sometimes
|
||||||
possible to leave the GC Index always pointing to the Data Rotate
|
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
|
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
|
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
|
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
|
rotate *latched* data, then there would be all sorts of useful
|
||||||
applications for rotation, but, sadly, only CPU data can be rotated.
|
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}
|
### A Brief Note on Word OUTs {#Heading9}
|
||||||
|
|
||||||
In the early days of the EGA and VGA, there was considerable debate
|
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
|
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
|
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
|
Data register first, then Index register, with predictably disastrous
|
||||||
results. Consequently, I generally wrote my code in those days to use
|
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
|
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
|
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
|
`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
|
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
|
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
|
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.
|
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
|
The first time I encountered write mode 3, I understood immediately how
|
||||||
it functioned, but could think of very few useful applications for it.
|
it functioned, but could think of very few useful applications for it.
|
||||||
As time passed, and as I came to understand the atrocious performance
|
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
|
pattern drawing as well, write mode 3 grew considerably in my
|
||||||
estimation. In fact, my esteem for this mode ultimately reached the
|
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,
|
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
|
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,
|
write mode 3 is excellent for a surprisingly large number of purposes,
|
||||||
because it makes it possible to avoid the bane of VGA performance,
|
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
|
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
|
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.
|
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
|
> 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
|
> 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,
|
> sets that register is time-consuming, especially for proportional text,
|
||||||
> which requires a different rotation for each character. Also, if the
|
> which requires a different rotation for each character. Also, if the
|
||||||
> code performs full-byte accesses to display memory—that is, if it
|
> 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
|
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
|
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
|
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
|
be those required to set the bit mask and data rotation. This technique
|
||||||
could well outperform single-character bit-mapped text drivers such as
|
could well outperform single-character bit-mapped text drivers such as
|
||||||
the one in Listing 26.1 by a significant margin. Listing 26.2
|
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
|
combine adjacent characters into complete, rotated bytes whenever
|
||||||
possible, so that only one drawing operation would be required per byte
|
possible, so that only one drawing operation would be required per byte
|
||||||
of display memory modified. By doing this, we would eliminate all
|
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.
|
approximately doubling text-drawing speed.
|
||||||
|
|
||||||
As a final note, consider that non-transparent text could also be
|
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