Intel, AMD及VIA CPU的微架構(24)
8.6. 棧引擎
Core2與Nehalem有一個與PM工作方式相同的專用棧引擎,如第81頁所述,它對更大的流水線做了必要的調整。
PUSH,POP,CALL與RET指令對棧的修改由一個特殊的棧引擎完成,在流水線中,它緊跟著解碼步驟,在亂序核之前。這把流水線從修改棧指標的μop負擔中解放出來。這個機制節省了棧指標的兩次拷貝:一次在棧引擎,另一次在暫存器檔案與亂序核。這兩個棧指標可能需要同步,如果PUSH,POP,CALL與RET指令序列後跟一條直接讀或修改棧指標的指令,比如ADD ESP, 4或者MOV EAX, [ESP+8]。棧引擎在需要同步兩個棧指標的情形裡插入一個額外的棧同步μop。更詳細解釋,參考第81頁解釋。
通過不混用在棧引擎修改棧指標的指令與在亂序執行單元中訪問棧指標的指令,棧同步μop有時可以避免。僅包含這兩個類別之一指令的序列不需要同步μop,但混用這兩個類別的序列需要這些額外的μop。例如,在一個函式呼叫後的ADD ESP, 4指令被POP ECX替換是有好處的,如果之前的指令是RET且下一條觸碰棧指標的指令是PUSH或CALL。
在一個關鍵函式中完全避免棧同步μop是可能的,如果所有的函式引數都在暫存器中傳遞,且所有的區域性變數都儲存在暫存器中,或者被PUSH及POP。在64位Linux呼叫慣例中,這最實用。這時棧任何必要的對齊都可以通過假的PUSH指令來完成。
8.7. 暫存器重新命名
所有的整形、浮點、MMX、XMM、標記與段暫存器都可以重新命名。浮點控制字也可以。
暫存器重新命名由圖6.1中顯示的暫存器別名表(RAT)以及重排緩衝(ROB)控制。來自解碼器與棧引擎的μop通過一個佇列去往RAT,然後到達ROB-讀與保留站。RAT每時鐘週期可以處理4個μop。RAT每時鐘週期可以重新命名4個暫存器,在一個時鐘週期裡,它甚至可以重新命名同一個暫存器四次。
8.8. 暫存器讀暫停
Core2與Nehalem,與PM及更早的處理器一樣,受相同的暫存器讀暫停的影響,如第65頁所述。在Core2與Nehalem上,永久暫存器檔案有三個讀埠用於讀指令運算元。
ROB-讀階段每時鐘週期可以從永久暫存器檔案讀不超過3個不同的暫存器。這適用於所有通用暫存器,棧指標,標記暫存器,浮點控制暫存器,MMX暫存器與XMM暫存器。在Core上,一個XMM暫存器算作一個,而在之前的處理器上,算作兩個64位暫存器。
三個暫存器讀埠的前兩個可以讀用於指令運算元的暫存器、基址暫存器及索引暫存器。第三個讀埠在Core2上僅能讀用作索引指標的暫存器。在Core Nehalem上,所有這三個讀埠都可用於任何運算元。在同一個時鐘週期裡,可以無限次讀同一個暫存器,不會導致暫停。
最近被寫過的暫存器可以直接從ROB讀出,如果它們還沒經過ROB-回寫階段。可以直接從ROB讀的暫存器不需要暫存器檔案上的讀埠。我的測量顯示一個μop從ROB-讀階段到ROB-回寫階段大約需要5個時鐘週期。這意味著在被修改後的5個時鐘週期內,讀一個暫存器沒有問題。考慮到每時鐘週期4個μop吞吐率,可以假定,在被修改後的20個μop內,除非與此同時流水線因任何原因暫停,可以無需使用暫存器讀埠來讀一個暫存器。
一個非融合μop可以包含最多2個暫存器讀,而一個融合μop可以包含最多3個暫存器讀。例如,指令ADD EAX, [EBX+ECX]讀暫存器EAX,EBX與ECX,然後寫暫存器EAX。在一個時鐘週期裡,解碼器向ROB-讀步驟最多可以傳送4個融合μop。因此,在一個μop四件套中,最大暫存器讀數目是12。在所有暫存器都在永久暫存器檔案中的最壞情形裡,ROB-讀階段可能需要4個時鐘週期來讀這12個暫存器。
; Example 8.1a. Register read stall on Core2
L: mov eax, [esi+ecx]
mov [edi+ecx], ebx
add ecx, 4
js L
這個迴圈有一個暫存器讀暫停,因為在迴圈裡讀三個暫存器,但是不寫:ESI,EDI與EBX。ECX不需要讀埠,因為它最近被修改了。有三個暫存器讀埠,但在Core2上,第三個讀埠僅能用於索引暫存器,這三個只讀暫存器都沒用作索引暫存器。注意指令程式碼的SIB位元組區分基址與索引暫存器。在例子8.1a中ESI與EDI是基址暫存器,而ECX是索引暫存器。索引暫存器可以有一個比例因子,而基址暫存器不行。
稍微修改一下程式碼,可以使EDI成為一個索引暫存器,這樣就可以使用第三個讀埠:
; Example 8.1b. Register read stall removed
L: mov eax, [ecx+esi*1]
mov [ecx+edi*1], ebx
add ecx, 4
js L
這裡,對ESI及EDI應用比例因子*1,確保彙編器把ESI與EDI用作索引暫存器,而不是ECX。現在可由第三個暫存器讀埠讀ESI或EDI,因此暫停消除了。在Core2上,例子8.1b每迭代需要1個時鐘週期,而例子8.1a每迭代需要2個時鐘週期。
預測哪些μop一起進入ROB-讀階段是困難的。μop依次到來,但你不知道每個四件套在哪裡開始,除非解碼器已經暫停了。因此,如果任何4個連續μop讀超過2個或3個暫存器,且這些暫存器最近都沒有被寫入,會發生暫存器讀暫停。
消除暫存器讀暫停通常要求某些試驗。Core2與Nehalem有可用來檢測暫存器讀暫停的效能監控計數器。
在像MOV EAX, [EBX]這樣的指令中將一個基址暫存器變為索引暫存器是可能的。但僅當試驗結果表明可以防止一次暫存器讀暫停時,才應該這樣做,因為指令MOV EAX, [EBX*1]比MOV EAX, [EBX]長5個位元組(4位元組用於基址0,1個SIB位元組)。
消除暫存器讀暫停的其他方法是儘量減少經常讀、但很少寫入的暫存器數目,用常量或記憶體運算元替換隻讀暫存器,組織程式碼限制同一個暫存器寫入與後續讀之間的距離。棧指標,棧幀指標及this指標是經常讀但很少修改的暫存器的常見例子。
8.9. 執行單元
Core2的執行單元在之前處理器基礎上擴充套件了許多。有6個執行埠。埠0,1與5用於算術與邏輯操作(ALU),埠2用於記憶體讀,埠3用於寫地址計算,埠4用於記憶體寫資料。這給出了每時鐘週期最多6個非融合μop的吞吐率。
所有執行埠都支援128位向量。大多數ALU操作有1時鐘週期的時延。不同的單元在下面的表8.1中列出。所有三個ALU埠可以處理128位移動及布林操作。所有三個埠可以處理通用暫存器上的加法。埠0與5還可以處理整數向量加法。
對整數乘法與浮點乘法有獨立的單元。在埠1上的整數乘法器是完全流水線化的,時延為3,吞吐率為每時鐘週期一個完整向量操作。在埠0上的浮點乘法器,對單精度時延為4,對雙精度與長雙精度的時延為5。在Core2上,除了長雙精度,浮點乘法器的吞吐率是每時鐘週期1個操作,浮點加法器連線到埠1。它的時延是3且完全流水線化。
整數除法使用浮點除法單元,這是僅有的沒有流水線化的單元。埠5上的跳轉單元處理所有的跳轉與分支操作,包括巨集融合的比較-分支操作。
連線到埠0與1的浮點單元處理所有浮點棧暫存器上的操作,以及XMM暫存器上的大多數浮點計算。Core2不區分整數與浮點操作,而Core Nehalem區分。例如,在Core2上,MOVAPS,MOAVPD與MOVDQA是相同的,都可由整數單元執行。在Core Nehalem上,MOVAPS與MOVAPD不同於MOVDQA,僅能在埠5上執行。在Core2上浮點XMM移動,布林及大多數浮點混排操作由整數單元完成,但在Core Nehalem上使用埠5上的一個專用單元。
算術/邏輯執行單元被良好地分佈在埠0,1與5之間。這使得每時鐘週期執行3個向量指令成為可能,例如埠0上的浮點向量乘法,埠1上的浮點向量加法,及埠5上的浮點移動。
執行埠 |
執行單元 |
子單元 |
最大資料大小,位元 |
時延,時鐘週期 |
處理器 |
0 |
int |
move |
128 |
1 |
|
1 |
int |
move |
128 |
1 |
|
5 |
int |
move |
128 |
1 |
|
0 |
int |
add |
128 |
1 |
|
1 |
int |
add |
64 |
1 |
僅Core 2 |
5 |
int |
add |
128 |
1 |
|
0 |
int |
Boolean |
128 |
1 |
|
1 |
int |
Boolean |
128 |
1 |
|
5 |
int |
Boolean |
128 |
1 |
|
1 |
int |
multiply |
128 |
3 |
|
0 |
int |
shift |
128 |
1 |
僅Core 2 |
1 |
int |
shift |
128 |
1 |
僅Nehalem |
5 |
int |
shift |
64 |
1 |
僅Core 2 |
0 |
int |
pack |
128 |
1 |
|
1 |
int |
pack |
64 |
1 |
僅Nehalem |
5 |
int |
pack |
128 |
1 |
僅Nehalem |
1 |
int |
shuffle |
128 |
1 |
僅Nehalem |
5 |
int |
shuffle |
128 |
1 |
|
5 |
int |
jump |
64 |
1 |
|
0 |
float |
fp stack move |
80 |
1 |
|
1 |
float |
fp add |
128 |
3 |
|
0 |
float |
fp mul |
128 |
4-5 |
|
0 |
float |
fp div and sqrt |
128 |
> 5 |
|
0 |
float |
fp convert |
128 |
1 |
|
1 |
float |
fp convert |
128 |
3 |
|
5 |
float |
fp mov, shuffle |
128 |
1 |
僅Nehalem |
5 |
float |
fp boolean |
128 |
1 |
僅Nehalem |
2 |
int |
memory read |
128 |
2 |
|
3 |
store |
store address |
64 |
1 |
|
4 |
store |
store data |
128 |
3 |
|
表8.1. Core2與Nehalem中的執行單元
整形向量操作的時延與通用暫存器中的操作相同。這使得在耗盡通用暫存器時,對簡單的整數操作使用MMX暫存器或XMM暫存器,很方便。雖然,支援向量操作的執行單元要少些。
Core2上的資料旁路時延
在Core2上,當整數單元的一個輸出μop用作浮點單元的輸入時,有一個時鐘週期的額外時延,反之亦然。這展示在下面例子中。
; Example 8.2a. Bypass delays in Core 2
.data
align 16
signbits label xmmword ; Used for changing sign
dq 2 dup (8000000000000000H) ; Two qwords with sign bit set
.code
movaps xmm0, [a] ; Unit = int, Latency = 2
mulpd xmm0, xmm1 ; Unit = float, Latency = 5 + 1
xorps xmm0, [signbits] ; Unit = int, Latency = 1 + 1
addpd xmm0, xmm2 ; Unit = float, Latency = 3 + 1
在例子8.2a中,在整數與浮點單元間來回移動資料有3個額外時延。這個程式碼可以通過重排指令,減少整數與浮點單元間切換次數,來改進:
; Example 8.2b. Bypass delays in Core 2
.code
movaps xmm0, [a] ; Unit = int, Latency = 2
xorps xmm0, [signbits] ; Unit = int, Latency = 1
mulpd xmm0, xmm1 ; Unit = float, Latency = 5 + 1
addpd xmm0, xmm2 ; Unit = float, Latency = 3
在例子8.2b中,在乘上XMM1前,我們改變了XMM0的符號。這將整數與浮點單元間的遷移次數從3次降為1次,總時延降低了2個時鐘週期。(我們使用MOVAPS與XORPS,而不是MOVAPD與XORPD,因為前者功能相同,但更短)。
讀/寫單元與整數單元緊密連線,因此在整數單元與讀/寫單元間傳輸資料沒有額外的時延。當從記憶體(讀單元)向浮點單元傳輸資料時有1個時鐘週期的時延,但在從浮點單元向記憶體(寫單元)傳輸資料時,沒有額外的時延。在手冊4“指令表”中,在合適的地方列出了執行單元。
Nehalem上的資料旁路時延
在Nehalem上,執行單元被分為5個“域(domain)”:
- 處理所有通用暫存器中操作的整數域。
- 處理向量暫存器中整數操作的整數向量(SIMD)域。
- 處理XMM與x87暫存器中浮點操作的FP域。
- 處理所有記憶體讀的讀域。
- 處理所有記憶體寫的寫域。
當一個域操作的輸出被用作另一個域的輸入時,有一個1或2個時鐘週期的額外時延。這些稱為旁路時延,列在表8.2中。
|
目標域 |
|||
來源域 |
integer |
integer vector |
FP |
store |
integer |
0 |
1 |
2 |
0 |
integer vector |
1 |
0 |
2 |
1 |
FP |
2 |
2 |
0 |
1 |
load |
0 |
1 |
2 |
0 |
表8.2. Nehalem中的旁路時延
對整數向量域及FP域,幾條XMM指令分別有幾個版本。例如,對暫存器-暫存器移動,在整數向量域中有MOVDQA,在FP域中有MOVAPS與MOVAPD。使用錯誤域中的指令,額外時延相當可觀,如下例所示。
; Example 8.3a. Bypass delays in Nehalem
movaps xmm0, [a] ; Load domain, Latency = 2
mulps xmm0, xmm1 ; FP domain, Latency = 4 + 2
pshufd xmm2, xmm0, 0 ; int vec. dom., Latency = 1 + 2
addps xmm2, xmm3 ; FP domain, Latency = 3 + 2
pxor xmm2, xmm4 ; int vec. dom., Latency = 1 + 2
movdqa [b], xmm1 ; Store domain, Latency = 3 + 1
在這個例子中,通過儘可能僅使用同一個域中的指令,可以降低旁路時延:
; Example 8.3b. Bypass delays in Nehalem
movaps xmm0, [a] ; Load domain, Latency = 2
mulps xmm0, xmm1 ; FP domain, Latency = 4 + 2
movaps xmm2, xmm0 ; FP domain, Latency = 1 + 0
shufps xmm2, xmm2, 0 ; FP domain, Latency = 1 + 0
addps xmm2, xmm3 ; FP domain, Latency = 3 + 0
xorps xmm2, xmm4 ; FP domain, Latency = 1 + 0
movaps [b], xmm1 ; Store domain, Latency = 3 + 1
在例子8.3b中,以SHUFPS替換PSHUFD,要求額外一個時延為1的MOVAPS(如果XMM0中值在後面需要),但它節省了4個時鐘週期的旁路時延。以XORPS替換PXOR是顯而易見的,因為這兩條指令功能相同。以MOVAPS替換MOVDQA不改變時延,但在將來的處理器上可能會。
這裡重要的結論是,在Nehalem上,使用錯誤型別的XMM指令會有時延形式的懲罰。在之前的Intel處理器上,對預定型別以外的運算元使用移動或混排指令沒有懲罰。
在時延是瓶頸的長時延鏈中,旁路時延是重要的,但在吞吐率比時延更受關注的地方不是。整數向量版本的移動及布林指令的吞吐率為每時鐘週期3條,而FP版本的移動與布林指令僅有1時鐘週期1條的吞吐率,使用整數向量版本事實上會提升吞吐率。
在錯誤的資料型別上使用讀、寫指令仍然沒有額外的旁路時延。例如,在整數資料上使用MOVHPS讀或寫一個XMM暫存器的高半部是方便的。
混用不同時延的μop
當不同時延的μop被髮布到相同的執行埠時,會有問題。例如:
; Example 8.4. Mixing uops with different latencies on port 0
mulpd xmm1,xmm2 ; Double precision multiply has latency 5
mulps xmm3,xmm4 ; Single precision multiply has latency 4
假定時延為5的雙精度乘法在時刻T開始,在時刻T+5結束。如果我們嘗試在時刻T+1啟動時延為4的單精度乘法,那麼這也將在時刻T+5結束。這兩條指令都使用埠0。每個執行埠僅有一個回寫埠,一次僅能處理一個結果。因此,不可能在同一時間結束兩條指令。排程器將預測並通過將後來的指令推遲到時刻T+2,因而在時刻T+6結束,來避免這個回寫衝突。代價是一個浪費的時鐘週期。
當兩個或更多被髮布到相同執行埠的μop有不同的時延時,會出現這種衝突。在每個埠,每時鐘週期一個μop的最大吞吐率,僅在所有去往相同埠的μop有相同的時延時,才能獲得。在浮點乘法的例子中,可以通過對所有浮點計算使用相同的精度,或者分開不同精度的浮點乘法,而不是混用它們,使吞吐量最大。
設計者嘗試通過使μop時延標準化來弱化這個問題。埠0僅可以處理時延1或≥ 4的μop。埠1僅可以處理時延1或3的μop。埠5僅可以處理時延1的μop。埠2,3及4處理記憶體操作,幾乎沒有別的。在任何執行單元中,沒有使用2個時鐘週期的μop。(像MOVD EAX, XMM0這樣的指令有時延2,但是執行單元中1時鐘週期,以及單元間旁路時延的1個額外週期)。
混用時延的問題也會出現在埠1,但不那麼頻繁:
; Example 8.5. Mixing uops with different latency on port 1 (Nehalem)
imul eax, 10 ; Port 1. Latency 3
lea ebx, [mem1] ; Port 1. Latency 1
lea ecx, [mem2] ; Port 1. Latency 1
在例子8.5中,在IMUL μop兩個時鐘週期後,我們不能向埠1釋出最後的LEA μop,因為在Nehalem上,它們將在相同時間結束(Core2在埠0上有LEA)。在埠1上這個問題是罕見的,因為大多數可以去往埠1的單週期μop也可以去往埠0或5。
8.10. 回收
回收站看起來比PM高效。在Core2或Nehalem上,我沒有檢測到任何由於回收站瓶頸導致的時延。