【Microarchitecture of Intel and AMD CPU】 9 Sandy Bridge and Ivy Bridge pipeline 【9.7】

9.7 Stack Engine

Sandy Bridge 設計了獨立的stack engine,工作方式與之前的處理器中的Stack Engine相同。


引入7.7 Stack Engine對stack的介紹

棧指令,比如push,pop,call 和ret都會修改棧指針。例如,指令push EAX將會在P3 處理器上產生3條uop,兩條用於存儲EAX,一條用於將ESP減4。在PM中,相同的指令只會產生一條uop。兩條存儲uops通過uop fusion的方式合併爲一條,ESP-4則是通過一個專門用於計算ESP的加法器執行的,成爲stack engine。Stack Engine 被放置在指令被譯碼的階段,在亂序執行core之前。Stack Engine可以每週期處理3條加法。因此,沒有指令需要在棧操作之後等待棧指針的更新。使用這個技術的併發症就是ESP的值可能在亂序執行core中讀取或者修改。需要一個特殊的機制來同步stack engine和亂序執行核的棧指針的值。ESPp的真實邏輯值通過一個亂序執行核內的32bit的ESPo寄存器,或者是寄存器File 以及有符號存儲在stack engine中的ESPd寄存器來獲得。

ESPp=ESPo+ESPd

譯者注:我們再來回顧一下爲什麼早期的一條push指令需要3條uop

push EAX

1)將ESP-4;

2)讀取EAX的值,將EAX的值存入ESP-4地址

Stack Engine 將偏移值ESPd作爲偏移放入每個棧操作的一個地址字節中,因此它可以在port2或者port3的地址計算單元中加到ESPo上。ESPd的值不能被放到每一個可能使用棧指針的uop中,只能是那些up由push,pop,call和ret產生的uop中。如果stack engine遇到了其他的使用ESP的uop,或者是ESPd的值非0。那麼後面的uop可以使用ESPo作爲棧指針的真實的值。這條操作棧指針的指令之後的同步ESP的指令已經被譯碼,並且不會影響譯碼器。

同步機制可以被下面的簡單的例子闡述:

; Example 7.3. Stack synchronization
push eax
push ebx 
mov ebp, esp
mov eax, [esp+16]

這個指令序列將會產生4條uops。假設啓動時ESPd爲0,第一條push 指令將會產生1條uop,將EAX的指寫入[ESPo-4]的內存中並且將ESPd-4。第二條Push指令將會產生另一條uop,將EAX的指寫入[ESPo-8]的內存中並且將ESPd-8。當stack engine接收到來自譯碼器的mov ebp, esp的微指令,他將會插入一個同步uop,將-8加到亂序執行核內部的ESPo上。在同一時刻,它會將ESPd設置爲0. 同步uop在mov ebp, esp之前進入流水線,所以mov指令可以將ESPo認爲是真實的ESPp的值。在最後一條指令,mov eax, [esp+16],也需要ESP的值,但是我們不會再增加同步操作,因爲ESPd的值在此刻爲0,所以沒有必要進行同步操作。

需要同步ESPo的指令包括所有的使用ESP作爲源操作數或者目標操作數,比如mov eax esp,mov esp eax和add esp,4 也包括那些使用esp作爲指針的指令,比如mov eax,[esp+16]。如果只是寫入esp看上去是沒必要插入同步指令。如果是將ESPd設置爲0,那麼是沒必要的。但是如果想要區分mov esp,eax和add esp,eax就會將邏輯複雜化。

同步uop在ESPd將要溢出時,也會進行。8bit的有符號ESPd將會overflow,在連續進行32次push eax或者64次push ax的指令時。在大多數的case中,我們將會在29條push或者是call 指令時插入同步uop,以防止下一個週期連續進入3個譯碼器的都是push指令而導致overflow。在同步uop之前最多31條uop,會發生在最後三條push指令在同一個週期進入的case中。POP和RET指令同理。實際上你可以使用比PUSH更多的POP指令,因爲esp value存儲的形式是-ESPd。而最小的有符號數是-128,最大的是127。

【譯者注】CALL和RET指令的介紹

CALL 指令調用一個過程,指揮處理器從新的內存地址開始執行。過程使用 RET(從過程返回)指令將處理器轉回到該過程被調用的程序點上。
從物理上來說,CALL 指令將其返回地址壓入堆棧,再把被調用過程的地址複製到指令指針寄存器。當過程準備返回時,它的 RET 指令從堆棧把返回地址彈回到指令指針寄存器。32 位模式下,CPU 執行的指令由 EIP(指令指針寄存器)在內存中指岀。16 位模式下,由 IP 指出指令。
調用和返回示例
假設在 main 過程中,CALL 指令位於偏移量爲 0000 0020 處。通常,這條指令需要 5 個字節的機器碼,因此,下一條語句(本例中爲一條 MOV 指令)就位於偏移量爲 0000 0025 處:
     main PROC
00000020  call MySub
00000025  mov eax,ebx
然後,假設 MySub 過程中第一條可執行指令位於偏移量 0000 0040 處:
   MySub PROC
00000040 mov eaxz edx
      .
      .
      ret
   MySub ENDP
當 CALL 指令執行時如下圖所示,調用之後的地址(0000 0025)被壓入堆棧,MySub 的地址加載到 EIP。
執行 MySub 中的全部指令直到 RET 指令。當執行 RET 指令時,ESP 指向的堆棧數值被彈岀到 EIP(如下圖所示,步驟 1)。在步驟 2 中,ESP 的數值增加,從而指向堆棧中的前一個值(步驟 2)。

同步uop可以在port0 和 por1的兩個整數計算單元中的任何一個進行計算。他們的retire與其他的uop的退休方式類似。PM有一個恢復表用於在分支預測錯誤的情況下恢復,以解耦stack engine帶來的影響。

下面的例子說明了同步uop是如何在典型的程序流中生成的:

; Example 7.4. Stack synchronization
 push 1
 call FuncA
 pop ecx
 push 2
 call FuncA
 pop ecx
...
...
FuncA:
 push ebp
 mov ebp, esp ; Synch uop first time, but not second time
 sub esp, 100
 mov eax, [ebp+8]
 mov esp, ebp
 pop ebp
 ret

MOV EBP, ESP 指令在FuncA函數中在push、call、和push指令之後。如果ESPd在一開始是0,那麼最後它會變成-12。在執行MOV EBP,ESP指令之前,我們需要一個同步uop。SUB ESP,100 和MOV ESP EBP不需要同步指令,因爲在同步指令之後就沒有push 或者pop指令了。在此之後,我們有一個指令序列POP/RET/POP/PUSH/CALL/PUSH,而後我們再次遇到MOV EBP,ESP(第二次調用函數時)。ESPd此時已經計數到12然後又回到了0(即三次累加和三次遞減)。所以第二次的時候我們不需要同步指令。如果POP ECX 被ADD ESP,4所取代,那麼我們需要在執行ADD ESP,4之前需要一個同步指令,在第二次遇到MOV ESP,ESP時也是需要。如果我們將POP ECX/PUSH 2替換成MOV DWORD [ESP],2 時也會發生相同的情況(即需要同步uop)。但是如果替換成MOV DWORD [EBP],就不需要了。

我們可以制定一個規則,以預測同步指令會在插入以下指令流的情況下會產生:

  1. 使用如下的stack engine的指令:push,pop,call,ret,除了RET n.
  2. 在亂序核中使用棧指針。比如指令使用ESP作爲源、目標或者指針以及CALL FAR,RETF,ENTER
  3. 既使用stack engine,又使用亂序核的棧指針的指令,比如:PUSH ESP, PUSH [ ESP] +4, POP [ESP+8], RET n.
  4. 總是同步ESPo的指令,PUSHF(D),POPF(D),PUSHA(D),POPA(D),LEAVE.
  5. 無論如何都不喚醒棧指針的指令

從上面1-5的一系列的指令都不會產生任何的同步指令,除非ESPd要溢出了。從2-5的一系列的指令不會產生任何的同步指令。如果來自2類的指令在1類的指令之後,那麼會產生一個同步uop,除非ESPd是0。來自3的指令在大多數的情況下都會產生同步指令。來自4類的指令會產生一個來自decoder的同步uop,而不是來自stack engine。即使是ESPd=0。

你可能想要使用這個規則以減少同步指令的數目,以避免那些每週期3條uop是瓶頸 和 執行單元0和1都已經滿負荷的情況。你不需要在意這些指令,如果瓶頸在其他地方。

【譯者注】

總的來說,stack engine位於decoder和亂序執行核之間。

正常的push EAX 這樣的指令,是需要ALU對ESP的值計算後,再調用store(如果是pop會是load),那麼對應的uop就會三條(如果採用了microuop fusion技術,會是兩條)。而如果出現連續的push指令,如果沒有采用stack engine,按照傳統的寄存器重命名技術,會存在讀後寫的依賴,即需要讀取ESP的值後+/- 4,再存回ESP,以便下一條push仍然可以使用準確的值,而導致性能的降低。

stack engine的思想就是一旦檢測到push,pop指令,既然他們都是固定的+4/-4,那麼直接就只亂序執行核之前把偏移計算好,那麼就只需要store、load uop了(偏移和基準ESP的計算在AGU中執行),這樣將三條uop減少到1條uop,而且解除了對ESP的依賴。

另一方面,我們知道在亂序執行核內,會存在物理架構寄存器即physical register file,用於存放寄存器的值。而如果是mov eax, [esp+16]這樣的指令,是需要獲取真實的esp的值的。所以Stack Engine會檢測decoder發出的指令流中是否會使用ESP作爲源/目標寄存器,如果存在這樣的指令,就在該指令之前加上一條同步ESP的指令,更改physical register file 中的ESP的值,這樣後續的指令流可以拿到真實的ESP了。

 


翻譯自【Microarchitecture of Intel and AMD CPU  An optimization guide for assembly programmers and compiler makers】

歡迎關注我的公衆號《處理器與AI芯片》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章