abrash-black-book/04-03.md
2013-12-30 20:26:41 +11:00

156 lines
No EOL
7.3 KiB
Markdown

Back to the 8-bit bus cycle-eater. As I've said, in 8088 work you should
strive to use byte-sized memory variables whenever possible. That does
*not* mean that you should use 2 byte-sized memory accesses to
manipulate a word-sized memory variable in preference to 1 word-sized
memory access, as, for instance,
mov dl,byte ptr [MemVar]
mov dh,byte ptr [MemVar+1]
versus:
mov dx,word ptr [MemVar]
Recall that every access to a memory byte takes at least 4 cycles; that
limitation is built right into the 8088. The 8088 is also built so that
the second byte-sized memory access to a 16-bit memory variable takes
just those 4 cycles and no more. There's no way you can manipulate the
second byte of a word-sized memory variable faster with a second
separate byte-sized instruction in less than 4 cycles. As a matter of
fact, you're bound to access that second byte much more slowly with a
separate instruction, thanks to the overhead of instruction fetching and
execution, address calculation, and the like.
For example, consider Listing 4.3, which performs 1,000 word-sized reads
from memory. This code runs in 3.77 µs per word read on a 4.77 MHz 8088.
That's 45 percent faster than the 5.49 µs per word read of Listing 4.4,
which reads the same 1,000 words as Listing 4.3 but does so with 2,000
byte-sized reads. Both listings perform exactly the same number of
memory accesses—2,000 accesses, each byte-sized, as all 8088 memory
accesses must be. (Remember that the Bus Interface Unit must perform two
byte-sized memory accesses in order to handle a word-sized memory
operand.) However, Listing 4.3 is considerably faster because it expends
only 4 additional cycles to read the second byte of each word, while
Listing 4.4 performs a second **LODSB,** requiring 13 cycles, to read
the second byte of each word.
**LISTING 4.3 LST4-3.ASM**
; Measures the performance of reading 1,000 words
; from memory with 1,000 word-sized accesses.
;
sub si,si
mov cx,1000
call ZTimerOn
rep lodsw
call ZTimerOff
**LISTING 4.4 LST4-4.ASM**
; Measures the performance of reading 1000 words
; from memory with 2,000 byte-sized accesses.
;
sub si,si
mov cx,2000
call ZTimerOn
rep lodsb
call ZTimerOff
In short, if you must perform a 16-bit memory access, let the 8088 break
the access into two byte-sized accesses for you. The 8088 is more
efficient at that task than your code can possibly be.
Word-sized variables should be stored in registers to the greatest
feasible extent, since registers are inside the 8088, where 16-bit
operations are just as fast as 8-bit operations because the 8-bit
cycle-eater can't get at them. In fact, it's a good idea to keep as many
variables of all sorts in registers as you can. Instructions with
register-only operands execute very rapidly, partially because they
avoid both the time-consuming memory accesses and the lengthy address
calculations associated with memory operands.
There is yet another reason why register operands are preferable to
memory operands, and it's an unexpected effect of the 8-bit bus
cycle-eater. Instructions with only register operands tend to be shorter
(in terms of bytes) than instructions with memory operands, and when it
comes to performance, shorter is usually better. In order to explain why
that is true and how it relates to the 8-bit bus cycle-eater, I must
diverge for a moment.
For the last few pages, you may well have been thinking that the 8-bit
bus cycle-eater, while a nuisance, doesn't seem particularly subtle or
difficult to quantify. After all, any instruction reference tells us
exactly how many cycles each instruction loses to the 8-bit bus
cycle-eater, doesn't it?
Yes and no. It's true that in general we know approximately how much
longer a given instruction will take to execute with a word-sized memory
operand than with a byte-sized operand, although the dynamic RAM refresh
and wait state cycle-eaters (which I'll cover a little later) can raise
the cost of the 8-bit bus cycle-eater considerably. However, *all*
word-sized memory accesses lose 4 cycles to the 8-bit bus cycle-eater,
and there's one sort of word-sized memory access we haven't discussed
yet: instruction fetching. The ugliest manifestation of the 8-bit bus
cycle-eater is in fact the prefetch queue cycle-eater.
### The Prefetch Queue Cycle-Eater {#Heading9}
In an 8088 context, here's the prefetch queue cycle-eater in a nutshell:
The 8088's 8-bit external data bus keeps the Bus Interface Unit from
fetching instruction bytes as fast as the 16-bit Execution Unit can
execute them, so the Execution Unit often lies idle while waiting for
the next instruction byte to be fetched.
Exactly why does this happen? Recall that the 8088 is an 8086
internally, but accesses word-sized memory data at only one-half the
maximum rate of the 8086 due to the 8088's 8-bit external data bus.
Unfortunately, instructions are among the word-sized data the 8086
fetches, meaning that the 8088 can fetch instructions at only one-half
the speed of the 8086. On the other hand, the 8086-equivalent Execution
Unit of the 8088 can *execute* instructions every bit as fast as the
8086. The net result is that the Execution Unit burns up instruction
bytes much faster than the Bus Interface Unit can fetch them, and ends
up idling while waiting for instructions bytes to arrive.
The BIU can fetch instruction bytes at a maximum rate of one byte every
4 cycles—*and that 4-cycle per instruction byte rate is the ultimate
limit on overall instruction execution time, regardless of EU speed.*
While the EU may execute a given instruction that's already in the
prefetch queue in less than 4 cycles per byte, over time the EU can't
execute instructions any faster than they can arrive—and they can't
arrive faster than 1 byte every 4 cycles.
Clearly, then, the prefetch queue cycle-eater is nothing more than one
aspect of the 8-bit bus cycle-eater. 8088 code often runs at less than
the Execution Unit's maximum speed because the 8-bit data bus can't keep
up with the demand for instruction bytes. That's straightforward
enough—so why all the fuss about the prefetch queue cycle-eater?
What makes the prefetch queue cycle-eater tricky is that it's
undocumented and unpredictable. That is, with a word-sized memory
access, such as
mov [bx],ax
it's well-documented that an extra 4 cycles will always be required to
write the upper byte of AX to memory. Not so with the prefetch queue
cycle-eater lurking nearby. For instance, the instructions
shr ax,1
shr ax,1
shr ax,1
shr ax,1
shr ax,1
should execute in 10 cycles, since each **SHR** takes 2 cycles to
execute, according to Intel's specifications. Those specifications
contain Intel's official instruction execution times, but in this
case—and in many others—the specifications are drastically wrong. Why?
Because they describe execution time *once an instruction reaches the
prefetch queue.* They say nothing about whether a given instruction will
be in the prefetch queue when it's time for that instruction to run, or
how long it will take that instruction to reach the prefetch queue if
it's not there already. Thanks to the low performance of the 8088's
external data bus, that's a glaring omission—but, alas, an unavoidable
one. Let's look at why the official execution times are wrong, and why
that can't be helped.