Intel, AMD及VIA CPU的微架構(22)
7.11. 連線到埠0與1的執行單元
如上面提到的,有些執行單元是雙倍的。例如,兩個整形向量加法μop可以同時執行,分別在通過埠0與1的ALU上。
其他一些執行單元可通過埠0與1訪問,但不是雙倍的。例如,一個浮點加法μop可以通過埠0或埠1,但僅有一個浮點加法器,因此同時執行兩個浮點加法μop是不可能的。
實現這個機制讓浮點加法μop通過任一空閒的埠,無疑是為了提升效能。但不幸的是,這個機制壞處比好處多。大多數μop是浮點加法,或其他可以通過任一埠的程式碼需要比預期更長的時間執行,通常是多50%。
這個現象最可能的解釋是,排程器在同一時鐘週期裡釋出兩個浮點加法μop,每個埠一個。但這兩個μop不能同時執行,因為它們需要相同的執行單元。結果,其中一個埠暫停,一個時鐘週期無事可做。
這適用於以下指令:
- 所有浮點加法與減法,包括單精度,雙精度與長雙精度,在ST()與XMM暫存器中,帶有標量及向量運算元,即FADD,ADDSD,SUBPS。
- 帶有xmm結果的xmm比較指令,即CMPEQPS。
- Xmm max與min指令,即MAXPS。
- 16位整數的xmm向量乘法,即PMULLW,PMADDWD。
任何包含許多這些指令的程式碼執行的時間很可能超過所需。指令都是上述的相同型別還是混合型別,沒有區別。
使用僅能通過其中一個埠的μop,不會發生這個問題,即MULPS,COMISS,PMULUDQ。使用能通過兩個埠,ALU是雙倍的μop也不會發生這個問題。
一個實際意義不那麼重要,但揭示了一點硬體設計的進一步複雜性是,像PMULLW與ADDPS重要的指令不能同時執行,甚至它們在使用不同的執行單元。
包含許多上述型別指令程式碼差勁的效能是一個不良設計的結果。這個機制無疑是實現來提升效能的。當然,找出這個機制確實提升了效能的例子是可能的,設計者可能在腦子裡有一個特定的例子。但僅在只有少量上述型別指令,多數指令佔據埠1的程式碼中看到效能提升。這樣的例子很少見,好處很有限,因為它僅適用於只有少量這些指令的程式碼。當許多指令是上述型別,但不是所有時,效能損失最大。在浮點程式碼中,這樣的情形很常見。在浮點程式碼中關鍵的最裡層迴圈可能有許多加法。因此浮點程式碼的效能很可能因為這個不良設計受到不小的影響。
程式設計師對這個問題幾乎無能為力,因為不能在CPU流水線中控制μop的重排。使用整數替代浮點數可行性很低。一個只做浮點加法的迴圈可以通過展開來改進,因為這降低了受限於被阻塞埠的迴圈開銷指令的相對代價。有時可以通過重排指令,使得沒有兩個FADD μop在同一時鐘週期裡釋出給埠,改進僅少數指令是浮點加法的迴圈。這要求許多經驗,而且結果對迴圈前的程式碼很敏感。
上面對包含許多浮點加法程式碼差勁效能的解釋,基於系統化的實驗,但沒有確鑿的證據。我的理論基於以下的觀察:
- 對所有生成去往埠0或埠1,但僅有一個執行單元的μop的指令,都能觀察到這個現象。
- 對僅能去往其中一個埠的μop沒有觀察到這個現象。例如,在ADDPS指令被MULPS替代時,這個問題消失了。
- 對能去往兩個埠且有可以同時執行兩個μop的雙倍執行單元的μop,觀察不到這個現象。
通過兩個巢狀迴圈進行測試,其中內層迴圈包含許多浮點加法,外層迴圈測量內層迴圈使用的時鐘週期數。內層迴圈的時鐘週期數不總是恆定,通常依照一個週期模式變化。週期依賴於程式碼裡的小細節。已經觀察到高達5與9的週期。這些週期不能被我能提出的其他任何理論所解釋。
當然所有的測試例子受到埠執行吞吐率的限制,而不是執行時延。
關於哪個μop去往哪個埠與執行單元的資訊,是通過飽和一個特定埠或執行單元的實驗來獲取的。
7.12. 回收
在PM上的回收站像PPro,P2及P3上那樣工作。每時鐘週期,回收站可以處理3個融合μop。被採用的跳轉僅能在回收站3個工位中的第一個裡回收。如果一個被採用的跳轉恰好進入其他工位,回收站將暫停一個時鐘週期。因此,小迴圈中融合μop數最好能被3整除。細節參考第71頁。
7.13. 暫存器的部分訪問
PM可以將暫存器的不同部分儲存在不同的臨時暫存器中,以消除虛假的依賴性,例如:
; Example 7.6. Partial registers
mov al, [esi]
inc ah
這裡,第二條指令不需要等待第一條指令完成,因為AL與AH可以使用不同的臨時暫存器。在這些μop被回收時,AL與AH被儲存到永久EAX暫存器中它們各自的部分。
在寫入一個暫存器部分,後接一個從整個暫存器讀的時候,出現一個問題:
; Example 7.7. Partial register problem
mov al, 1
mov ebx, eax
在PM型號9上,從整個暫存器(EAX)讀必須等待,直到部分暫存器(AL)被回收,且AL中的值與永久暫存器EAX的餘下部分合並完成。這稱為部分暫存器暫停。在PM型號9上,部分暫存器暫停與PPro上的相同。細節參考第71頁。
在更新的PM型號D上,通過插入將暫存器各部分整合起來的額外μop,解決了這個問題。我假設這些額外的μop在ROB-讀階段產生。在上面的例子中,ROB-讀將生成一個在MOV EBX, EAX指令前,將AL與EAX餘下部分合併到一個臨時暫存器的額外μop。在ROB-讀階段,這需要1或2個額外時鐘週期,不過這小於之前處理器上部分暫存器暫停5-6時鐘週期的代價。
在PM型號M上生成額外μop的情形與較早處理器上產生部分暫存器暫停的情形相同。寫入AH,BH,CH,DH的高8位產生兩個額外μop,而寫入一個暫存器的低8位或16位部分,產生一個額外μop。例如:
; Example 7.8a. Partial register access
mov al, [esi]
inc ax ; 1 extra uop for read ax after write al
mov ah, 2
mov bx, ax ; 2 extra uops for read ax after write ah
inc ebx ; 1 extra uop for read ebx after write ax
在ROB-讀中防止額外μop與暫停的最好方式是避免混用暫存器大小。上面例子可以這樣改進:
; Example 7.8b. Partial register problem avoided
movzx eax, byte ptr [esi]
inc eax
and eax, 0ffff00ffh
or eax, 000000200h
mov ebx, eax
inc ebx
避免這個問題的另一個方式是通過XOR自己將整個暫存器清零:
; Example 7.8c. Partial register problem avoided
xor eax, eax
mov al, [esi]
inc eax ; No extra uop
處理器知道暫存器XOR自己是清零。暫存器中一個特殊的標記記住暫存器的高位是0,因此EAX = AL。即使在迴圈中,這個標記也被記住:
; Example 7.9. Partial register problem avoided in loop
xor eax, eax
mov ecx, 100
LL: mov al, [esi]
mov [edi], eax ; No extra uop
inc esi
add edi, 4
dec ecx
jnz LL
通過暫存器清零來防止額外μop的規則與之前處理器上防止部分暫存器暫停的規則相同。細節參考第71頁。
(文獻:Performance and Power Consumption for Mobile Platform Components Under Common Usage Models. Intel Technology Journal, vol. 9, no. 1, 2005)。
標記的部分訪問暫停
不幸,PM不會產生額外的μop來防止標記暫存器上的暫停。因此,在一條修改標記暫存器部分的指令後,讀標記暫存器有一個4-6時鐘週期的暫停。例如:
; Example 7.10. Partial flags stalls
inc eax ; Modifies zero flag and sign flag, but not carry flag
jz L1 ; No stall, reads only modified part
jc L2 ; Stall, reads unmodified part
lahf ; Stall, reads both modified and unmodified bits
pushfd ; Stall, reads both modified and unmodified bits
可以通過ADD EAX, 1(修改所有標記)或LEA EAX, [EAX+1](不修改標記)替換INC EAX,避免上面的暫停。避免依賴INC或DEC不改變進位標記事實的程式碼。
在一條偏移數不是1的偏移指令後讀標記時,也會有一個部分標記暫停:
; Example 7.11. Partial flags stalls after shift
shr eax, 1
jc l1 ; No stall after shift by 1
shr eax, 2
jc l2 ; Stall after shift by 2
test eax, eax
jz l3 ; No stall because flags have been rewritten
部分標記暫停的細節參考第74頁。
7.14. 寫轉發暫停
在PM中,寫轉發暫停與之前處理器相同。細節參考第74頁。
; Example 7.12. Store forwarding stall
mov byte ptr [esi], al
mov ebx, dword ptr [esi] ; Stall
7.15. PM中的瓶頸
在優化一段程式碼時,找出控制執行速度的限制因素是重要的。調整錯誤的因素很可能沒有什麼效果。在下面段落中,我將解釋每個可能的限制因素。你必須依次考慮每個因素來確定哪一個是最窄的瓶頸,集中精力在這個因素上,直到它不再是最窄的瓶頸。正如以前解釋的那樣,你必須專注於程式最關鍵的部分——通常是最裡層迴圈。
記憶體訪問
如果程式正在訪問大量的資料,如果資料分散在記憶體的各處,那麼將有許多資料快取不命中。訪問非快取資料是如此地耗時,所有其他優化考慮都不重要。快取被組織為64位元組對齊行。如果一個64位元組對齊塊中的一個位元組被訪問,可以確定所有64位元組將被載入1級資料快取,且可以沒有額外代價地訪問。為了提升快取,建議用在程式相同部分的資料儲存在一起。可以把大陣列與資料結構邊界對齊到64位元組。如果沒有足夠的暫存器,在棧上儲存區域性變數。PM有四個寫埠。超過四個緊挨的寫會降低處理幾個時鐘週期,特別是如果同時還有記憶體讀。在PM上記憶體的非臨時寫是高效的。如果不預期很快讀同一快取行,可以使用MOVNTI,MOVNTQ與MOVNTPS用於分散記憶體寫。
Core Solo/Duo有一個改進的,預測未來記憶體讀的資料預取機制。
指令獲取與解碼
如果致力於每時鐘週期3個μop,應該根據4-1-1規則組織指令。記住4-1-1模式在IFETCH邊界被破壞。避免帶有超過一個字首的指令。避免在32位模式下帶有16位立即數的指令,參考第78頁。
微操作融合
μop融與棧引擎使得每個μop獲取更多資訊成為可能。如果每時鐘週期3個μop或解碼限制是瓶頸,這是一個優勢。浮點暫存器允許讀-修改指令的μop融合,但XMM暫存器不允許。如果可以利用μop融合,對浮點操作使用浮點暫存器,而不是XMM暫存器。
暫存器讀暫停
如果一段程式碼有超過3個經常讀但很少寫的暫存器時,小心暫存器讀暫停。參考第84頁。
在使用指標與絕對地址間有一個權衡。面向物件程式碼通常通過棧框指標以及this指標訪問大多數資料。指標暫存器是暫存器讀暫停的可能來源,因為它們被經常讀,但很少寫。不過,使用絕對地址而不是指標,有其他劣勢。它使程式碼變長,降低快取的效率,而且IFETCH邊界問題變多。
執行埠
非融合μop應該均勻地在5個執行埠分發。在有少數記憶體操作的程式碼裡,埠0與1很可能是瓶頸。如果不會導致快取不命中,通過將暫存器到暫存器移動以及暫存器與立即數間移動的指令,替換為暫存器與記憶體間移動的指令,可以將某些負荷從埠0及1轉移到埠2。
在PM上,使用64位MMX暫存器的指令不比使用32位整數暫存器的指令低效。如果整數暫存器用完,對整數計算可以使用MMX暫存器。XMM暫存器稍微低效一些,因為對讀-修改指令它們不使用μop融合。MMX與XMM指令比其他指令稍長。如果解碼是瓶頸,這可能增大了IFETCH邊界的問題。記住在同一個程式碼中,你不能同時使用浮點暫存器與MMX暫存器。
由於第85頁討論的設計問題,有許多浮點加法的程式碼很可能在埠0與1暫停。
棧同步μop去往埠0或1。有時可以通過PUSH及POP指令替換相對於棧指標的MOV指令,減少這樣μop的數量。細節參考第81頁。
部分暫存器訪問產生額外的μop,參考第87頁。
執行時延與依賴鏈
在PM上,執行單元有適度地的時延,許多操作比P4上要快。
當代碼有帶有慢指令的長依賴鏈時,效能很可能受到執行時延的限制。
避免長依賴鏈以及在依賴鏈中避免記憶體立即數。依賴鏈不會被暫存器與自身的XOR或PXOR打破。
部分暫存器訪問
避免混合使用暫存器大小,避免使用AH,BH,CH,DH的高8位。在修改某些標記位的指令或者偏移與旋轉後,小心部分標記暫停。參考第89頁。
分支預測
比其他處理器,PM裡的分支預測更加先進。可以完美預測重複最多64次的迴圈。目標不停變換的間接跳轉與呼叫也可以被預測,只要它們遵循一個規則的模式,或者與前面的分支有一一對應的關係。不過分支目標緩衝(BTB)比其他處理器小得多。因此,應該避免不必要的跳轉,以減少BTB上的負荷。如果大多數處理器時間花在一小塊具有相對少分支的程式碼上,分支預測將是良好的。但如果處理器時間分散在具有許多分支且沒有特別的熱點的程式碼上時,分支預測將是不良的。參考第18頁。
回收
在具有許多分支的小迴圈中,已採用分支的回收會是瓶頸。參考第71頁。