642 lines
25 KiB
Markdown
642 lines
25 KiB
Markdown
---
|
||
title: Michael Abrash's Graphics Programming Black Book, Special Edition
|
||
author: Michael Abrash
|
||
date: '1997-07-01'
|
||
identifier:
|
||
- scheme: ISBN
|
||
text: 1576101746
|
||
publisher: The Coriolis Group
|
||
category: 'Web and Software Development: Game Development,Web and Software Development:
|
||
Graphics and Multimedia Development'
|
||
chapter: '12'
|
||
pages: 233-246
|
||
---
|
||
|
||
## Chapter 12 -- Pushing the 486
|
||
|
||
### It's Not Just a Bigger 386
|
||
|
||
So this traveling salesman is walking down a road, and he sees a group
|
||
of men digging a ditch with their bare hands. "Whoa, there!" he says.
|
||
"What you guys need is a Model 8088 ditch digger!" And he whips out a
|
||
trowel and sells it to them.
|
||
|
||
A few days later, he stops back around. They're happy with the trowel,
|
||
but he sells them the latest ditch-digging technology, the Model 80286
|
||
spade. That keeps them content until he stops by again with a Model
|
||
80386 shovel (a full 32 inches wide, with a narrow point to emulate the
|
||
trowel), and *that* holds them until he comes back around with what they
|
||
really need: a Model 80486 bulldozer.
|
||
|
||
Having reached the top of the line, the salesman doesn't pay them a call
|
||
for a while. When he does, not only are they none too friendly, but
|
||
they're digging with the 80386 shovel; the bulldozer is sitting off to
|
||
one side. "Why on earth are you using that shovel?" the salesman asks.
|
||
"Why aren't you digging with the bulldozer?"
|
||
|
||
"Well, Lord knows we tried," says the foreman, "but it was all we could
|
||
do just to lift the damn thing!"
|
||
|
||
Substitute "processor" for the various digging implements, and you get
|
||
an idea of just how different the optimization rules for the 486 are
|
||
from what you're used to. Okay, it's not quite *that* bad—but upon
|
||
encountering a processor where string instructions are often to be
|
||
avoided and memory-to-register `MOV`s are frequently as fast as
|
||
register-to-register `MOV`s, Dorothy was heard to exclaim (before she
|
||
sank out of sight in a swirl of hopelessly mixed metaphors), "I don't
|
||
think we're in Kansas anymore, Toto."
|
||
|
||
#### Enter the 486
|
||
|
||
No chip that is a direct, fully compatible descendant of the 8088, 286,
|
||
and 386 could ever be called a RISC chip, but the 486 certainly contains
|
||
RISC elements, and it's those elements that are most responsible for
|
||
making 486 optimization unique. Simple, common instructions are executed
|
||
in a single cycle by a RISC-like core processor, but other instructions
|
||
are executed pretty much as they were on the 386, where every
|
||
instruction takes at least 2 cycles. For example, `MOV AL, [TestChar]`
|
||
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
|
||
cycle *slower* than on the 386. The floating-point execution unit inside
|
||
the 486 is also much faster than the 387 math coprocessor, largely
|
||
because, being in the same silicon as the CPU (the 486 has a math
|
||
coprocessor built in), it is more tightly coupled. The results are
|
||
sometimes startling: `FMUL` (floating point multiply) is usually
|
||
faster on the 486 than `IMUL` (integer multiply)!
|
||
|
||
An encyclopedic approach to 486 optimization would take a book all by
|
||
itself, so in this chapter I'm only going to hit the highlights of 486
|
||
optimization, touching on several optimization rules, some documented,
|
||
some not. You might also want to check out the following sources of 486
|
||
information: *i486 Microprocessor Programmer's Reference Manual,* from
|
||
Intel; "8086 Optimization: Aim Down the Middle and Pray," in the March,
|
||
1991 *Dr. Dobb's Journal*; and "Peak Performance: On to the 486," in the
|
||
November, 1990 *Programmer's Journal.*
|
||
|
||
### Rules to Optimize By
|
||
|
||
In Appendix G of the *i486 Microprocessor Programmer*'*s* *Reference
|
||
Manual*, Intel lists a number of optimization techniques for the 486.
|
||
While neither exhaustive (we'll look at two undocumented optimizations
|
||
shortly) nor entirely accurate (we'll correct two of the rules here),
|
||
Intel's list is certainly a good starting point. In particular, the list
|
||
conveys the extent to which 486 optimization differs from optimization
|
||
for earlier x86 processors. Generally, I'll be discussing optimization
|
||
for real mode (it being the most widely used mode at the moment),
|
||
although many of the rules should apply to protected mode as well.
|
||
|
||
> 
|
||
> 486 optimization is generally more precise and less frustrating than
|
||
> optimization for other x86 processors because every 486 has an identical
|
||
> internal cache. Whenever both the instructions being executed and the
|
||
> data the instructions access are in the cache, those instructions will
|
||
> run in a consistent and calculatable number of cycles on all 486s, with
|
||
> little chance of interference from the prefetch queue and without regard
|
||
> to the speed of external memory.
|
||
|
||
In other words, for cached code (which time-critical code almost always
|
||
is), performance is predictable and can be calculated with good
|
||
precision, and those calculations will apply on any 486. However,
|
||
"predictable" doesn't mean "trivial"; the cycle times printed for the
|
||
various instructions are not the whole story. You must be aware of all
|
||
the rules, documented and undocumented, that go into calculating actual
|
||
execution times—and uncovering some of those rules is exactly what this
|
||
chapter is about.
|
||
|
||
#### The Hazards of Indexed Addressing
|
||
|
||
Rule \#1: Avoid indexed addressing (that is, try not to use either two
|
||
registers or scaled addressing to point to memory).
|
||
|
||
Intel cautions against using indexing to address memory because there's
|
||
a one-cycle penalty for indexed addressing. True enough—but "indexed
|
||
addressing" might not mean what you expect.
|
||
|
||
Traditionally, SI and DI are considered the index registers of the x86
|
||
CPUs. That is not the sense in which "indexed addressing" is meant here,
|
||
however. In real mode, indexed addressing means that two registers,
|
||
rather than one or none, are used to point to memory. (In this context,
|
||
the use of one register to address memory is "base addressing," no
|
||
matter what register is used.) `MOV AX, [BX+DI]` and `MOV CL,
|
||
[BP+SI+10]` perform indexed addressing; `MOV AX,[BX]` and `MOV DL,
|
||
[SI+1]` do not.
|
||
|
||
> 
|
||
> Therefore, in real mode, the rule is to avoid using two registers to
|
||
> point to memory whenever possible. Often, this simply means adding the
|
||
> two registers together outside a loop before memory is actually
|
||
> addressed.
|
||
|
||
As an example, you might adhere to this rule by replacing the code
|
||
|
||
```nasm
|
||
LoopTop:
|
||
add ax,[bx+si]
|
||
add si,2
|
||
dec cx
|
||
jnz LoopTop
|
||
```
|
||
|
||
with this
|
||
|
||
```nasm
|
||
add si,bx
|
||
LoopTop:
|
||
add ax,[si]
|
||
add si,2
|
||
dec cx
|
||
jnz LoopTop
|
||
sub si,bx
|
||
```
|
||
|
||
which calculates the same sum and leaves the registers in the same state
|
||
as the first example, but avoids indexed addressing.
|
||
|
||
In protected mode, the definition of indexed addressing is a tad more
|
||
complex. The use of two registers to address memory, as in `MOV EAX,
|
||
[EDX+EDI]`, still qualifies for the one-cycle penalty. In addition, the
|
||
use of 386/486 scaled addressing, as in `MOV [ECX*2],EAX`, also
|
||
constitutes indexed addressing, even if only one register is used to
|
||
point to memory.
|
||
|
||
All this fuss over one cycle! You might well wonder how much difference
|
||
one cycle could make. After all, on the 8088, effective address
|
||
calculations take a *minimum* of 5 cycles. On the 486, however, 1 cycle
|
||
is a big deal because many instructions, including most register-only
|
||
instructions (`MOV`, `ADD`, `CMP`, and so on) execute in just 1
|
||
cycle. In particular, `MOV`s to and from memory execute in 1 cycle—if
|
||
they're not hampered by something like indexed addressing, in which case
|
||
they slow to half speed (or worse, as we will see shortly).
|
||
|
||
For example, consider the summing example shown earlier. The version
|
||
that uses base+index ([BX+SI]) addressing executes in eight cycles per
|
||
loop. As expected, the version that uses base ([SI]) addressing runs one
|
||
cycle faster, at seven cycles per loop. However, the loop code executes
|
||
so fast on the 486 that the single cycle saved by using base addressing
|
||
makes the *whole loop* more than 14 percent faster.
|
||
|
||
In a key loop on the 486, 1 cycle can indeed matter.
|
||
|
||
#### Calculate Memory Pointers Ahead of Time
|
||
|
||
Rule \#2: Don't use a register as a memory pointer during the next two
|
||
cycles after loading it.
|
||
|
||
Intel states that if the destination of one instruction is used as the
|
||
base addressing component of the next instruction, then a one-cycle
|
||
penalty is imposed. This rule, unlike anything ever before seen in the
|
||
x86 family, reflects the heavily pipelined nature of the 486.
|
||
Apparently, the 486 starts each effective address calculation before the
|
||
start of the instruction that will need it, as shown in Figure 12.1;
|
||
this effectively makes the address calculation time vanish, because it
|
||
happens while the preceding instruction executes.
|
||
|
||
Of course, the 486 *can't* perform an effective address calculation for
|
||
a target instruction ahead of time if one of the address components
|
||
isn't known until the instruction starts, and that's exactly the case
|
||
when the preceding instruction modifies one of the target instruction's
|
||
addressing registers. For example, in the code
|
||
|
||
```nasm
|
||
MOV BX,OFFSET MemVar
|
||
MOV AX,[BX]
|
||
```
|
||
|
||
there's no way that the 486 can calculate the address referenced by
|
||
`MOV AX,[BX]` until `MOV BX,OFFSET MemVar` finishes, so pipelining
|
||
that calculation ahead of time is not possible. A good workaround is
|
||
rearranging your code so that at least one instruction lies between the
|
||
loading of the memory pointer and its use. For example,
|
||
postdecrementing, as in the following
|
||
|
||
```nasm
|
||
LoopTop:
|
||
add ax,[si]
|
||
add si,2
|
||
dec cx
|
||
jnz LoopTop
|
||
```
|
||
|
||
is faster than preincrementing, as in:
|
||
|
||
```nasm
|
||
LoopTop:
|
||
add si,2
|
||
add ax,[SI]
|
||
dec cx
|
||
jnz LoopTop
|
||
```
|
||
|
||
Now that we understand what Intel means by this rule, let me make a very
|
||
important comment: My observations indicate that for real-mode code, the
|
||
documentation understates the extent of the penalty for interrupting the
|
||
address calculation pipeline by loading a memory pointer just before
|
||
it's used.
|
||
|
||
> 
|
||
> The truth of the matter appears to be that if a register is the
|
||
> destination of one instruction and is then used by the next instruction
|
||
> to address memory in real mode, not one but two cycles are lost!
|
||
|
||
In 32-bit protected mode, however, the penalty is, in fact, the 1 cycle
|
||
that Intel .
|
||
|
||
Considering that `MOV` normally takes only one cycle total, that's
|
||
quite a loss. For example, the postdecrement loop shown above is 2 full
|
||
cycles faster than the preincrement loop, resulting in a 29 percent
|
||
improvement in the performance of the entire loop. But wait, there's
|
||
more. If a register is loaded 2 cycles (which generally means 2
|
||
instructions, but, because some 486 instructions take more than 1 cycle,
|
||
|
||

|
||
|
||
the 2 are not always equivalent) before it's used to point to memory, 1
|
||
cycle is lost. Therefore, whereas this code
|
||
|
||
```nasm
|
||
mov bx,offset MemVar
|
||
mov ax,[bx]
|
||
inc dx
|
||
dec cx
|
||
jnz LoopTop
|
||
```
|
||
|
||
loses two cycles from interrupting the address calculation pipeline,
|
||
this code
|
||
|
||
```nasm
|
||
mov bx,offset MemVar
|
||
inc dx
|
||
mov ax,[bx]
|
||
dec cx
|
||
jnz LoopTop
|
||
```
|
||
|
||
loses only one cycle, and this code
|
||
|
||
```nasm
|
||
mov bx,offset MemVar
|
||
inc dx
|
||
dec cx
|
||
mov ax,[bx]
|
||
jnz LoopTop
|
||
```
|
||
|
||
loses no cycles at all. Apparently, the 486's addressing calculation
|
||
pipeline actually starts 2 cycles ahead, as shown in Figure 12.2. (In
|
||
truth, my best guess at the moment is that the addressing pipeline
|
||
really does start only 1 cycle ahead; the additional cycle crops up when
|
||
the addressing pipeline has to wait for a register to be written into
|
||
the register file before it can read it out for use in addressing
|
||
calculations. However, I'm guessing here, and the 2-cycle-ahead model in
|
||
Figure 12.2 will do just fine for optimization purposes.)
|
||
|
||
Clearly, there's considerable optimization potential in careful
|
||
rearrangement of 486 code.
|
||
|
||

|
||
|
||
### Caveat Programmor
|
||
|
||
A caution: I'm quite certain that the 2-cycle-ahead addressing pipeline
|
||
interruption penalty I've described exists in the two 486s I've tested.
|
||
However, there's no guarantee that Intel won't change this aspect of the
|
||
486 in the future, especially given that the documentation indicates
|
||
otherwise. Perhaps the 2-cycle penalty is the result of a bug in the
|
||
initial steps of the 486, and will revert to the documented 1-cycle
|
||
penalty someday; likewise for the undocumented optimizations I'll
|
||
describe below. Nonetheless, none of the optimizations I suggest would
|
||
hurt performance even if the undocumented performance characteristics of
|
||
the 486 were to vanish, and they certainly will help performance on at
|
||
least some 486s right now, so I feel they're well worth using.
|
||
|
||
There is, of course, no guarantee that I'm entirely correct about the
|
||
optimizations discussed in this chapter. Without knowing the internals
|
||
of the 486, all I can do is time code and make inferences from the
|
||
results; I invite you to deduce your own rules and cross-check them
|
||
against mine. Also, most likely there are other optimizations that I'm
|
||
unaware of. If you have further information on these or any other
|
||
undocumented optimizations, please write and let me know. And, of
|
||
course, if anyone from Intel is reading this and wants to give us the
|
||
gospel truth, please do!
|
||
|
||
#### Stack Addressing and Address Pipelining
|
||
|
||
Rule \#2A: Rule \#2 sometimes, but not always, applies to the stack
|
||
pointer when it is implicitly used to point to memory.
|
||
|
||
Intel states that the stack pointer is an implied destination register
|
||
for `CALL`, `ENTER`, `LEAVE`, `RET`, `PUSH`, and `POP`
|
||
(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).
|
||
Intel then implies that the aforementioned addressing pipeline penalty
|
||
is incurred whenever the stack pointer is used as a destination by one
|
||
of the first set of instructions and is then immediately used to address
|
||
memory by one of the second set. This raises the specter of unpleasant
|
||
programming contortions such as intermixing `PUSH`es and `POP`s with
|
||
other instructions to avoid interrupting the addressing pipeline.
|
||
Fortunately, matters are actually not so grim as Intel's documentation
|
||
would indicate; my tests indicate that the addressing pipeline penalty
|
||
pops up only spottily when the stack pointer is involved.
|
||
|
||
For example, you'd certainly expect a sequence such as
|
||
|
||
```nasm
|
||
:
|
||
pop ax
|
||
ret
|
||
pop ax
|
||
et
|
||
:
|
||
```
|
||
|
||
to exhibit the addressing pipeline interruption phenomenon (SP is both
|
||
destination and addressing register for both instructions, according to
|
||
Intel), but this code runs in six cycles per `POP/RET` pair, matching
|
||
the official execution times exactly. Likewise, a sequence like
|
||
|
||
```nasm
|
||
pop dx
|
||
pop cx
|
||
pop bx
|
||
pop ax
|
||
```
|
||
|
||
runs in one cycle per instruction, just as it should.
|
||
|
||
On the other hand, performing arithmetic directly on SP as an *explicit*
|
||
destination—for example, to deallocate local variables—and then using
|
||
`PUSH`, `POP`, or `RET`, definitely can interrupt the addressing
|
||
pipeline. For example
|
||
|
||
```nasm
|
||
add sp,10h
|
||
ret
|
||
```
|
||
|
||
loses two cycles because SP is the explicit destination of one
|
||
instruction and then the implied addressing register for the next, and
|
||
the sequence
|
||
|
||
```nasm
|
||
add sp,10h
|
||
pop ax
|
||
```
|
||
|
||
loses two cycles for the same reason.
|
||
|
||
I certainly haven't tried all possible combinations, but the results so
|
||
far indicate that the stack pointer incurs the addressing pipeline
|
||
penalty only if (E)SP is the *explicit* destination of one instruction
|
||
and is then used by one of the two following instructions to address
|
||
memory. So, for instance, SP isn't the explicit operand of `POP AX`-AX
|
||
is—and no cycles are lost if `POP AX` is followed by `POP` or
|
||
`RET`. Happily, then, we need not worry about the sequence in which we
|
||
use `PUSH` and `POP`. However, adding to, moving to, or subtracting
|
||
from the stack pointer should ideally be done at least two cycles before
|
||
`PUSH`, `POP`, `RET`, or any other instruction that uses the stack
|
||
pointer to address memory.
|
||
|
||
#### Problems with Byte Registers
|
||
|
||
There are two ways to lose cycles by using byte registers, and neither
|
||
of them is documented by Intel, so far as I know. Let's start with the
|
||
lesser and simpler of the two.
|
||
|
||
Rule \#3: Do not load a byte portion of a register during one
|
||
instruction, then use that register in its entirety as a source register
|
||
during the next instruction.
|
||
|
||
So, for example, it would be a bad idea to do this
|
||
|
||
```nasm
|
||
mov ah,o
|
||
:
|
||
mov cx,[MemVar1]
|
||
mov al,[MemVar2]
|
||
add cx,ax
|
||
```
|
||
|
||
because AL is loaded by one instruction, then AX is used as the source
|
||
register for the next instruction. A cycle can be saved simply by
|
||
rearranging the instructions so that the byte register load isn't
|
||
immediately followed by the word register usage, like so:
|
||
|
||
```nasm
|
||
mov ah,o
|
||
:
|
||
mov al,[MemVar2]
|
||
mov cx,[MemVar1]
|
||
add cx,ax
|
||
```
|
||
|
||
Strange as it may seem, this rule is neither arbitrary nor nonsensical.
|
||
Basically, when a byte destination register is part of a word source
|
||
register for the next instruction, the 486 is unable to directly use the
|
||
result from the first instruction as the source for the second
|
||
instruction, because only part of the register required by the second
|
||
instruction is contained in the first instruction's result. The full,
|
||
updated register value must be read from the register file, and that
|
||
value can't be read out until the result from the first instruction has
|
||
been written *into* the register file, a process that takes an extra
|
||
cycle. I'm not going to explain this in great detail because it's not
|
||
important that you understand why this rule exists (only that it *does*
|
||
in fact exist), but it is an interesting window on the way the 486
|
||
works.
|
||
|
||
In case you're curious, there's no such penalty for the typical `XLAT`
|
||
sequence like
|
||
|
||
```nasm
|
||
mov bx,offset MemTable
|
||
:
|
||
mov al,[si]
|
||
xlat
|
||
```
|
||
|
||
even though AL must be converted to a word by `XLAT` before it can be
|
||
added to BX and used to address memory. In fact, none of the penalties
|
||
mentioned in this chapter apply to `XLAT`, apparently because `XLAT`
|
||
is so slow—4 cycles—that it gives the 486 time to perform addressing
|
||
calculations during the course of the instruction.
|
||
|
||
> 
|
||
> While it's nice that `XLAT` doesn't suffer from the various 486
|
||
> addressing penalties, the reason for that is basically that `XLAT` is
|
||
> slow, so there's still no compelling reason to use `XLAT` on the 486.
|
||
|
||
In general, penalties for interrupting the 486's pipeline apply
|
||
primarily to the fast core instructions of the 486, most notably
|
||
register-only instructions and `MOV`, although arithmetic and logical
|
||
operations that access memory are also often affected. I don't know all
|
||
the performance dependencies, and I don't plan to; figuring all of them
|
||
out would be a big, boring job of little value. Basically, on the 486
|
||
you should concentrate on using those fast core instructions when
|
||
performance matters, and all the rules I'll discuss do indeed apply to
|
||
those instructions.
|
||
|
||
You don't need to understand every corner of the 486 universe unless
|
||
you're a diehard ASMhead who does this stuff for fun. Just learn enough
|
||
to be able to speed up the key portions of your programs, and spend the
|
||
rest of your time on a fast design and overall implementation.
|
||
|
||
#### More Fun with Byte Registers
|
||
|
||
Rule \#4: Don't load *any* byte register exactly 2 cycles before using
|
||
*any* register to address memory.
|
||
|
||
This, the last of this chapter's rules, is the strangest of the lot. If
|
||
any byte register is loaded, and then two cycles later any register is
|
||
used to point to memory, one cycle is lost. So, for example, this code
|
||
|
||
```nasm
|
||
mov al,bl
|
||
mov cx,dx
|
||
mov si,[di]
|
||
```
|
||
|
||
takes four rather than the expected three cycles to execute. Note that
|
||
it is *not* required that the byte register be part of the register used
|
||
to address memory; any byte register will do the trick.
|
||
|
||
Worse still, loading byte registers both one and two cycles before a
|
||
register is used to address memory costs two cycles, as in
|
||
|
||
```nasm
|
||
mov bl,al
|
||
mov cl,3
|
||
mov bx,[si]
|
||
```
|
||
which takes five rather than three cycles to run. However, there is *no*
|
||
penalty if a byte register is loaded one cycle but not two cycles before
|
||
a register is used to address memory. Therefore,
|
||
|
||
```nasm
|
||
mov cx,3
|
||
mov dl,al
|
||
mov si,[bx]
|
||
```
|
||
|
||
runs in the expected three cycles.
|
||
|
||
In truth, I do not know why this happens. Clearly, it has something to
|
||
do with interrupting the start of the addressing pipeline, and I have my
|
||
theories about how this works, but at this point they're pure
|
||
speculation. Whatever the reason for this rule, ignorance of it—and of
|
||
its interaction with the other rules—could lead to considerable
|
||
performance loss in seemingly air-tight code. For instance, a casual
|
||
observer would expect the following code to run in 3 cycles:
|
||
|
||
```nasm
|
||
mov bx,offset MemVar
|
||
mov cl,al
|
||
mov ax,[bx]
|
||
```
|
||
|
||
A more sophisticated programmer would expect to lose one cycle, because
|
||
BX is loaded two cycles before being used to address memory. In fact,
|
||
though, this code takes 5 cycles—2 cycles, or 67 percent, longer than
|
||
normal. Why? Well, under normal conditions, loading a byte register—CL
|
||
in this case—one cycle before using a register to address memory
|
||
produces no penalty; loading 2 cycles ahead is the only case that
|
||
normally incurs a penalty. However, think of Rule \#4 as meaning that
|
||
loading a byte register disrupts the memory addressing pipeline as it
|
||
starts up. Viewed that way, we can see that `MOV BX,OFFSET MemVar`
|
||
interrupts the addressing pipeline, forcing it to start again, and then,
|
||
presumably, `MOV CL,AL` interrupts the pipeline again because the
|
||
pipeline is now on its first cycle: the one that loading a byte register
|
||
can affect.
|
||
|
||
> 
|
||
> I know—it seems awfully complicated. It isn't, really. Generally, try
|
||
> not to use byte destinations exactly two cycles before using a register
|
||
> to address memory, and try not to load a register either one or two
|
||
> cycles before using it to address memory, and you'll be fine.
|
||
|
||
#### Timing Your Own 486 Code
|
||
|
||
In case you want to do some 486 performance analysis of your own, let me
|
||
show you how I arrived at one of the above conclusions; at the same
|
||
time, I can warn you of the timing hazards of the cache. Listings 12.1
|
||
and 12.2 show the code I ran through the Zen timer in order to establish
|
||
the effects of loading a byte register before using a register to
|
||
address memory. Listing 12.1 ran in 120 µs on a 33 MHz 486, or 4 cycles
|
||
per repetition (120 µs/1000 repetitions = 120 ns per repetition; 120 ns
|
||
per repetition/30 ns per cycle = 4 cycles per repetition); Listing 12.2
|
||
ran in 90 µs, or 3 cycles, establishing that loading a byte register
|
||
costs a cycle only when it's performed exactly 2 cycles before
|
||
addressing memory.
|
||
|
||
**LISTING 12.1 LST12-1.ASM**
|
||
|
||
```nasm
|
||
; Measures the effect of loading a byte register 2 cycles before
|
||
; using a register to address memory.
|
||
mov bp,2 ;run the test code twice to make sure
|
||
; it's cached
|
||
sub bx,bx
|
||
CacheFillLoop:
|
||
call ZTimerOn ;start timing
|
||
rept 1000
|
||
mov dl,cl
|
||
nop
|
||
mov ax,[bx]
|
||
endm
|
||
call ZTimerOff ;stop timing
|
||
dec bp
|
||
jz Done
|
||
jmp CacheFillLoop
|
||
Done:
|
||
```
|
||
|
||
**LISTING 12.2 LST12-2.ASM**
|
||
|
||
```nasm
|
||
; Measures the effect of loading a byte register 1 cycle before
|
||
; using a register to address memory.
|
||
mov bp,2 ;run the test code twice to make sure
|
||
; it's cached
|
||
sub bx,bx
|
||
CacheFillLoop:
|
||
call ZTimerOn ;start timing
|
||
rept 1000
|
||
nop
|
||
mov dl,cl
|
||
mov ax,[bx]
|
||
endm
|
||
call ZTimerOff ;stop timing
|
||
dec bp
|
||
jz Done
|
||
jmp CacheFillLoop
|
||
Done:
|
||
```
|
||
|
||
Note that Listings 12.1 and 12.2 each repeat the timing of the code
|
||
under test a second time, to make sure that the instructions are in the
|
||
cache on the second pass, the one for which results are displayed. Also
|
||
note that the code is less than 8K in size, so that it can all fit in
|
||
the 486's 8K internal cache. If I double the `REPT` value in Listing
|
||
12.2 to 2,000, making the test code larger than 8K, the execution time
|
||
more than doubles to 224 µs, or 3.7 cycles per repetition; the extra
|
||
seven-tenths of a cycle comes from fetching non-cached instruction
|
||
bytes.
|
||
|
||
> 
|
||
> Whenever you see non-integral timing results of this sort, it's a good
|
||
> bet that the test code or data isn't cached.
|
||
|
||
### The Story Continues
|
||
|
||
There's certainly plenty more 486 lore to explore, including the 486's
|
||
unique prefetch queue, more optimization rules, branching optimizations,
|
||
performance implications of the cache, the cost of cache misses for
|
||
reads, and the implications of cache write-through for writes.
|
||
Nonetheless, we've covered quite a bit of ground in this chapter, and I
|
||
trust you've gotten a feel for the considerable extent to which 486
|
||
optimization differs from what you're used to. Odd as 486 optimization
|
||
is, though, it's well worth mastering, for the 486 is, at its best, so
|
||
staggeringly fast that carefully crafted 486 code can do more than twice
|
||
as much per cycle as the best 386 code—which makes it perhaps 50 times
|
||
as fast as optimized code for the original PC.
|
||
|
||
Sometimes it *is* hard to believe we're still in Kansas!
|