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

111 lines
6.6 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

However, this beneficial pairing does not extend to non-**MOV**
instructions with explicit memory destination operands, such as **ADD
[EBX],EAX**. The Pentium executes only one such memory instruction at a
time; if two memory-destination complex instructions get paired, first
the U-pipe instruction is executed, and then the V-pipe instruction,
with only one cycle of overlap, as shown in Figure 20.6. I don't know
for sure, but I'd guess that this is to guarantee that the two pipes
will never perform out-of-order access to any given memory location.
Thus, even though **AND [EBX],AL** pairs with **AND [ECX],DL**, the two
instructions take 5 cycles in all to execute, and 4 cycles of idle
time—2 in the U-pipe and 2 in the V-pipe, out of 10 cycles in all—are
incurred in the process.
![](images/20-06.jpg)\
**Figure 20.6**  *Non-overlapped lockstep execution.*
![](images/20-07.jpg)\
**Figure 20.7**  *Interleaving simple instructions for maximum
performance.*
The solution is to break the instructions into simple instructions and
interleave them, as shown in Figure 20.7, which accomplishes the same
task in 3 cycles, with no idle cycles whatsoever. Figure 20.7 is a good
example of what optimized Pentium code generally looks like: mostly
one-cycle instructions, mixed together so that at least two operations
are in progress at once. It's not the easiest code to read or write, but
it's the only way to get both pipes running at capacity.
### Superscalar Notes {#Heading6}
You may well ask why it's necessary to interleave operations, as is done
in Figure 20.7. It seems simpler just to turn
and [ebx],al
into
mov dl,[ebx]
and dl,al
mov [ebx],dl
and be done with it. The problem here is one of dependency. Before the
Pentium can execute **AND DL,AL,**, it must first know what is in DL,
and it can't know that until it loads DL from the address pointed to by
EBX. Therefore, **AND DL,AL** can't happen until the cycle after **MOV
DL,[EBX]** executes. Likewise, the result can't be stored until the
cycle after **AND DL,AL** has finished. This means that these
instructions, as written, can't possibly pair, so the sequence takes the
same three cycles as **AND [EBX],AL**. (Now it should be clear why **AND
[EBX]**, AL takes 3 cycles.) Consequently, it's necessary to interleave
these instructions with instructions that use other registers, so this
set of operations can execute in one pipe while the other, unrelated set
executes in the other pipe, as is done in Figure 20.7.
What we've just seen is the read-after-write form of the superscalar
hazard known as *register contention*. I'll return to the subject of
register contention in the next chapter; in the remainder of this
chapter I'd like to cover a few short items about superscalar execution.
#### Register Starvation {#Heading7}
The above examples should make it pretty clear that effective
superscalar programming puts a lot of strain on the Pentium's relatively
small register set. There are only seven general-purpose registers (I
strongly suggest using EBP in critical loops), and it does not help to
have to sacrifice one of those registers for temporary storage on each
complex memory operation; in pre-superscalar days, we used to employ
those handy CISC memory instructions to do all that stuff without using
any extra registers.
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
![](images/i.jpg) *More problematic still is that for maximum pairing, you'll typically have two operations proceeding at once, one in each pipe, and trying to keep two operations in registers at once is difficult indeed. There's not much to be done about this, other than clever and Spartan register usage, but be aware that it's a major element of Pentium performance programming.*
------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Also be aware that prefixes of every sort, with the sole exception of
the 0FH prefix on non-short conditional jumps, always execute in the
U-pipe, and that Intel's documentation indicates that no pairing can
happen while a prefix byte executes. (As I'll discuss in the next
chapter, my experiments indicate that this rule doesn't always apply to
multiple-cycle instructions, but you still won't go far wrong by
assuming that the above rule is correct and trying to eliminate prefix
bytes.) A prefix byte takes one cycle to execute; after that cycle, the
actual prefixed instruction itself will go through the U-pipe, and if it
and the following instruction are mutually pairable, then they will
pair. Nonetheless, prefix bytes are very expensive, effectively taking
at least as long as two normal instructions, and possibly, if a prefixed
instruction could otherwise have paired in the V-pipe with the previous
instruction, taking as long as three normal instructions, as shown in
Figure 20.8.
Finally, bear in mind that if the instructions being executed have not
already been executed at least once since they were loaded into the
internal cache, they can pair only if the first (U-pipe) instruction is
not only pairable but also exactly 1 byte long, a category that includes
only **INC *reg*, DEC *reg*, PUSH *reg***, and **POP *reg***. Knowing
this can help you understand why sometimes, timing reveals that your
code runs slower than it seems it should, although this will generally
occur only when the cache working set for the code you're timing is on
the order of 8K or more—an awful lot of code to try to optimize.
It should be excruciatingly clear by this point that you *must* time
your Pentium-optimized code if you're to have any hope of knowing if
your optimizations are working as well as you think they are; there are
just too many details involved for you to be sure your optimizations are
working properly without checking. My most basic optimization rule has
always been to grab the Zen timer and *measure actual performance*—and
nowhere is this more true than on the Pentium. Don't believe it until
you measure it!
![](images/20-08.jpg)\
**Figure 20.8**  *Prefix delays.*