170 lines
7 KiB
Markdown
170 lines
7 KiB
Markdown
------------------------ --------------------------------- --------------------
|
||
[Previous](21-01.html) [Table of Contents](index.html) [Next](21-03.html)
|
||
------------------------ --------------------------------- --------------------
|
||
|
||
As on the 486, you should keep a careful eye out for AGIs involving the
|
||
stack pointer. Implicit modifiers of ESP, such as **PUSH** and **POP**,
|
||
are special-cased so you don’t have to worry about AGIs. However, if you
|
||
explicitly modify ESP with this instruction
|
||
|
||
sub esp,100h
|
||
|
||
for example, or with the popular
|
||
|
||
mov esp,ebp
|
||
|
||
you can then get AGIs if you attempt to use ESP to address memory,
|
||
either explicitly with instructions like this one
|
||
|
||
moveax,[esp+20h]
|
||
|
||
or via **PUSH**, **POP**, or other instructions that implicitly use ESP
|
||
as an addressing register.
|
||
|
||
On the 486, any instruction that had both a constant value and an
|
||
addressing displacement, such as
|
||
|
||
mov dword ptr [ebp+16],1
|
||
|
||
suffered a 1-cycle penalty, taking a total of 2 cycles. Such
|
||
instructions take only one cycle on the Pentium, but they cannot pair,
|
||
so they’re still the most expensive sort of **MOV**. Knowing this can
|
||
speed up something as simple as zeroing two memory variables, as in
|
||
|
||
sub eax,eax ;U-pipe 1
|
||
;any V-pipe pairable
|
||
; instruction can go here,
|
||
; or SUB could be in V-pipe
|
||
mov [MemVar1],eax ;U-pipe 2
|
||
mov [MemVar2],eax ;V-pipe 2
|
||
|
||
which should never be slower and should potentially be 0.5 cycles
|
||
faster, and six bytes smaller than this sequence:
|
||
|
||
mov [MemVar1],0 ;U-pipe 1
|
||
mov [MemVar2],0 ;U-pipe 2
|
||
|
||
Note, however, that my experiments thus far indicate that the two writes
|
||
in the first case don’t actually pair (possibly because the memory
|
||
variables have never been read into the internal cache), so you might
|
||
want to insert an instruction between the two **MOV**s—and, of course,
|
||
this is yet another reason why you should always measure your code’s
|
||
actual performance.
|
||
|
||
### Register Contention {#Heading4}
|
||
|
||
Finally, we come to the last major component of superscalar
|
||
optimization: register contention. The basic premise here is simple: You
|
||
can’t use the same register in two inherently sequential ways in a
|
||
single cycle. For example, you can’t execute
|
||
|
||
inc eax ;U-pipe cycle 1
|
||
;V-pipe idle cycle 1
|
||
; due to dependency
|
||
and ebx,eax ;U-pipe cycle 2
|
||
|
||
in a single cycle; **AND EBX,EAX** can’t execute until the value in EAX
|
||
is known, and that can’t happen until **INC EAX** is done. Consequently,
|
||
the V-pipe idles while **INC EAX** executes in the U-pipe. We saw this
|
||
in the last chapter when we discussed splitting instructions into simple
|
||
instructions, and it is by far the most common sort of register
|
||
contention, known as read-after-write register contention.
|
||
Read-after-write register contention is the primary reason we have to
|
||
interleave independent operations in order to get maximum V-pipe usage.
|
||
|
||
The other sort of register contention is known as write-after-write.
|
||
Write-after-write register contention happens when two instructions try
|
||
to write to the same register on the same cycle. While that may not seem
|
||
like a particularly useful operation in general, it can happen when
|
||
subregisters are being set, as in the following
|
||
|
||
sub eax,eax ;U-pipe cycle 1
|
||
;V-pipe idle cycle 1
|
||
; due to register contention
|
||
mov al,[Var] ;U-pipe cycle 2
|
||
|
||
where an attempt is made to set both EAX and its AL subregister on the
|
||
same cycle. Write-after-write contention implies that the two
|
||
instructions comprising the above substitute for **MOVZX** should have
|
||
at least one unrelated instruction between them when **SUB EAX,EAX**
|
||
executes in the V-pipe.
|
||
|
||
#### Exceptions to Register Contention {#Heading5}
|
||
|
||
Intel has special-cased some very useful exceptions to register
|
||
contention. Happily, write-after-read operations do *not* cause
|
||
contention. Such operations, as in
|
||
|
||
mov eax,edx ;U-pipe cycle 1
|
||
sub edx,edxX ;V-pipe cycle 1
|
||
|
||
are free of charge.
|
||
|
||
Also, stack-related instructions that modify ESP only implicitly
|
||
(without ESP as part of any explicit operand) do not cause AGIs, and
|
||
neither do they cause register contention with other instructions that
|
||
use ESP only implicitly; such instructions include **PUSH *reg/immed*,
|
||
POP *reg***, and **CALL**. (However, these instructions do cause
|
||
register contention on ESP—but not AGIs—with instructions that use ESP
|
||
explicitly, such as **MOV EAX,[ESP+4]**.) Without this special case, the
|
||
following sequence would hardly use the V-pipe at all:
|
||
|
||
mov eax,[MemVar] ;U-pipe cycle 1
|
||
push esi ;V-pipe cycle 1
|
||
push eax ;U-pipe cycle 2
|
||
push edi ;V-pipe cycle 2
|
||
push ebx ;U-pipe cycle 3
|
||
call FooTilde ;V-pipe cycle 3
|
||
|
||
But in fact, all the instructions pair, even though ESP is modified five
|
||
times in the space of six instructions.
|
||
|
||
The final register-contention special case is both remarkable and
|
||
remarkably important. There is exactly one sort of instruction that can
|
||
pair only in the V-pipe: branches. Any near call or conditional or
|
||
unconditional near jump can execute in the V-pipe paired with any
|
||
pairable U-pipe instruction, as illustrated by this sequence:
|
||
|
||
LoopTop:
|
||
mov [esi],eax ;U-pipe cycle 1
|
||
add esi,4 ;V-pipe cycle 1
|
||
dec ecx ;U-pipe cycle 2
|
||
jnz LoopTop ;V-pipe cycle 2
|
||
|
||
Branches can’t pair in the U-pipe; a branch that executes in the U-pipe
|
||
runs alone, with the V-pipe idle. If a call or jump is correctly
|
||
predicted by the Pentium’s branch prediction circuitry (as discussed in
|
||
the last chapter), it executes in a single cycle, pairing if it runs in
|
||
the V-pipe; if mispredicted, conditional jumps take 4 cycles in the
|
||
U-pipe and 5 cycles in the V-pipe, and mispredicted calls and
|
||
unconditional jumps take 3 cycles in either pipe. Note that **RET**
|
||
can’t pair.
|
||
|
||
### Who’s in First? {#Heading6}
|
||
|
||
One of the trickiest things about superscalar optimization is that a
|
||
given instruction stream can execute at a different speed depending on
|
||
the pipe where it starts execution, because which instruction goes
|
||
through the U-pipe first determines which of the following instructions
|
||
will be able to pair. If we take the last example and add one more
|
||
instruction, the other instructions will go through different pipes than
|
||
previously, and cause the loop as a whole to take 50 percent longer,
|
||
even though we only added 25 percent more cycles:
|
||
|
||
LoopTop:
|
||
inc edx ;U-pipe cycle 1
|
||
mov [esi],eax ;V-pipe cycle 1
|
||
add esi,4 ;U-pipe cycle 2
|
||
dec ecx ;V-pipe cycle 2
|
||
jnz LoopTop ;U-pipe cycle 3
|
||
;V-pipe idle cycle 3
|
||
; because JNZ can’t
|
||
; pair in the U-pipe
|
||
|
||
------------------------ --------------------------------- --------------------
|
||
[Previous](21-01.html) [Table of Contents](index.html) [Next](21-03.html)
|
||
------------------------ --------------------------------- --------------------
|
||
|
||
* * * * *
|
||
|
||
Graphics Programming Black Book © 2001 Michael Abrash
|