5.9. 写转发暂停(Storeforwarding stalls)
访问一个内存操作数一部分的问题,比访问一个寄存器一部分的问题,要严重得多。对之前的处理器,这些问题都是相同的,参考第76页。
例子:
;Example 5.8a. Store forwarding stall
movdword ptr [mem1], eax
movdword ptr [mem1+4], 0
fild qword ptr [mem1] ; Large penalty
通过改变为这样,你可以节省10-20个时钟周期:
;Example 5.8b. Avoid store forwarding stall
movdxmm0, eax
movqqword ptr [mem1], xmm0
fild qword ptr [mem1] ; No penalty
5.10. 依赖链中的内存中介
P4有一个不幸的倾向,尝试在一个内存操作数就绪前读取它。如果你写
;Example 5.9. Memory intermediate in dependency chain
imuleax, 5
mov[mem1], eax
movebx, [mem1]
add ebx, ecx
在IMUL与内存写完成前,微处理器可能尝试将[MEM1]的值读到EBX。很快,它发现它读的这个值是无效的,因此它将丢弃EBX并再次尝试。它将持续重演这个读指令以及后续指令,直到[MEM1]中的数据就绪。看起来它可以重演指令序列多少次没有限制,这个过程盗取其他过程的资源。在一个长的依赖链里,代价通常是10-20个时钟周期!使用MFENCE指令串行化内存访问不能解决这个问题,因为这个指令代价更高。在其他微处理器上,包括P4E,在写入相同内存位置后,立即读这个内存操作数的代价仅是几个时钟周期。
当然在上面的例子中,避免这个问题最好的方式是以MOVEBX, EAX替换MOVEBX, [MEM1]。另一个可能的解决方案是,在同一个地址的写与读之间,给处理器大量的工作。
不过,有两个情形,不可能将数据保持在寄存器中。第一个情形是,在16位及32位模式中,在高级语言例程调用中的参数传递;第二个情形是在浮点寄存器与其他寄存器间传递数据。
传递参数到例程
在C++中以一个整形参数调用一个函数,在32位模式中看起来像这样:
;Example 5.10. Memory intermediate in function call (32-bit mode)
pusheax ; Saveparameter on stack
call_ff ;Call function _ff
addesp, 4 ; Clean up stack after call
...
_ffproc near ; Function entry
pushebp ; Saveebp
movebp,esp ;Copy stack pointer
moveax,[ebp+8] ;Read parameter from stack
...
popebp ; Restoreebp
ret ;Return from function
_ffendp
只要调用程序或被调用函数以高级语言写就,你可能必须坚持在栈上传递参数的惯例。当函数被声明为__fastcall时,大多数C++编译器可以在寄存器中传递2或3个整形参数。不过,这个方法不是标准的。不同的编译器使用不同的寄存器传递参数。为了避免这个问题,你可以汇编语言保持整个依赖链。在64位模式中这个问题可以被避免,其中大多数参数通过寄存器传递。
浮点与其他寄存器间的数据传递
没有办法在浮点寄存器与其他寄存器间传递数据,除了通过内存。例如:
;Example 5.11. Memory intermediate in integer to f.p. conversion
imuleax, ebx
mov[temp], eax ; Transfer datafrom integer register to f.p.
fild[temp]
fsqrt
fistp[temp] ; Transfer data from f.p. register to integer
moveax, [temp]
这里,我们有通过内存传递数据两次的问题。你通过在浮点寄存器中保存整个依赖链,或使用XMM寄存器替代浮点寄存器来避免这个问题。
避免过早读内存操作数的另一个方式是使读地址依赖这个数据。第一个传递可以像这样做:
;Example 5.12. Avoid stall in integer to f.p. conversion
mov[temp], eax
and eax, 0 ; Make eax = 0, but keepdependence
fild [temp+eax] ; Make read address depend on eax
AND EAX, 0指令将EAX设置为0,但保持在之前值上的一个虚假依赖。通过将EAX加入FILD指令的地址中,我们阻止了EAX就绪前的读。
在从浮点寄存器传输数据到整形寄存器时,制作类似的依赖性要复杂一点。解决这个问题最简单的方式是:
;Example 5.13. Avoid stall in f.p. to integer conversion
fistp [temp]
fnstswax ;Transfer status after fistp to ax
and eax, 0 ; Set to 0
mov eax,[temp+eax] ; Make dependent on eax
文献
重演机制的一个详细研究由VictorKartunov等发表:“Replay:Unknown Features of the NetBurst Core”,www.xbitlabs.com/articles/cpu/print/replay.html。也可参考US Patents6,163,838; 6,094,717; 6,385,715。
5.11. 打破依赖链
将一个寄存器置零的一个常用方式是XOREAX, EAX或SUBEBX, EBX。P4/P4E处理器认识到这些指令与寄存器之前的值无关。因此,任何使用这个寄存器新值的指令将无需等待在XOR或SUB指令前面的值就绪。这同样适用于使用64位或128位寄存器的PXOR指令,但不适用于以下使用8位或16位寄存器的XOR或SUB:SBB,PANDN,PSUB,XORPS,XORPD,SUBPS,SUBPD,FSUB。
指令XOR,SUB与PXOR对打破不必要的依赖性是有用的,但它不能工作在比如PM处理器上。
你也可以使用这些指令来打破标记上的依赖性。例如在P4上,旋转(rotate)指令有对标记的一个虚假依赖。这可以下面的方式消除:
;Example 5.14. Break false dependence on flags
roreax, 1
subedx, edx ; Remove false dependenceon the flags
rorebx, 1
如果你没有用于这个目的的一个空闲寄存器,那么使用一条不改变寄存器,仅改变标记的指令,比如CMP或TEST。你不能使用CLC来打破进位标记上的依赖。