1104 lines
52 KiB
Markdown
1104 lines
52 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: '11'
|
||
pages: 205-231
|
||
---
|
||
|
||
## Chapter 11 -- Pushing the 286 and 386
|
||
|
||
### New Registers, New Instructions, New Timings, New Complications
|
||
|
||
This chapter, adapted from my earlier book *Zen of Assembly Language*
|
||
(1989; now out of print), provides an overview of the 286 and 386, often
|
||
contrasting those processors with the 8088. At the time I originally
|
||
wrote this, the 8088 was the king of processors, and the 286 and 386
|
||
were the new kids on the block. Today, of course, all three processors
|
||
are past their primes, but many millions of each are still in use, and
|
||
the 386 in particular is still well worth considering when optimizing
|
||
software.
|
||
|
||
This chapter provides an interesting look at the evolution of the x86
|
||
architecture, to a greater degree than you might expect, for the x86
|
||
family came into full maturity with the 386; the 486 and the Pentium are
|
||
really nothing more than faster 386s, with very little in the way of new
|
||
functionality. In contrast, the 286 added a number of instructions,
|
||
respectable performance, and protected mode to the 8088's capabilities,
|
||
and the 386 added more instructions and a whole new set of addressing
|
||
modes, and brought the x86 family into the 32-bit world that represents
|
||
the future (and, increasingly, the present) of personal computing. This
|
||
chapter also provides insight into the effects on optimization of the
|
||
variations in processors and memory architectures that are common in the
|
||
PC world. So, although the 286 and 386 no longer represent the
|
||
mainstream of computing, this chapter is a useful mix of history lesson,
|
||
x86 overview, and details on two workhorse processors that are still in
|
||
wide use.
|
||
|
||
#### Family Matters
|
||
|
||
While the x86 family is a large one, only a few members of the
|
||
family—which includes the 8088, 8086, 80188, 80186, 286, 386SX, 386DX,
|
||
numerous permutations of the 486, and now the Pentium—really matter.
|
||
|
||
The 8088 is now all but extinct in the PC arena. The 8086 was used
|
||
fairly widely for a while, but has now all but disappeared. The 80186
|
||
and 80188 never really caught on for use in PC and don't require further
|
||
discussion.
|
||
|
||
That leaves us with the high-end chips: the 286, the 386SX, the 386, the
|
||
486, and the Pentium. At this writing, the 386SX is fast going the way
|
||
of the 8088; people are realizing that its relatively small cost
|
||
advantage over the 386 isn't enough to offset its relatively large
|
||
performance disadvantage. After all, the 386SX suffers from the same
|
||
debilitating problem that looms over the 8088—a too-small bus.
|
||
Internally, the 386SX is a 32-bit processor, but externally, it's a
|
||
16-bit processor, a non-optimal architecture, especially for 32-bit
|
||
code.
|
||
|
||
I'm not going to discuss the 386SX in detail. If you do find yourself
|
||
programming for the 386SX, follow the same general rules you should
|
||
follow for the 8088: use short instructions, use the registers as
|
||
heavily as possible, and don't branch. In other words, avoid memory,
|
||
since the 386SX is by definition better at processing data internally
|
||
than it is at accessing memory.
|
||
|
||
The 486 is a world unto itself for the purposes of optimization, and the
|
||
Pentium is a *universe* unto itself. We'll treat them separately in
|
||
later chapters.
|
||
|
||
This leaves us with just two processors: the 286 and the 386. Each was
|
||
*the* PC standard in its day. The 286 is no longer used in new systems,
|
||
but there are millions of 286-based systems still in daily use. The 386
|
||
is still being used in new systems, although it's on the downhill leg of
|
||
its lifespan, and it is in even wider use than the 286. The future
|
||
clearly belongs to the 486 and Pentium, but the 286 and 386 are still
|
||
very much a part of the present-day landscape.
|
||
|
||
#### Crossing the Gulf to the 286 and the 386
|
||
|
||
Apart from vastly improved performance, the biggest difference between
|
||
the 8088 and the 286 and 386 (as well as the later Intel CPUs) is that
|
||
the 286 introduced protected mode, and the 386 greatly expanded the
|
||
capabilities of protected mode. We're only going to talk about real-mode
|
||
operation of the 286 and 386 in this book, however. Protected mode
|
||
offers a whole new memory management scheme, one that isn't supported by
|
||
the 8088. Only code specifically written for protected mode can run in
|
||
that mode; it's an alien and hostile environment for MS-DOS programs.
|
||
|
||
In particular, segments are different creatures in protected mode.
|
||
They're *selectors*—indexes into a table of segment descriptors—rather
|
||
than plain old registers, and can't be set to arbitrary values. That
|
||
means that segments can't be used for temporary storage or as part of a
|
||
fast indivisible 32-bit load from memory, as in
|
||
|
||
```nasm
|
||
les ax,dword ptr [LongVar]
|
||
mov dx,es
|
||
```
|
||
|
||
which loads `LongVar` into DX:AX faster than this:
|
||
|
||
```nasm
|
||
mov ax,word ptr [LongVar]
|
||
mov dx,word ptr [LongVar+2]
|
||
```
|
||
|
||
Protected mode uses those altered segment registers to offer access to a
|
||
great deal more memory than real mode: The 286 supports 16 megabytes of
|
||
memory, while the 386 supports 4 gigabytes (4K megabytes) of physical
|
||
memory and 64 *terabytes* (64K gigabytes!) of virtual memory.
|
||
|
||
In protected mode, your programs generally run under an operating system
|
||
(OS/2, Unix, Windows NT or the like) that exerts much more control over
|
||
the computer than does MS-DOS. Protected mode operating systems can
|
||
generally run multiple programs simultaneously, and the performance of
|
||
any one program may depend far less on code quality than on how
|
||
efficiently the program uses operating system services and how often and
|
||
under what circumstances the operating system preempts the program.
|
||
Protected mode programs are often mostly collections of operating system
|
||
calls, and the performance of whatever code *isn't* operating-system
|
||
oriented may depend primarily on how large a time slice the operating
|
||
system gives that code to run in.
|
||
|
||
In short, taken as a whole, protected mode programming is a different
|
||
kettle of fish altogether from what I've been describing in this book.
|
||
There's certainly a knack to optimizing specifically for protected mode
|
||
under a given operating system...but it's not what we've been learning,
|
||
and now is not the time to pursue it further. In general, though, the
|
||
optimization strategies discussed in this book still hold true in
|
||
protected mode; it's just issues specific to protected mode or a
|
||
particular operating system that we won't discuss.
|
||
|
||
#### In the Lair of the Cycle-Eaters, Part II
|
||
|
||
Under the programming interface, the 286 and 386 differ considerably
|
||
from the 8088. Nonetheless, with one exception and one addition, the
|
||
cycle-eaters remain much the same on computers built around the 286 and
|
||
386. Next, we'll review each of the familiar cycle-eaters I covered in
|
||
Chapter 4 as they apply to the 286 and 386, and we'll look at the new
|
||
member of the gang, the data alignment cycle-eater.
|
||
|
||
The one cycle-eater that vanishes on the 286 and 386 is the 8-bit bus
|
||
cycle-eater. The 286 is a 16-bit processor both internally and
|
||
externally, and the 386 is a 32-bit processor both internally and
|
||
externally, so the Execution Unit/Bus Interface Unit size mismatch that
|
||
plagues the 8088 is eliminated. Consequently, there's no longer any need
|
||
to use byte-sized memory variables in preference to word-sized
|
||
variables, at least so long as word-sized variables start at even
|
||
addresses, as we'll see shortly. On the other hand, access to byte-sized
|
||
variables still isn't any *slower* than access to word-sized variables,
|
||
so you can use whichever size suits a given task best.
|
||
|
||
You might think that the elimination of the 8-bit bus cycle-eater would
|
||
mean that the prefetch queue cycle-eater would also vanish, since on the
|
||
8088 the prefetch queue cycle-eater is a side effect of the 8-bit bus.
|
||
That would seem all the more likely given that both the 286 and the 386
|
||
have larger prefetch queues than the 8088 (6 bytes for the 286, 16 bytes
|
||
for the 386) and can perform memory accesses, including instruction
|
||
fetches, in far fewer cycles than the 8088.
|
||
|
||
However, the prefetch queue cycle-eater *doesn't* vanish on either the
|
||
286 or the 386, for several reasons. For one thing, branching
|
||
instructions still empty the prefetch queue, so instruction fetching
|
||
still slows things down after most branches; when the prefetch queue is
|
||
empty, it doesn't much matter how big it is. (Even apart from emptying
|
||
the prefetch queue, branches aren't particularly fast on the 286 or the
|
||
386, at a minimum of seven-plus cycles apiece. Avoid branching whenever
|
||
possible.)
|
||
|
||
After a branch it *does* matter how fast the queue can refill, and there
|
||
we come to the second reason the prefetch queue cycle-eater lives on:
|
||
The 286 and 386 are so fast that sometimes the Execution Unit can
|
||
execute instructions faster than they can be fetched, even though
|
||
instruction fetching is *much* faster on the 286 and 386 than on the
|
||
8088.
|
||
|
||
(All other things being equal, too-slow instruction fetching is more of
|
||
a problem on the 286 than on the 386, since the 386 fetches 4
|
||
instruction bytes at a time versus the 2 instruction bytes fetched per
|
||
memory access by the 286. However, the 386 also typically runs at least
|
||
twice as fast as the 286, meaning that the 386 can easily execute
|
||
instructions faster than they can be fetched unless very high-speed
|
||
memory is used.)
|
||
|
||
The most significant reason that the prefetch queue cycle-eater not only
|
||
survives but prospers on the 286 and 386, however, lies in the various
|
||
memory architectures used in computers built around the 286 and 386. Due
|
||
to the memory architectures, the 8-bit bus cycle-eater is replaced by a
|
||
new form of the wait state cycle-eater: wait states on accesses to
|
||
normal system memory.
|
||
|
||
#### System Wait States
|
||
|
||
The 286 and 386 were designed to lose relatively little performance to
|
||
the prefetch queue cycle-eater...*when used with zero-wait-state
|
||
memory:* memory that can complete memory accesses so rapidly that no
|
||
wait states are needed. However, true zero-wait-state memory is almost
|
||
never used with those processors. Why? Because memory that can keep up
|
||
with a 286 is fairly expensive, and memory that can keep up with a 386
|
||
is *very* expensive. Instead, computer designers use alternative memory
|
||
architectures that offer more performance for the dollar—but less
|
||
performance overall—than zero-wait-state memory. (It *is* possible to
|
||
build zero-wait-state systems for the 286 and 386; it's just so
|
||
expensive that it's rarely done.)
|
||
|
||
The IBM AT and true compatibles use one-wait-state memory (some AT
|
||
clones use zero-wait-state memory, but such clones are less common than
|
||
one-wait-state AT clones). The 386 systems use a wide variety of memory
|
||
systems—including high-speed caches, interleaved memory, and
|
||
static-column RAM—that insert anywhere from 0 to about 5 wait states
|
||
(and many more if 8 or 16-bit memory expansion cards are used); the
|
||
exact number of wait states inserted at any given time depends on the
|
||
interaction between the code being executed and the memory system it's
|
||
running on.
|
||
|
||
> 
|
||
> The performance of most 386 memory systems can vary greatly from one
|
||
> memory access to another, depending on factors such as what data happens
|
||
> to be in the cache and which interleaved bank and/or RAM column was
|
||
> accessed last.
|
||
|
||
The many memory systems in use make it impossible for us to optimize for
|
||
286/386 computers with the precision that's possible on the 8088.
|
||
Instead, we must write code that runs reasonably well under the varying
|
||
conditions found in the 286/386 arena.
|
||
|
||
The wait states that occur on most accesses to system memory in 286 and
|
||
386 computers mean that nearly every access to system memory—memory in
|
||
the DOS's normal 640K memory area—is slowed down. (Accesses in computers
|
||
with high-speed caches may be wait-state-free if the desired data is
|
||
already in the cache, but will certainly encounter wait states if the
|
||
data isn't cached; this phenomenon produces highly variable instruction
|
||
execution times.) While this is our first encounter with system memory
|
||
wait states, we have run into a wait-state cycle-eater before: the
|
||
display adapter cycle-eater, which we discussed along with the other
|
||
8088 cycle-eaters way back in Chapter 4. System memory generally has
|
||
fewer wait states per access than display memory. However, system memory
|
||
is also accessed far more often than display memory, so system memory
|
||
wait states hurt plenty—and the place they hurt most is instruction
|
||
fetching.
|
||
|
||
Consider this: The 286 can store an immediate value to memory, as in
|
||
`MOV [WordVar],0`, in just 3 cycles. However, that instruction is 6
|
||
bytes long. The 286 is capable of fetching 1 word every 2 cycles;
|
||
however, the one-wait-state architecture of the AT stretches that to 3
|
||
cycles. Consequently, nine cycles are needed to fetch the six
|
||
instruction bytes. On top of that, 3 cycles are needed to write to
|
||
memory, bringing the total memory access time to 12 cycles. On balance,
|
||
memory access time—especially instruction prefetching—greatly exceeds
|
||
execution time, to the extent that this particular instruction can take
|
||
up to four times as long to run as it does to execute in the Execution
|
||
Unit.
|
||
|
||
And that, my friend, is unmistakably the prefetch queue cycle-eater. I
|
||
might add that the prefetch queue cycle-eater is in rare good form in
|
||
the above example: A 4-to-1 ratio of instruction fetch time to execution
|
||
time is in a class with the best (or worst!) that's found on the 8088.
|
||
|
||
Let's check out the prefetch queue cycle-eater in action. Listing 11.1
|
||
times `MOV [WordVar],0`. The Zen timer reports that on a
|
||
one-wait-state 10 MHz 286-based AT clone (the computer used for all
|
||
tests in this chapter), Listing 11.1 runs in 1.27 µs per instruction.
|
||
That's 12.7 cycles per instruction, just as we calculated. (That extra
|
||
seven-tenths of a cycle comes from DRAM refresh, which we'll get to
|
||
shortly.)
|
||
|
||
**LISTING 11.1 L11-1.ASM**
|
||
|
||
```nasm
|
||
;
|
||
; *** Listing 11.1 ***
|
||
;
|
||
; Measures the performance of an immediate move to
|
||
; memory, in order to demonstrate that the prefetch
|
||
; queue cycle-eater is alive and well on the AT.
|
||
;
|
||
jmp Skip
|
||
;
|
||
even ;always make sure word-sized memory
|
||
; variables are word-aligned!
|
||
WordVar dw 0
|
||
;
|
||
Skip:
|
||
call ZTimerOn
|
||
rept 1000
|
||
mov [WordVar],0
|
||
endm
|
||
call ZTimerOff
|
||
```
|
||
|
||
What does this mean? It means that, practically speaking, the 286 as
|
||
used in the AT doesn't have a 16-bit bus. From a performance
|
||
perspective, the 286 in an AT has two-thirds of a 16-bit bus (a 10.7-bit
|
||
bus?), since every bus access on an AT takes 50 percent longer than it
|
||
should. A 286 running at 10 MHz *should* be able to access memory at a
|
||
maximum rate of 1 word every 200 ns; in a 10 MHz AT, however, that rate
|
||
is reduced to 1 word every 300 ns by the one-wait-state memory.
|
||
|
||
In short, a close relative of our old friend the 8-bit bus
|
||
cycle-eater—the system memory wait state cycle-eater—haunts us still on
|
||
all but zero-wait-state 286 and 386 computers, and that means that the
|
||
prefetch queue cycle-eater is alive and well. (The system memory wait
|
||
state cycle-eater isn't really a new cycle-eater, but rather a variant
|
||
of the general wait state cycle-eater, of which the display adapter
|
||
cycle-eater is yet another variant.) While the 286 in the AT can fetch
|
||
instructions much faster than can the 8088 in the PC, it can execute
|
||
those instructions faster still.
|
||
|
||
The picture is less clear in the 386 world since there are so many
|
||
different memory architectures, but similar problems can occur in any
|
||
computer built around a 286 or 386. The prefetch queue cycle-eater is
|
||
even a factor—albeit a lesser one—on zero-wait-state machines, both
|
||
because branching empties the queue and because some instructions can
|
||
outrun even zero—5 cycles longer than the official execution time.)
|
||
|
||
To summarize:
|
||
|
||
* Memory-accessing instructions don't run at their official speeds on
|
||
non-zero-wait-state 286/386 computers.
|
||
* The prefetch queue cycle-eater reduces performance on 286/386
|
||
computers, particularly when non-zero-wait-state memory is used.
|
||
* Branches often execute at less than their rated speeds on the 286
|
||
and 386 since the prefetch queue is emptied.
|
||
* The extent to which the prefetch queue and wait states affect
|
||
performance varies from one 286/386 computer to another, making
|
||
precise optimization impossible.
|
||
|
||
What's to be learned from all this? Several things:
|
||
|
||
* Keep your instructions short.
|
||
* Keep it in the registers; avoid memory, since memory generally can't
|
||
keep up with the processor.
|
||
* Don't jump.
|
||
|
||
Of course, those are exactly the rules that apply to 8088 optimization
|
||
as well. Isn't it convenient that the same general rules apply across
|
||
the board?
|
||
|
||
#### Data Alignment
|
||
|
||
Thanks to its 16-bit bus, the 286 can access word-sized memory variables
|
||
just as fast as byte-sized variables. There's a catch, however: That's
|
||
only true for word-sized variables that start at even addresses. When
|
||
the 286 is asked to perform a word-sized access starting at an odd
|
||
address, it actually performs two separate accesses, each of which
|
||
fetches 1 byte, just as the 8088 does for all word-sized accesses.
|
||
|
||
Figure 11.1 illustrates this phenomenon. The conversion of word-sized
|
||
accesses to odd addresses into double byte-sized accesses is transparent
|
||
to memory-accessing instructions; all any instruction knows is that the
|
||
requested word has been accessed, no matter whether 1 word-sized access
|
||
or 2 byte-sized accesses were required to accomplish it.
|
||
|
||
The penalty for performing a word-sized access starting at an odd
|
||
address is easy to calculate: Two accesses take twice as long as one
|
||
access.
|
||
|
||
> 
|
||
> In other words, the effective capacity of the 286's external data bus
|
||
> is *halved* when a word-sized access to an odd address is performed.
|
||
|
||
That, in a nutshell, is the data alignment cycle-eater, the one new
|
||
cycle-eater of the 286 and 386. (The data alignment cycle-eater is a
|
||
close relative of the 8088's 8-bit bus cycle-eater, but since it behaves
|
||
differently—occurring only at odd addresses—and is avoided with a
|
||
different workaround, we'll consider it to be a new cycle-eater.)
|
||
|
||

|
||
|
||
The way to deal with the data alignment cycle-eater is straightforward:
|
||
*Don't perform word-sized accesses to odd addresses on the 286 if you
|
||
can help it*. The easiest way to avoid the data alignment cycle-eater is
|
||
to place the directive `EVEN` before each of your word-sized
|
||
variables. `EVEN` forces the offset of the next byte assembled to be
|
||
even by inserting a `NOP` if the current offset is odd; consequently,
|
||
you can ensure that any word-sized variable can be accessed efficiently
|
||
by the 286 simply by preceding it with `EVEN`.
|
||
|
||
Listing 11.2, which accesses memory a word at a time with each word
|
||
starting at an odd address, runs on a 10 MHz AT clone in 1.27 ms per
|
||
repetition of `MOVSW`, or 0.64 ms per word-sized memory access. That's
|
||
6-plus cycles per word-sized access, which breaks down to two separate
|
||
memory accesses—3 cycles to access the high byte of each word and 3
|
||
cycles to access the low byte of each word, the inevitable result of
|
||
non-word-aligned word-sized memory accesses—plus a bit extra for DRAM
|
||
refresh.
|
||
|
||
**LISTING 11.2 L11-2.ASM**
|
||
|
||
```nasm
|
||
;
|
||
; *** Listing 11.2 ***
|
||
;
|
||
; Measures the performance of accesses to word-sized
|
||
; variables that start at odd addresses (are not
|
||
; word-aligned).
|
||
;
|
||
Skip:
|
||
push ds
|
||
pop es
|
||
mov si,1 ;source and destination are the same
|
||
mov di,si ; and both are not word-aligned
|
||
mov cx,1000 ;move 1000 words
|
||
cld
|
||
call ZTimerOn
|
||
rep movsw
|
||
call ZTimerOff
|
||
```
|
||
|
||
On the other hand, Listing 11.3, which is exactly the same as Listing
|
||
11.2 save that the memory accesses are word-aligned (start at even
|
||
addresses), runs in 0.64 ms per repetition of `MOVSW`, or 0.32 µs per
|
||
word-sized memory access. That's 3 cycles per word-sized access—exactly
|
||
twice as fast as the non-word-aligned accesses of Listing 11.2, just as
|
||
we predicted.
|
||
|
||
**LISTING 11.3 L11-3.ASM**
|
||
|
||
```nasm
|
||
;
|
||
; *** Listing 11.3 ***
|
||
;
|
||
; Measures the performance of accesses to word-sized
|
||
; variables that start at even addresses (are word-aligned).
|
||
;
|
||
Skip:
|
||
push ds
|
||
pop es
|
||
sub si,si ;source and destination are the same
|
||
mov di,si ; and both are word-aligned
|
||
mov cx,1000 ;move 1000 words
|
||
cld
|
||
call ZTimerOn
|
||
rep movsw
|
||
call ZTimerOff
|
||
```
|
||
|
||
The data alignment cycle-eater has intriguing implications for speeding
|
||
up 286/386 code. The expenditure of a little care and a few bytes to
|
||
make sure that word-sized variables and memory blocks are word-aligned
|
||
can literally double the performance of certain code running on the 286.
|
||
Even if it doesn't double performance, word alignment usually helps and
|
||
never hurts.
|
||
|
||
#### Code Alignment
|
||
|
||
Lack of word alignment can also interfere with instruction fetching on
|
||
the 286, although not to the extent that it interferes with access to
|
||
word-sized memory variables. The 286 prefetches instructions a word at a
|
||
time; even if a given instruction doesn't begin at an even address, the
|
||
286 simply fetches the first byte of that instruction at the same time
|
||
that it fetches the last byte of the previous instruction, as shown in
|
||
Figure 11.2, then separates the bytes internally. That means that in
|
||
most cases, instructions run just as fast whether they're word-aligned
|
||
or not.
|
||
|
||
There is, however, a non-word-alignment penalty on *branches* to odd
|
||
addresses. On a branch to an odd address, the 286 is only able to fetch
|
||
1 useful byte with the first instruction fetch following the branch, as
|
||
shown in Figure 11.3. In other words, lack of word alignment of the
|
||
target instruction for any branch effectively cuts the
|
||
instruction-fetching power of the 286 in half for the first instruction
|
||
fetch after that branch. While that may not sound like much, you'd be
|
||
surprised at what it can do to tight loops; in fact, a brief story is in
|
||
order.
|
||
|
||
When I was developing the Zen timer, I used my trusty 10 MHz 286-based
|
||
AT clone to verify the basic functionality of the timer by measuring the
|
||
performance of simple instruction sequences. I was cruising along with
|
||
no problems until I timed the following code:
|
||
|
||
```nasm
|
||
mov cx,1000
|
||
call ZTimerOn
|
||
LoopTop:
|
||
loop LoopTop
|
||
call ZTimerOff
|
||
```
|
||
|
||

|
||
|
||

|
||
|
||
Now, this code *should* run in, say, about 12 cycles per loop at most.
|
||
Instead, it took over 14 cycles per loop, an execution time that I could
|
||
not explain in any way. After rolling it around in my head for a while,
|
||
I took a look at the code under a debugger...and the answer leaped out
|
||
at me. *The loop began at an odd address!* That meant that two
|
||
instruction fetches were required each time through the loop; one to get
|
||
the opcode byte of the `LOOP` instruction, which resided at the end of
|
||
one word-aligned word, and another to get the displacement byte, which
|
||
resided at the start of the next word-aligned word.
|
||
|
||
One simple change brought the execution time down to a reasonable 12.5
|
||
cycles per loop:
|
||
|
||
```nasm
|
||
mov cx,1000
|
||
call ZTimerOn
|
||
even
|
||
LoopTop:
|
||
loop LoopTop
|
||
call ZTimerOff
|
||
```
|
||
|
||
While word-aligning branch destinations can improve branching
|
||
performance, it's a nuisance and can increase code size a good deal, so
|
||
it's not worth doing in most code. Besides, `EVEN` inserts a `NOP`
|
||
instruction if necessary, and the time required to execute a `NOP` can
|
||
sometimes cancel the performance advantage of having a word-aligned
|
||
branch destination.
|
||
|
||
> 
|
||
> Consequently, it's best to word-align only those branch destinations
|
||
> that can be reached solely by branching.
|
||
|
||
I recommend that you only go out of your way to word-align the start
|
||
offsets of your subroutines, as in:
|
||
|
||
```nasm
|
||
even
|
||
FindChar proc near
|
||
:
|
||
```
|
||
|
||
In my experience, this simple practice is the one form of code alignment
|
||
that consistently provides a reasonable return for bytes and effort
|
||
expended, although sometimes it also pays to word-align tight
|
||
time-critical loops.
|
||
|
||
#### Alignment and the 386
|
||
|
||
So far we've only discussed alignment as it pertains to the 286. What,
|
||
you may well ask, of the 386?
|
||
|
||
The 386 adds the issue of *doubleword* alignment (that is, alignment to
|
||
addresses that are multiples of four.) The rule for the 386 is:
|
||
Word-sized memory accesses should be word-aligned (it's impossible for
|
||
word-aligned word-sized accesses to cross doubleword boundaries), and
|
||
doubleword-sized memory accesses should be doubleword-aligned. However,
|
||
in real (as opposed to 32-bit protected) mode, doubleword-sized memory
|
||
accesses are rare, so the simple word-alignment rule we've developed for
|
||
the 286 serves for the 386 in real mode as well.
|
||
|
||
As for code alignment...the subroutine-start word-alignment rule of the
|
||
286 serves reasonably well there too since it avoids the worst case,
|
||
where just 1 byte is fetched on entry to a subroutine. While optimum
|
||
performance would dictate doubleword alignment of subroutines, that
|
||
takes 3 bytes, a high price to pay for an optimization that improves
|
||
performance *only* on the post 286 processors.
|
||
|
||
#### Alignment and the Stack
|
||
|
||
One side-effect of the data alignment cycle-eater of the 286 and 386 is
|
||
that you should *never* allow the stack pointer to become odd. (You can
|
||
make the stack pointer odd by adding an odd value to it or subtracting
|
||
an odd value from it, or by loading it with an odd value.) An odd stack
|
||
pointer on the 286 or 386 (or a non-doubleword-aligned stack in 32-bit
|
||
protected mode on the 386, 486, or Pentium) will significantly reduce
|
||
the performance of `PUSH`, `POP`, `CALL`, and `RET`, as well as
|
||
`INT` and `IRET`, which are executed to invoke DOS and BIOS
|
||
functions, handle keystrokes and incoming serial characters, and manage
|
||
the mouse. I know of a Forth programmer who vastly improved the
|
||
performance of a complex application on the AT simply by forcing the
|
||
Forth interpreter to maintain an even stack pointer at all times.
|
||
|
||
An interesting corollary to this rule is that you shouldn't `INC SP`
|
||
twice to add 2, even though that takes fewer bytes than `ADD SP,2`.
|
||
The stack pointer is odd between the first and second `INC`, so any
|
||
interrupt occurring between the two instructions will be serviced more
|
||
slowly than it normally would. The same goes for decrementing twice; use
|
||
`SUB SP,2` instead.
|
||
|
||
> 
|
||
> Keep the stack pointer aligned at all times.
|
||
|
||
#### The DRAM Refresh Cycle-Eater: Still an Act of God
|
||
|
||
The DRAM refresh cycle-eater is the cycle-eater that's least changed
|
||
from its 8088 form on the 286 and 386. In the AT, DRAM refresh uses a
|
||
little over five percent of all available memory accesses, slightly less
|
||
than it uses in the PC, but in the same ballpark. While the DRAM refresh
|
||
penalty varies somewhat on various AT clones and 386 computers (in fact,
|
||
a few computers are built around static RAM, which requires no refresh
|
||
at all; likewise, caches are made of static RAM so cached systems
|
||
generally suffer less from DRAM refresh), the 5 percent figure is a good
|
||
rule of thumb.
|
||
|
||
Basically, the effect of the DRAM refresh cycle-eater is pretty much the
|
||
same throughout the PC-compatible world: fairly small, so it doesn't
|
||
greatly affect performance; unavoidable, so there's no point in worrying
|
||
about it anyway; and a nuisance since it results in fractional cycle
|
||
counts when using the Zen timer. Just as with the PC, a given code
|
||
sequence on the AT can execute at varying speeds at different times as a
|
||
result of the interaction between the code and DRAM refresh.
|
||
|
||
There's nothing much new with DRAM refresh on 286/386 computers, then.
|
||
Be aware of it, but don't overly concern yourself—DRAM refresh is still
|
||
an act of God, and there's not a blessed thing you can do about it.
|
||
Happily, the internal caches of the 486 and Pentium make DRAM refresh
|
||
largely a performance non-issue on those processors.
|
||
|
||
#### The Display Adapter Cycle-Eater
|
||
|
||
Finally we come to the last of the cycle-eaters, the display adapter
|
||
cycle-eater. There are two ways of looking at this cycle-eater on
|
||
286/386 computers: (1) It's much worse than it was on the PC, or (2)
|
||
it's just about the same as it was on the PC.
|
||
|
||
Either way, the display adapter cycle-eater is extremely bad news on
|
||
286/386 computers and on 486s and Pentiums as well. In fact, this
|
||
cycle-eater on those systems is largely responsible for the popularity
|
||
of VESA local bus (VLB).
|
||
|
||
The two ways of looking at the display adapter cycle-eater on 286/386
|
||
computers are actually the same. As you'll recall from my earlier
|
||
discussion of the matter in Chapter 4, display adapters offer only a
|
||
limited number of accesses to display memory during any given period of
|
||
time. The 8088 is capable of making use of most but not all of those
|
||
slots with `REP MOVSW`, so the number of memory accesses allowed by a
|
||
display adapter such as a standard VGA is reasonably well-matched to an
|
||
8088's memory access speed. Granted, access to a VGA slows the 8088 down
|
||
considerably—but, as we're about to find out, "considerably" is a
|
||
relative term. What a VGA does to PC performance is nothing compared to
|
||
what it does to faster computers.
|
||
|
||
Under ideal conditions, a 286 can access memory much, much faster than
|
||
an 8088. A 10 MHz 286 is capable of accessing a word of system memory
|
||
every 0.20 ms with `REP MOVSW`, dwarfing the 1 byte every 1.31 µs that
|
||
the 8088 in a PC can manage. However, access to display memory is
|
||
anything but ideal for a 286. For one thing, most display adapters are
|
||
8-bit devices, although newer adapters are 16-bit in nature. One
|
||
consequence of that is that only 1 byte can be read or written per
|
||
access to display memory; word-sized accesses to 8-bit devices are
|
||
automatically split into 2 separate byte-sized accesses by the AT's bus.
|
||
Another consequence is that accesses are simply slower; the AT's bus
|
||
inserts additional wait states on accesses to 8-bit devices since it
|
||
must assume that such devices were designed for PCs and may not run
|
||
reliably at AT speeds.
|
||
|
||
However, the 8-bit size of most display adapters is but one of the two
|
||
factors that reduce the speed with which the 286 can access display
|
||
memory. Far more cycles are eaten by the inherent memory-access
|
||
limitations of display adapters—that is, the limited number of display
|
||
memory accesses that display adapters make available to the 286. Look at
|
||
it this way: If `REP MOVSW` on a PC can use more than half of all
|
||
available accesses to display memory, then how much faster can code
|
||
running on a 286 or 386 possibly run when accessing display memory?
|
||
|
||
That's right—less than twice as fast.
|
||
|
||
In other words, instructions that access display memory won't run a
|
||
whole lot faster on ATs and faster computers than they do on PCs. That
|
||
explains one of the two viewpoints expressed at the beginning of this
|
||
section: The display adapter cycle-eater is just about the same on
|
||
high-end computers as it is on the PC, in the sense that it allows
|
||
instructions that access display memory to run at just about the same
|
||
speed on all computers.
|
||
|
||
Of course, the picture is quite a bit different when you compare the
|
||
performance of instructions that access display memory to the *maximum*
|
||
performance of those instructions. Instructions that access display
|
||
memory receive many more wait states when running on a 286 than they do
|
||
on an 8088. Why? While the 286 is capable of accessing memory much more
|
||
often than the 8088, we've seen that the frequency of access to display
|
||
memory is determined not by processor speed but by the display adapter
|
||
itself. As a result, both processors are actually allowed just about the
|
||
same maximum number of accesses to display memory in any given time. By
|
||
definition, then, the 286 must spend many more cycles waiting than does
|
||
the 8088.
|
||
|
||
And that explains the second viewpoint expressed above regarding the
|
||
display adapter cycle-eater vis-a-vis the 286 and 386. The display
|
||
adapter cycle-eater, as measured in cycles lost to wait states, is
|
||
indeed much worse on AT-class computers than it is on the PC, and it's
|
||
worse still on more powerful computers.
|
||
|
||
> 
|
||
> How bad is the display adapter cycle-eater on an AT? It's this bad:
|
||
> Based on my (not inconsiderable) experience in timing display adapter
|
||
> access, I've found that the display adapter cycle-eater can slow an
|
||
> AT—or even a 386 computer—to near-PC speeds when display memory is
|
||
> accessed.
|
||
|
||
I know that's hard to believe, but the display adapter cycle-eater gives
|
||
out just so many display memory accesses in a given time, and no more,
|
||
no matter how fast the processor is. In fact, the faster the processor,
|
||
the more the display adapter cycle-eater hurts the performance of
|
||
instructions that access display memory. The display adapter cycle-eater
|
||
is not only still present in 286/386 computers, it's worse than ever.
|
||
|
||
What can we do about this new, more virulent form of the display adapter
|
||
cycle-eater? The workaround is the same as it was on the PC: Access
|
||
display memory as little as you possibly can.
|
||
|
||
#### New Instructions and Features: The 286
|
||
|
||
The 286 and 386 offer a number of new instructions. The 286 has a
|
||
relatively small number of instructions that the 8088 lacks, while the
|
||
386 has those instructions and quite a few more, along with new
|
||
addressing modes and data sizes. We'll discuss the 286 and the 386
|
||
separately in this regard.
|
||
|
||
The 286 has a number of instructions designed for protected-mode
|
||
operations. As I've said, we're not going to discuss protected mode in
|
||
this book; in any case, protected-mode instructions are generally used
|
||
only by operating systems. (I should mention that the 286's protected
|
||
mode brings with it the ability to address 16 MB of memory, a
|
||
considerable improvement over the 8088's 1 MB. In real mode, however,
|
||
programs are still limited to 1 MB of addressable memory on the 286. In
|
||
either mode, each segment is still limited to 64K.)
|
||
|
||
There are also a handful of 286-specific real-mode instructions, and
|
||
they can be quite useful. `BOUND` checks array bounds. `ENTER` and
|
||
`LEAVE` support compact and speedy stack frame construction and
|
||
removal, ideal for interfacing to high-level languages such as C and
|
||
Pascal (although these instructions are actually relatively slow on the
|
||
386 and its successors, and should be used with caution when performance
|
||
matters). `INS` and `OUTS` are new string instructions that support
|
||
efficient data transfer between memory and I/O ports. Finally, `PUSHA`
|
||
and `POPA` push and pop all eight general-purpose registers.
|
||
|
||
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.
|
||
For another, the 286 allows all shifts and rotates to be performed for
|
||
not just 1 bit or the number of bits specified by CL, but for *any*
|
||
constant number of bits.
|
||
|
||
#### New Instructions and Features: The 386
|
||
|
||
The 386 is somewhat more complex than the 286 regarding new features.
|
||
Once again, we won't discuss protected mode, which on the 386 comes with
|
||
the ability to address up to 4 gigabytes per segment and 64 terabytes in
|
||
all. In real mode (and in virtual-86 mode, which allows the 386 to
|
||
multitask MS-DOS applications, and which is identical to real mode so
|
||
far as MS-DOS programs are concerned), programs running on the 386 are
|
||
still limited to 1 MB of addressable memory and 64K per segment.
|
||
|
||
The 386 has many new instructions, as well as new registers, addressing
|
||
modes and data sizes that have trickled down from protected mode. Let's
|
||
take a quick look at these new real-mode features.
|
||
|
||
Even in real mode, it's possible to access many of the 386's new and
|
||
extended registers. Most of these registers are simply 32-bit extensions
|
||
of the 16-bit registers of the 8088. For example, EAX is a 32-bit
|
||
register containing AX as its lower 16 bits, EBX is a 32-bit register
|
||
containing BX as its lower 16 bits, and so on. There are also two new
|
||
segment registers: FS and GS.
|
||
|
||
The 386 also comes with a slew of new real-mode instructions beyond
|
||
those supported by the 8088 and 286. These instructions can scan data on
|
||
a bit-by-bit basis, set the Carry flag to the value of a specified bit,
|
||
sign-extend or zero-extend data as it's moved, set a register or memory
|
||
variable to 1 or 0 on the basis of any of the conditions that can be
|
||
tested with conditional jumps, and more. (Again, beware: Many of these
|
||
complex 386-specific instructions are slower than equivalent sequences
|
||
of simple instructions on the 486 and especially on the Pentium.) What's
|
||
more, both old and new instructions support 32-bit operations on the
|
||
386. For example, it's relatively simple to copy data in chunks of 4
|
||
bytes on a 386, even in real mode, by using the `MOVSD` ("move string
|
||
double") instruction, or to negate a 32-bit value with `NEG eax`.
|
||
|
||
Finally, it's possible in real mode to use the 386's new addressing
|
||
modes, in which *any* 32-bit general-purpose register or pair of
|
||
registers can be used to address memory. What's more, multiplication of
|
||
memory-addressing registers by 2, 4, or 8 for look-ups in word,
|
||
doubleword, or quadword tables can be built right into the memory
|
||
addressing mode. (The 32-bit addressing modes are discussed further in
|
||
later chapters.) In protected mode, these new addressing modes allow you
|
||
to address a full 4 gigabytes per segment, but in real mode you're still
|
||
limited to 64K, even with 32-bit registers and the new addressing modes,
|
||
unless you play some unorthodox tricks with the segment registers.
|
||
|
||
> 
|
||
> Note well: Those tricks don't necessarily work with system software such
|
||
> as Windows, so I'd recommend against using them. If you want 4-gigabyte
|
||
> segments, use a 32-bit environment such as Win32.
|
||
|
||
#### Optimization Rules: The More Things Change...
|
||
|
||
Let's see what we've learned about 286/386 optimization. Mostly what
|
||
we've learned is that our familiar PC cycle-eaters still apply, although
|
||
in somewhat different forms, and that the major optimization rules for
|
||
the PC hold true on ATs and 386-based computers. You won't go wrong on
|
||
any of these computers if you keep your instructions short, use the
|
||
registers heavily and avoid memory, don't branch, and avoid accessing
|
||
display memory like the plague.
|
||
|
||
Although we haven't touched on them, repeated string instructions are
|
||
still desirable on the 286 and 386 since they provide a great deal of
|
||
functionality per instruction byte and eliminate both the prefetch queue
|
||
cycle-eater and branching. However, string instructions are not quite so
|
||
spectacularly superior on the 286 and 386 as they are on the 8088 since
|
||
non-string memory-accessing instructions have been speeded up
|
||
considerably on the newer processors.
|
||
|
||
There's one cycle-eater with new implications on the 286 and 386, and
|
||
that's the data alignment cycle-eater. From the data alignment
|
||
cycle-eater we get a new rule: Word-align your word-sized variables, and
|
||
start your subroutines at even addresses.
|
||
|
||
#### Detailed Optimization
|
||
|
||
While the major 8088 optimization rules hold true on computers built
|
||
around the 286 and 386, many of the instruction-specific optimizations
|
||
no longer hold, for the execution times of most instructions are quite
|
||
different on the 286 and 386 than on the 8088. We have already seen one
|
||
such example of the sometimes vast difference between 8088 and 286/386
|
||
instruction execution times: `MOV [WordVar],0`, which has an Execution
|
||
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.
|
||
|
||
In fact, the performance of virtually all memory-accessing instructions
|
||
has been improved enormously on the 286 and 386. The key to this
|
||
improvement is the near elimination of effective address (EA)
|
||
calculation time. Where an 8088 takes from 5 to 12 cycles to calculate
|
||
an EA, a 286 or 386 usually takes no time whatsoever to perform the
|
||
calculation. If a base+index+displacement addressing mode, such as `MOV
|
||
AX,[WordArray+bx+si]`, is used on a 286 or 386, 1 cycle is taken to
|
||
perform the EA calculation, but that's both the worst case and the only
|
||
case in which there's any EA overhead at all.
|
||
|
||
The elimination of EA calculation time means that the EU execution time
|
||
of memory-addressing instructions is much closer to the EU execution
|
||
time of register-only instructions. For instance, on the 8088 `ADD
|
||
[WordVar],100H` is a 31-cycle instruction, while `ADD DX,100H` is a
|
||
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`
|
||
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
|
||
the 286 than it was on the 8088, but that's simply not the case, for
|
||
reasons we've already seen. The key is this: The 286 can execute
|
||
memory-addressing instructions so fast that there's no spare instruction
|
||
prefetching time during those instructions, so the prefetch queue runs
|
||
dry, especially on the AT, with its one-wait-state memory. On the AT,
|
||
the 6-byte instruction `ADD [WordVar],100H` is effectively at least a
|
||
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`
|
||
and write the result back to memory.
|
||
|
||
Granted, the register-only instruction `ADD DX,100H` also slows
|
||
down—to 6 cycles—because of instruction prefetching, leaving a ratio of
|
||
2.5 to 1. Now, however, let's look at the performance of the same code
|
||
on an 8088. The register-only code would run in 16 cycles (4 instruction
|
||
bytes at 4 cycles per byte), while the memory-accessing code would run
|
||
in 40 cycles (6 instruction bytes at 4 cycles per byte, plus 2
|
||
word-sized memory accesses at 8 cycles per word). That's a ratio of 2.5
|
||
to 1, *exactly the same as on the 286*.
|
||
|
||
This is all theoretical. We put our trust not in theory but in actual
|
||
performance, so let's run this code through the Zen timer. On a PC,
|
||
Listing 11.4, which performs register-only addition, runs in 3.62 ms,
|
||
while Listing 11.5, which performs addition to a memory variable, runs
|
||
in 10.05 ms. On a 10 MHz AT clone, Listing 11.4 runs in 0.64 ms, while
|
||
Listing 11.5 runs in 1.80 ms. Obviously, the AT is much faster...but the
|
||
ratio of Listing 11.5 to Listing 11.4 is virtually identical on both
|
||
computers, at 2.78 for the PC and 2.81 for the AT. If anything, the
|
||
register-only form of `ADD` has a slightly *larger* advantage on the
|
||
AT than it does on the PC in this case.
|
||
|
||
Theory confirmed.
|
||
|
||
**LISTING 11.4 L11-4.ASM**
|
||
|
||
```nasm
|
||
;
|
||
; *** Listing 11.4 ***
|
||
;
|
||
; Measures the performance of adding an immediate value
|
||
; to a register, for comparison with Listing 11.5, which
|
||
; adds an immediate value to a memory variable.
|
||
;
|
||
call ZTimerOn
|
||
rept 1000
|
||
add dx,100h
|
||
endm
|
||
call ZTimerOff
|
||
```
|
||
|
||
**LISTING 11.5 L11-5.ASM**
|
||
|
||
```nasm
|
||
;
|
||
; *** Listing 11.5 ***
|
||
;
|
||
; Measures the performance of adding an immediate value
|
||
; to a memory variable, for comparison with Listing 11.4,
|
||
; which adds an immediate value to a register.
|
||
;
|
||
jmp Skip
|
||
;
|
||
even ;always make sure word-sized memory
|
||
; variables are word-aligned!
|
||
WordVar dw 0
|
||
;
|
||
Skip:
|
||
call ZTimerOn
|
||
rept 1000
|
||
add [WordVar]100h
|
||
endm
|
||
call ZTimerOff
|
||
```
|
||
|
||
What's going on? Simply this: Instruction fetching is controlling
|
||
overall execution time on *both* processors. Both the 8088 in a PC and
|
||
the 286 in an AT can execute the bytes of the instructions in Listings
|
||
11.4 and 11.5 faster than they can be fetched. Since the instructions
|
||
are exactly the same lengths on both processors, it stands to reason
|
||
that the ratio of the overall execution times of the instructions should
|
||
be the same on both processors as well. Instruction length controls
|
||
execution time, and the instruction lengths are the same—therefore the
|
||
ratios of the execution times are the same. The 286 can both fetch and
|
||
execute instruction bytes faster than the 8088 can, so code executes
|
||
much faster on the 286; nonetheless, because the 286 can also execute
|
||
those instruction bytes much faster than it can fetch them, overall
|
||
performance is still largely determined by the size of the instructions.
|
||
|
||
Is this always the case? No. When the prefetch queue is full,
|
||
memory-accessing instructions on the 286 and 386 are much faster
|
||
(relative to register-only instructions) than they are on the 8088.
|
||
Given the system wait states prevalent on 286 and 386 computers,
|
||
however, the prefetch queue is likely to be empty quite a bit,
|
||
especially when code consisting of instructions with short EU execution
|
||
times is executed. Of course, that's just the sort of code we're likely
|
||
to write when we're optimizing, so the performance of high-speed code is
|
||
more likely to be controlled by instruction size than by EU execution
|
||
time on most 286 and 386 computers, just as it is on the PC.
|
||
|
||
All of which is just a way of saying that faster memory access and EA
|
||
calculation notwithstanding, it's just as desirable to keep instructions
|
||
short and memory accesses to a minimum on the 286 and 386 as it is on
|
||
the 8088. And the way to do that is to use the registers as heavily as
|
||
possible, use string instructions, use short forms of instructions, and
|
||
the like.
|
||
|
||
The more things change, the more they remain the same....
|
||
|
||
#### POPF and the 286
|
||
|
||
We've one final 286-related item to discuss: the hardware malfunction of
|
||
`POPF` under certain circumstances on the 286.
|
||
|
||
The problem is this: Sometimes `POPF` permits interrupts to occur when
|
||
interrupts are initially off and the setting popped into the Interrupt
|
||
flag from the stack keeps interrupts off. In other words, an interrupt
|
||
can happen even though the Interrupt flag is never set to 1. Now, I
|
||
don't want to blow this particular bug out of proportion. It only causes
|
||
problems in code that cannot tolerate interrupts under any
|
||
circumstances, and that's a rare sort of code, especially in user
|
||
programs. However, some code really does need to have interrupts
|
||
absolutely disabled, with no chance of an interrupt sneaking through.
|
||
For example, a critical portion of a disk BIOS might need to retrieve
|
||
data from the disk controller the instant it becomes available; even a
|
||
few hundred microseconds of delay could result in a sector's worth of
|
||
data misread. In this case, one misplaced interrupt during a `POPF`
|
||
could result in a trashed hard disk if that interrupt occurs while the
|
||
disk BIOS is reading a sector of the File Allocation Table.
|
||
|
||
There is a workaround for the `POPF` bug. While the workaround is easy
|
||
to use, it's considerably slower than `POPF`, and costs a few bytes as
|
||
well, so you won't want to use it in code that can tolerate interrupts.
|
||
On the other hand, in code that truly cannot be interrupted, you should
|
||
view those extra cycles and bytes as cheap insurance against mysterious
|
||
and erratic program crashes.
|
||
|
||
One obvious reason to discuss the `POPF` workaround is that it's
|
||
useful. Another reason is that the workaround is an excellent example of
|
||
Zen-level assembly coding, in that there's a well-defined goal to be
|
||
achieved but no obvious way to do so. The goal is to reproduce the
|
||
functionality of the `POPF` instruction without using `POPF`, and
|
||
the place to start is by asking exactly what `POPF` does.
|
||
|
||
All `POPF` does is pop the word on top of the stack into the FLAGS
|
||
register, as shown in Figure 11.4. How can we do that without `POPF`?
|
||
Of course, the 286's designers intended us to use `POPF` for this
|
||
purpose, and didn't intentionally provide any alternative approach, so
|
||
we'll have to devise an alternative approach of our own. To do that,
|
||
we'll have to search for instructions that contain some of the same
|
||
functionality as `POPF`, in the hope that one of those instructions
|
||
can be used in some way to replace `POPF`.
|
||
|
||
Well, there's only one instruction other than `POPF` that loads the
|
||
FLAGS register directly from the stack, and that's `IRET`, which loads
|
||
the FLAGS register from the stack as it branches, as shown in Figure
|
||
11.5. iret has no known bugs of the sort that plague `POPF`, so it's
|
||
certainly a candidate to replace popf in non-interruptible applications.
|
||
Unfortunately, `IRET` loads the FLAGS register with the *third* word
|
||
down on the stack, not the word on top of the stack, as is the case with
|
||
`POPF`; the far return address that `IRET` pops into CS:IP lies
|
||
between the top of the stack and the word popped into the FLAGS
|
||
register.
|
||
|
||
Obviously, the segment:offset that `IRET` expects to find on the stack
|
||
above the pushed flags isn't present when the stack is set up for
|
||
`POPF`, so we'll have to adjust the stack a bit before we can
|
||
substitute `IRET` for `POPF`. What we'll have to do is push the
|
||
segment:offset of the instruction after our workaround code onto the
|
||
stack right above the pushed flags. `IRET` will then branch to that
|
||
address and pop the flags, ending up at the instruction after the
|
||
workaround code with the flags popped. That's just the result that would
|
||
have occurred had we executed `POPF`—WITH the bonus that no interrupts
|
||
can accidentally occur when the Interrupt flag is 0 both before and
|
||
after the pop.
|
||
|
||

|
||
|
||
How can we push the segment:offset of the next instruction? Well,
|
||
finding the offset of the next instruction by performing a near call to
|
||
that instruction is a tried-and-true trick. We can do something similar
|
||
here, but in this case we need a far call, since `IRET` requires both
|
||
a segment and an offset. We'll also branch backward so that the address
|
||
pushed on the stack will point to the instruction we want to continue
|
||
with. The code works out like this:
|
||
|
||
```nasm
|
||
jmpshort popfskip
|
||
popfiret:
|
||
iret; branches to the instruction after the
|
||
; call, popping the word below the address
|
||
; pushed by CALL into the FLAGS register
|
||
popfskip:
|
||
call far ptr popfiret
|
||
;pushes the segment:offset of the next
|
||
; instruction on the stack just above
|
||
; the flags word, setting things up so
|
||
; that IRET will branch to the next
|
||
; instruction and pop the flags
|
||
; When execution reaches the instruction following this comment,
|
||
; the word that was on top of the stack when JMP SHORT POPFSKIP
|
||
; was reached has been popped into the FLAGS register, just as
|
||
; if a POPF instruction had been executed.
|
||
```
|
||
|
||

|
||
|
||
The operation of this code is illustrated in Figure 11.6.
|
||
|
||
The `POPF` workaround can best be implemented as a macro; we can also
|
||
emulate a far call by pushing CS and performing a near call, thereby
|
||
shrinking the workaround code by 1 byte:
|
||
|
||
```nasm
|
||
EMULATE_POPF macro
|
||
local popfskip, popfiret
|
||
jmp short popfskip
|
||
popfiret:
|
||
iret
|
||
popfskip:
|
||
push cs
|
||
call popfiret
|
||
endm
|
||
```
|
||
|
||
By the way, the flags can be popped much more quickly if you're willing
|
||
to alter a register in the process. For example, the following macro
|
||
emulates `POPF` with just one branch, but wipes out AX:
|
||
|
||
```nasm
|
||
EMULATE_POPF_TRASH_AX macro
|
||
push cs
|
||
mov ax,offset $+5
|
||
push ax
|
||
iret
|
||
endm
|
||
```
|
||
|
||
It's not a perfect substitute for `POPF`, since `POPF` doesn't alter
|
||
any registers, but it's faster and shorter than `EMULATE_POPF` when
|
||
you can spare the register. If you're using 286-specific instructions,
|
||
you can use which is shorter still, alters no registers, and branches
|
||
just once. (Of course, this version of `EMULATE_POPF` won't work on
|
||
an 8088.)
|
||
|
||
```nasm
|
||
.286
|
||
:
|
||
EMULATE_POPFmacro
|
||
pushcs
|
||
pushoffset $+4
|
||
iret
|
||
endm
|
||
```
|
||
|
||

|
||
|
||
The standard version of `EMULATE_POPF` is 6 bytes longer than
|
||
`POPF` and much slower, as you'd expect given that it involves three
|
||
branches. Anyone in his/her right mind would prefer `POPF` to a
|
||
larger, slower, three-branch macro—given a choice. In noncode, however,
|
||
there's no choice here; the safer—if slower—approach is the best.
|
||
(Having people associate your programs with crashed computers is *not* a
|
||
desirable situation, no matter how unfair the circumstances under which
|
||
it occurs.)
|
||
|
||
And now you know the nature of and the workaround for the `POPF` bug.
|
||
Whether you ever need the workaround or not, it's a neatly packaged
|
||
example of the tremendous flexibility of the x86 instruction set.
|