Intel, AMD及VIA CPU的微架構(19)
6.8. 部分暫存器暫停
部分暫存器暫停發生在我們寫入一個32位暫存器部分,隨後讀整個暫存器或更大的部分時。例如:
mov al, byte ptr [mem8]
mov ebx, eax ; Partial register stall
這給出了5-6個時鐘週期的時延。原因是臨時暫存器已經被分配給AL,使它與AH無關。執行單元必須等待,直到對AL的寫被回收,才可能將AL的值合併到EAX餘下部分的值。可以修改程式碼,避免這個暫停:
; Example 6.10b. Partial register stall removed
movzx ebx, byte ptr [mem8]
and eax, 0ffffff00h
or ebx, eax
當然,通過將其他指令放在部分暫存器寫之後,使得你在讀整個暫存器之前,有時間回收,也可以避免這個部分暫停。
你應該知道只要混合不同的資料大小(8,16及32位),部分暫停就會發生:
; Example 6.11. Partial register stalls
mov bh, 0
add bx, ax ; Stall
inc ebx ; Stall
在寫整個暫存器或更大的部分之後讀部分暫存器,暫停不會發生:
; Example 6.12. Partial register stalls
mov eax, [mem32]
add bl, al ; No stall
add bh, ah ; No stall
mov cx, ax ; No stall
mov dx, bx ; Stall
避免部分暫存器暫停最簡單的方法是,總是使用完整的暫存器,在讀更小的記憶體運算元時,使用MOVZX或MOVSX。在PPro,P2與P3上,這些指令是快的,但在更早的處理器上是慢的。因此,在你希望程式碼在所有的處理器上都有合理的效能時,需要進行折衷。像這樣替換MOVZX EAX, BYTE PTR [MEM8]:
; Example 6.13. Replacement for movzx
xor eax, eax
mov al, byte ptr [mem8]
對這個組合,PPro,P2與P3處理器有一個特殊情形,在後面從EAX讀的時候,避免部分暫存器暫停。這個技巧是,一個暫存器在與自己XOR時被標記為空。處理器記住EAX的高24位是0,因此可以避免部分暫存器暫停。這個機制僅在特定的組合中奏效:
; Example 6.14. Removing partial register stalls with xor
xor eax, eax
mov al, 3
mov ebx, eax ; No stall
xor ah, ah
mov al, 3
mov bx, ax ; No stall
xor eax, eax
mov ah, 3
mov ebx, eax ; Stall
sub ebx, ebx
mov bl, dl
mov ecx, ebx ; No stall
mov ebx, 0
mov bl, dl
mov ecx, ebx ; Stall
mov bl, dl
xor ebx, ebx ; No stall
通過減去自己把暫存器置為0,與XOR效果相同,但使用MOV指令把它置為0不能防止暫停。
我們可以在一個迴圈外設定XOR:
; Example 6.15. Removing partial register stalls with xor outside loop
xor eax, eax
mov ecx, 100
LL: mov al, [esi]
mov [edi], eax ; no stall
inc esi
add edi, 4
dec ecx
jnz LL
處理器記住EAX的高24位是0,只要你沒有得到一箇中斷、誤預測或其他序列事件。
在呼叫可能壓棧整個暫存器的例程之前,你應該記住將所有部分暫存器置0(neutralize):
; Example 6.16. Removing partial register stall before call
add bl, al
mov [mem8], bl
xor ebx, ebx ; neutralize bl
call _highLevelFunction
許多高階語言例程在一開始就將EBX壓棧,在上面的例子中,如果你沒有將BL置0,這會產生一個部分暫存器暫停。
在PPro,P2,P3與PM上,使用XOR方法將一個暫存器置0,不會打破其對更早指令的依賴(但在P4上會)。例如:
; Example 6.17. Remove partial register stalls and break dependence
div ebx
mov [mem], eax
mov eax, 0 ; Break dependence
xor eax, eax ; Prevent partial register stall
mov al, cl
add ebx, eax
這裡將EAX置0兩次看起來是多餘的,但沒有MOV EAX, 0,最後的指令將不得不等待慢的DIV完成,而沒有XOR EAX, EAX,將有一個部分暫存器暫停。
指令FNSTSW AX是特殊的:在32位模式中,其行為就像寫入整個EAX。事實上,在32位模式裡,它所做的類似這樣:
; Example 6.18. Equivalence model for fnstsw ax
and eax, 0ffff0000h
fnstsw temp
or eax, temp
因此,在32位模式中,在這條指令後讀EAX時,不會有一個部分暫存器暫停:
; Example 6.19. Partial register stalls with fnstsw ax
fnstsw ax / mov ebx,eax ; Stall only if 16 bit mode
mov ax,0 / fnstsw ax ; Stall only if 32 bit mode
部分標記暫停
標記暫存器也會導致部分暫存器暫停:
; Example 6.20. Partial flags stall
cmp eax, ebx
inc ecx
jbe xx ; Partial flags stall
指令JBE讀進位標記與零標記。因為INC指令改變零標記,但不改變進位標記,JBE指令必須等前兩條指令回收後,才能合併來自CMP指令的進位標記以及來自INC指令的零標記。在彙編程式碼中,這個情形很可能是一個bug,而不是一個有目的的標記組合,要改正它,將INC ECX改為ADD ECX, 1。一個導致部分標記暫停的類似的bug是SAHF / JL XX。JL指令測試符號標記以及溢位標記,但SAHF不改變溢位標記。要改正它,將JL XX改為JS XX。
出乎意料地(與Intel手冊所說的相反),在一條修改了某些標記位的指令後,在讀未修改標記位時,我們也遇到了部分標記暫停:
; Example 6.21. Partial flags stall when reading unmodified flag bits
cmp eax, ebx
inc ecx
jc xx ; Partial flags stall
但在讀修改標記時不會:
; Example 6.22. No partial flags stall when reading modified bits
cmp eax, ebx
inc ecx
jz xx ; No stall
部分標記暫停有可能發生在讀許多或所有標記位的指令上,比如LAHF,PUSHF,PUSHFD。以下指令在後接LAHF或PUSH(D)時,導致部分標記暫停:INC,DEC,TEST,位元測試,位元掃描,CLC,STC,CMC,CLD,STD,CLI,STI,MUL,IMUL,以及所有的偏移與旋轉。以下指令不會導致部分標記暫停:AND,OR,XOR,ADD,ADC,SUB,SBB,CMP,NEG。很奇怪TEST與AND行為不同,根據定義,它們對標記做同樣的事。為了避免暫停,你可以使用SETcc指令替代LAHF或PUSHF(D),來儲存一個標記的值。
例如:
; Example 6.23. Partial flags stalls
inc eax / pushfd ; Stall
add eax,1 / pushfd ; No stall
shr eax,1 / pushfd ; Stall
shr eax,1 / or eax,eax / pushfd ; No stall
test ebx,ebx / lahf ; Stall
and ebx,ebx / lahf ; No stall
test ebx,ebx / setz al ; No stall
clc / setz al ; Stall
cld / setz al ; No stall
部分標記暫停的代價大約是4個時鐘週期。
偏移與旋轉後的標記暫停
在一次偏移或旋轉後,會得到一個相似的部分標記暫停,除了偏移或旋轉一位(短形式):
; Example 6.24. Partial flags stalls after shift and rotate
shr eax,1 / jz xx ; No stall
shr eax,2 / jz xx ; Stall
shr eax,2 / or eax,eax / jz xx ; No stall
shr eax,5 / jc xx ; Stall
shr eax,4 / shr eax,1 / jc xx ; No stall
shr eax,cl / jz xx ; Stall, even if cl = 1
shrd eax,ebx,1 / jz xx ; Stall
rol ebx,8 / jc xx ; Stall
這些暫停的代價大約是4個時鐘週期。
6.9. 寫轉發暫停
寫轉發暫停有點類似於部分暫存器暫停。在對同一記憶體地址混合使用不同資料大小時發生:
; Example 6.25. Store-to-load forwarding stall
mov byte ptr [esi], al
mov ebx, dword ptr [esi] ; Stall. Big read after small write
一個小的寫之後大的讀阻礙寫到讀轉發,這個代價是大約7 – 8個時鐘週期。
不像部分暫存器暫停,在向記憶體寫一個運算元,然後讀它的一部分,如果這個較小的部分不是在同一個地址開始,你也會有寫轉發暫停:
; Example 6.26. Store-to-load forwarding stall
mov dword ptr [esi], eax
mov bl, byte ptr [esi] ; No stall
mov bh, byte ptr [esi+1] ; Stall. Not same start address
通過把最後一行改為MOV BH, AH,可以避免這個暫停,但這樣一個解決方案在像這樣的情形中是不可能的:
; Example 6.27. Store-to-load forwarding stall
fistp qword ptr [edi]
mov eax, dword ptr [edi]
mov edx, dword ptr [edi+4] ; Stall. Not same start address
有趣的,在讀寫不同的地址時,如果它們恰好在不同的快取庫(cache bank)中有相同的組值,會得到一個偽寫轉發暫停:
; Example 6.28. Bogus store-to-load forwarding stall
mov byte ptr [esi], al
mov ebx, dword ptr [esi+4092] ; No stall
mov ecx, dword ptr [esi+4096] ; Bogus stall
6.10. PPro,P2,P3中的瓶頸
在為這些處理器優化程式碼時,分析瓶頸在哪是重要的。如果一個瓶頸更窄,花時間優化掉另一個瓶頸是不合理的。
如果預期程式碼快取不命中,你應該重構程式碼將程式碼大多數使用的部分維繫一起。
如果預期許多資料快取不命中,那麼忘掉其他一切,專注如何重構程式碼,減少快取不命中的數量(第2頁),在一次資料讀快取不命中後,避免長的依賴鏈。
如果有許多除法,那麼嘗試如手冊1“優化C++程式碼”與手冊2“優化彙編例程”裡描述的那樣減少它們,確保在除法期間處理器有事可做。
依賴鏈傾向於阻礙亂序執行。嘗試打破長的依賴鏈,特別是如果它們包含慢的指令,比如乘法、除法以及浮點指令。參考手冊1“優化C++程式碼” 與手冊2“優化彙編例程”。
如果有許多跳轉、呼叫或返回,特別是如果跳轉的可預測性很差,嘗試避免其中一些。如果不會增加依賴性的話,使用條件移動替換可預測性差的條件跳轉。內聯小的例程(參考手冊2“優化彙編例程”)。
如果混合使用不同的資料大小(8,16及32位整數),小心部分暫停。如果使用PUSHF或LAHF指令,小心部分標記暫停。在偏移或旋轉超過1時,避免測試標記(第71頁)。
如果致力於每時鐘週期3個μop,小心在指令獲取及解碼中可能的時延(第62頁),特別是在小迴圈中。在這些處理器裡,指令解碼通常是最窄的瓶頸,而且不幸的是,這個因素使得優化相當的複雜。如果在程式碼開頭做了修改來改進它,這個改進可能會有移動後續程式碼的IFETCH塊及16位元組邊界的效果。邊界的改變在總體時鐘週期數會有不可預知的影響,掩蓋這個改進的效果。
每時鐘週期兩個永久暫存器讀的限制會將吞吐率降到低於每時鐘週期3個μop(第66頁)。如果通常在最後一次修改,超過4個時鐘週期後,讀暫存器,這可能發生。例如,如果經常使用指標對資料取址,但很少修改這些指標,這就可能發生。
每時鐘3個μop的吞吐率要求執行埠得到的μop超過不能三分之一(第70頁)。
回收站每時鐘週期可以處理3個μop,但對被採用的跳轉稍微不那麼有效(第70頁)。