[譯]Intel, AMD及VIA CPU的微架構(35)
17. AMD K8與K10流水線
17.1. AMD K8與K10處理器中的流水線
與Intel桌面處理器的原理相同,AMD微處理器基於亂序執行以及暫存器重新命名。
在流水線中,指令被儘可能晚、儘可能小地分解。在執行階段,每個讀-修改巨集指令被分解為一個讀及一個修改微指令,並在回收前合併為這個巨集操作。在AMD術語裡,巨集操作有點類似於Intel術語裡的融合微操作。K8微架構沒有比64或80位元大的執行單元,而K10微架構在浮點流水線裡有128位執行單元,因此在單個巨集指令裡可以處理128位XMM指令。
與Intel微架構最重要的區別是,AMD微架構包含3條並行流水線。就在獲取階段後,指令在3個流水線間分發。在簡單的情形裡,指令待在自己的流水線裡直到回收。
流水線的實際長度未知,但基於測得的分支誤預測懲罰為12時鐘週期,可以推斷出,它大約有12個階段。
下面列出的階段基於AMD公開的資料,以及由Chip Architect公佈的一個獨立分析。
- 指令獲取1。在K10上,每時鐘週期32位元組指令,在K7與K8上16位元組。
- 指令獲取2與分支預測。
- 挑選(Pick)/掃描。可緩衝最多7條指令。將3條指令分發給3條解碼器流水線。以下階段都被分為3條並行流水線。
- 解碼1。將指令程式碼分解為組成。
- 解碼2。確定輸入與輸出暫存器。
- 封裝。從解碼器生成的最多6個巨集操作被安排進3個執行流水線的3個巨集操作路線。
- 封裝/解碼。暫存器重新命名。從“整數將來檔案與暫存器檔案(Integer Future File and Register File)”讀整數暫存器。將整數巨集操作提交給3個整數流水線。將浮點巨集操作提交給浮點流水線。
整數流水線:
- 釋出。將巨集操作傳送到有3x8項的保留站。
- 排程。亂序排程巨集操作。讀-修改與讀-修改-寫巨集操作被分解為提交給算術邏輯單元(ALU),地址生成單元(AGU)及讀/寫單元的微操作。
- 執行單元與地址生成單元。3個整數流水線都有一個ALU與一個AGU。整數算術、邏輯與偏移操作在這個階段中執行。整數乘法僅在流水線0中處理,其他整數指令可由任一整數流水線處理。
- 資料快取訪問。提交一個讀或寫請求到資料快取。
- 資料快取響應。資料快取返回一個命中或不命中給讀操作。
- 回收。巨集操作依次回收。如果沒有記憶體操作,跳過資料快取階段。
浮點流水線:
- 棧對映。將浮點棧暫存器對映到虛擬暫存器。
- 暫存器重新命名。
- 釋出。排程器寫。
- 排程器。使用3x12項的保留站亂序排程指令。微操作是3個浮點執行單元中一個的目標。
- 暫存器讀。讀源運算元暫存器值。
- 執行單元。在浮點流水線中的3個執行單元被命名為FADD,FMUL與FMISC。這些單元專用於各自的目的。浮點執行單元完全流水線化為3個階段,以大於1的時延處理浮點操作。整數向量操作由浮點單元處理。而不是整數ALU。
- – 16. 可能與整數流水線共享的地址生成、快取訪問及回收的過程
浮點流水線比整數流水線長,因為棧對映、暫存器重新命名與暫存器讀的額外步驟。因為額外的暫存器讀步驟,浮點指令測得的最小時延是2時鐘週期。浮點指令的最大時延是4時鐘週期。
浮點流水線的長度難以測量,因為分支誤預測懲罰僅能測量整數流水線的長度。
你可能將流水線的結構想象為一個順序前端、一個亂序執行核,以及一個順序回收單元。不過,上面描繪的流水線線性形象有些誤導,因為某些過程並行進行,或多或少彼此獨立。地址生成需要幾個時鐘週期,可能在ALU操作之前開始。讀-修改與讀-修改-寫巨集操作被分解為去往不同單元的微操作,在不同的時間在亂序核裡。順序前端、分支預測單元、地址生成單元、讀寫單元、整數算術邏輯單元以及浮點單元都是有自己流水線的獨立結構。整數ALU單元與浮點單元可亂序執行操作。讀寫單元順序執行所有的記憶體讀,順序執行所有的記憶體寫,可以在一個後續寫之前執行讀。其他單元順序執行所有的操作。
根據我的測試,如果段基址是0,計算地址並在1級快取中讀這個地址需要3時鐘週期,如果段基址不是0,需要4時鐘週期。現代作業系統使用分頁而不是分段來組織記憶體。因此,在32位與64位作業系統中,可以假設段基址是0(除了通過FS或GS訪問的執行緒資訊塊)。在16位系統的保護模式以及真實模式中,段基址幾乎總是0。
要求超過兩個巨集操作的複雜指令是所謂的向量路徑指令。在一條解碼線路、一條重排緩衝線路等裡,這些指令獨佔使用所有3個工位,因此其他指令不能並行進行。巨集操作在流水線的3 ~ 5階段從微程式碼ROM生成。
K7處理器沒有雙指令(double Instruction)。對所有要求多個巨集操作,它使用向量路徑過程。否則,K7處理器的微架構非常類似於K8與K10的64位架構,如上面概括的。更早期的AMD處理器有不同的微架構,這裡不討論。
文獻:
AMD Athlon™ Processor x86 Code Optimization Guide, Feb. 2002.
AMD Software Optimization Guide for AMD64 Processors, 2005, 2008.
Fred Weber: AMD’s Next Generation Microprocessor Architecture. Oct. 2001.
Hans de Vries: Understanding the detailed Architecture of AMD's 64 bit Core, 2003. www.chip-architect.com 。
Yury Malich: AMD K10 Micro-Architecture. 2007. www.xbitlabs.com 。
AMD流水線的研究,在Andreas Kaiser與Xucheng Tang幫助下進行。
17.2. 指令獲取
在K10上,指令獲取器每時鐘週期可以從1級程式碼快取獲取32位元組程式碼。在K7與K8上,每時鐘週期可以獲取16位元組程式碼到一個32位元組緩衝。因此,在更舊的處理器上,如果程式碼包含許多長指令或跳轉,指令獲取會是一個瓶頸。2級快取的程式碼提交頻寬,對K10,測得平均4.22位元組每時鐘,對K8是2.56位元組每時鐘。
在K10上,獲取的包對齊32位元組,在K7與K8上對齊16位元組。這有程式碼對齊的隱含要求。關鍵例程項與迴圈項不應該在一個16位元組塊末尾附近開始。可以對齊關鍵項到16位元組,或至少確保在一個關鍵標籤後前3條指令中沒有16位元組邊界。
分支資訊儲存在程式碼快取中,分支目標緩衝用於獲取被預測分支後的程式碼。跳轉與被採用分支的吞吐率是每2時鐘週期一個跳轉。我視之為獲取緩衝僅能包含連續程式碼的一個跡象。它不能跨越一個被預測的分支。
17.3. 預解碼與指令長度解碼
指令長度從1到15位元組。在程式碼快取中,指令邊界被標記並拷貝到2級快取。因此,指令長度解碼很少成為瓶頸,即使指令長度解碼器每時鐘週期僅能處理一條指令。1級程式碼快取包含可觀的預解碼資訊。這包括每條指令在哪裡結束,opcode位元組在哪裡,單、雙與向量路徑指令間的區別,以及跳轉與呼叫識別的資訊。這些資訊中的一些,但不是所有,被拷貝到2級快取。來自2級快取指令的低頻寬,可能是由於新增更多預解碼資訊的過程。
我的實驗表明,在K8上,在一個時鐘週期裡解碼3條指令是可能的,即使第三條指令在第一條指令後超出16位元組處開始,只要32位元組緩衝裡留有足夠的位元組。
微處理器的吞吐率是每時鐘週期3條指令,即使對包含被預測跳轉的指令流。我們知道一個跳轉導致指令獲取過程中的一個時延空泡,但在獲取與解碼間有一個緩衝,使它在這個時延後能趕上。在AMD’s Optimization Guide的某些版本里一個建議是,將每3條指令對齊到8位元組,可以改進解碼。通過插入偽字首,使每3條指令8位元組長。根據我的實驗,這個建議已經過時。解碼器總是可以每時鐘週期處理3條相對短的指令,不管是否對齊。使指令變長沒有好處。我觀察到使指令變長,讓指令組長度成為8位元組倍數(不管是否對齊),僅在很少的情形裡會有改進。但使指令變長,更可能有負面影響。
每個指令解碼器每時鐘週期可以處理3個字首。這意味著在同一個時鐘週期裡,可以解碼帶有3個字首的3條指令。帶有4 ~ 6個字首的指令需要一個額外時鐘週期來解碼。
17.4. 單、雙與向量路徑指令
- 產生一個巨集操作的指令被稱為直接路徑單指令(direct path single Instruction)。
- 產生兩個巨集操作的指令被稱為直接路徑雙指令(direct path double Instruction,僅K8)。
- 產生超過兩個巨集操作的指令被稱為向量路徑指令(vector path Instruction)。
每條指令產生的巨集運算元在手冊4“指令表”中列出。
使用一條雙指令還是兩條單指令沒有區別,除了減小程式碼大小。吞吐率仍然限制在每時鐘週期3個巨集操作,不是3條指令。這個瓶頸的來源最有可能是回收階段。如果瓶頸在排程器中,那麼不能預期浮點排程器中的一條雙指令會限制整數排程器的吞吐率,反之亦然。
向量路徑指令效率比單或雙指令低,因為它們要求獨佔解碼器與流水線,且不總是重排最優。例如:
; Example 17.1. AMD instruction breakdown
xchg eax, ebx ; Vector path, 3 ops
nop ; Direct path, 1 op
xchg ecx, edx ; Vector path, 3 ops
nop ; Direct path, 1 op
這個序列需要4時鐘週期解碼,因為向量路徑指令必須單獨解碼。
大多數讀-修改與讀-修改-寫指令僅產生一個巨集操作。因此,這些指令比使用單獨的讀與修改指令更高效。從記憶體運算元到讀-修改指令結果的時延,與讀的時延加上算術操作的時延相同。例如,在32位模式裡,指令ADD EAX, [EBX]有從EAX輸入到EAX輸出的1時鐘週期時延,以及從EBX到EAX輸出的4時鐘週期時延。8位或16位讀的行為類似於讀-修改指令。例如,MOV AX, [EBX]比MOV EAX, [EBX]多1個時鐘週期。
巨集操作可有任意數量的輸入依賴。這意味著帶有超過2個輸入依賴的指令,比如MOV [EAX+EBX], ECX,ADC EAX, EBX及CMOVBE EAX, EBX,僅產生一個巨集操作,而在Intel處理器上,它們要求兩個微操作。
17.5. 棧引擎
K10有一個非常類似於Intel處理器的棧引擎(第頁)。這使得棧操作(PUSH,POP,CALL,RET)在K10上比之前的處理器更高效。
17.6. 整數執行流水線
3個整數流水線都有自己的ALU(算術邏輯單元)與AGU(地址生成單元)。每個ALU都可以處理任意整數操作,除了乘法。這意味著在同一時鐘週期執行3個單整數指令是可能的,如果它們是無關的。AGU用於記憶體讀、寫以及LEA指令的複雜版本。在同一時鐘週期進行兩個記憶體操作與一條LEA是可能的。進行3個記憶體操作是不可能的,因為到資料快取僅有兩個埠。
K10可以在ALU裡執行不超過兩個運算元的LEA指令,即使它有一個SIB位元組。帶有比例因子,或者同時帶有基址暫存器,索引暫存器及加數的LEA指令,在AGU中執行。未知帶有RIP相對地址的LEA是否在AGU執行。在AGU中執行的LEA有2時鐘週期的時延。如果AGU與ALU在流水線的同一個階段,如模型暗示的,假設這兩個單元間沒有快速的資料轉發通道,可能解釋了額外的時延。
整數乘法僅在ALU0中進行。一個32位整數乘法需要3時鐘週期,且完全流水線化,使新乘法可以在每時鐘週期開始。帶有累計物件作為隱含運算元及一個顯式運算元的整數乘法指令,在DX:AX,EDX:EAX或RDX:RAX中,產生一個雙精度大小的結果。對結果的高半部,這些指令使用ALU1。建議使用不產生雙精度大小結果的乘法指令,以釋放ALU1與EDX。例如,如果結果可以放入32位,使用IMUL EAX, EBX替換MUL EBX。
17.7. 浮點執行流水線
浮點流水線中的3個執行單元被稱為FADD,FMUL與FMISC。FADD可以處理浮點加法。FMUL可以處理浮點乘法與除法。FMISC可以處理記憶體寫與型別轉換。所有3個單元都可以處理記憶體讀。浮點單元有自己的暫存器檔案以及80位資料匯流排。
浮點加法與乘法的時延是4時鐘週期。這些單元完全流水線化,因此新操作可以在每個時鐘週期開始。除法需要11個時鐘週期,且沒有完全流水線化。移動與比較操作的時延是2時鐘週期。
3DNow指令的時延與XMM指令相同。使用3DNow指令,而不是XMM指令,幾乎沒有好處,除非你需要近似倒數指令,3DNow版本比XMM版本更準確、高效。3DNow指令集已經過時,在更新的微處理器上不可用。
在MMX與XMM暫存器裡的SIMD整數操作在浮點流水線中處理,而不是整數流水線。FADD與FMUL流水線都有可以處理加法、布林與偏移操作的整數ALU。整數乘法僅由FMUL處理。
浮點單元的最小時延是2時鐘週期。這個時延由流水線設計導致,而不是低時鐘頻率或慢的加法。大多數SIMD整數ALU操作有2時鐘週期的時延。整數乘法有3時鐘週期的時延。這些呼叫完全流水線化,因此新操作可以在每個時鐘週期開始。
根據去往的執行單元,浮點單元的巨集操作可以分為5類。我將如下命名這些類別:
巨集操作類別 |
處理單元 |
||
FADD |
FMUL |
FMISC |
|
FADD |
X |
||
FMUL |
X |
||
FMISC |
X |
||
FA/M |
X |
X |
|
FANY |
X |
X |
X |
表17.1. AMD浮點巨集操作類別
浮點排程器將每個巨集操作傳送給可以處理它的單元。FA/M類別的巨集操作可以去往FADD或FMUL單元。FANY類別的巨集操作可以去往任意單元。在手冊4“指令表”中,所有浮點指令的類別在“AMD指令時序與μop分解”下。
不幸,排程器不能最優地在3個單元間釋出巨集操作。FA/M類別的巨集操作根據可能最簡單的演算法排程:FA/M巨集操作交替去往FADD與FMUL。這個演算法確保同一時鐘週期提交的兩個FA/M巨集操作不會進入同一個單元。一個狀態位元記錄上一次使用的單元,這個位元不會失效。我找不到重置它的方法。
FANY類別巨集操作的排程演算法僅是稍複雜。FANY類別的巨集操作優先去往由它恰好來自的流水線確定的單元。在一系列整數巨集操作後的第一個FANY巨集操作去往FMISC。在同一時鐘週期裡,第二條FANY巨集操作去往FMUL,可能的第三個FANY巨集操作去往FADD。如果在同一時鐘週期提交的其他巨集操作需要一個特定的浮點單元,那麼FANY巨集操作可以被重定向到另一個單元。
在確定將一個FA/M或FANY類別巨集操作傳送到哪裡時,浮點排程器不檢查一個特定單元是否空閒或者有長的佇列。例如,如果一個指令流產生了10個FADD類別的巨集操作,然後一個FA/M類別巨集操作,有50%的可能性FA/M巨集操作將前往FADD單元,儘管傳送到FMUL將節約1個時鐘週期。
巨集操作的這個次優的排程會顯著拖慢帶有許多浮點指令程式碼的執行。通過為每個浮點單元設定的效能監控計數器測試一小段關鍵程式碼,可以分析這個問題。不過,這個問題難以解決。有時,改變指令次序、使用不同的指令或插入NOP,可能改進巨集操作的分佈。不過沒有通用及可靠的方式解決這個問題。
另一個後果更嚴重的排程問題在下一段解釋。
17.8. 混用不同時延的指令
在混用不同時延的指令時,有排程問題。浮點執行單元被流水線化,因此如果一個4時延的巨集操作在時刻0開始,在時刻3結束,同一型別的第二個巨集操作可以在時刻1開始,時刻4結束。不過,如果第二個巨集操作時延是3,那麼它不可以在時刻1開始,因為它將在時刻3結束,與前面的巨集操作同時。每個執行單元僅有一個結果匯流排,這阻止了巨集操作同時提交它們的結果。排程器通過不把巨集操作派遣到一個執行單元,防止衝突,如果它可以預測在這個巨集操作完成時,結果匯流排不是空閒的。它不能將巨集操作重定向到另一個執行單元。
這個問題由下面的例子說明:
; Example 17.2. AMD mixing instruction with different latencies (K8)
; Unit time op 1 time op 2
mulpd xmm0, xmm1 ; FMUL 0-3 1-4
mulpd xmm0, xmm2 ; FMUL 4-7 5-8
movapd xmm3, xmm4 ; FADD/FMUL 0-1 8-9
addpd xmm3, xmm5 ; FADD 2-5 10-13
addpd xmm3, xmm6 ; FADD 6-9 14-17
在這個例子中的每條指令產生兩個巨集操作,128位暫存器的每個64位部分一個。頭兩個巨集操作是時延為4的乘法。它們分別在時刻0與1開始,在時刻3與4結束。後兩個乘法巨集操作需要前面巨集操作的結果。因此,它們分別直到時刻4與5才能開始,在時刻7與8結束。到目前還好。MOVAPD指令產生時延為2的兩個FA/M類別巨集操作。其中一個去往空閒的FADD流水線,因此這個巨集操作可以立即開始。MOVAPD的另一個巨集操作去往FMUL流水線,因為FA/M類別的巨集操作交替使用這兩個流水線。在時刻2,FMUL流水線準備開始執行新的巨集操作,但MOVAPD巨集操作不能在時刻2開始,因為這樣它將在MULPD第一個巨集操作結束的時刻3結束。它不能在時刻3開始,因為這樣它將在MULPD第二個巨集操作結束的時刻4結束。它不能在時刻4或5開始,因為後兩個MULPD巨集操作在那裡開始。它不能在時刻6或7開始,因為這樣結果與後兩個MULPD巨集操作結果衝突。因此,對這個巨集操作,時刻8是是第一個可能的開始時刻。後果是,後續依賴MOVAPD的加法,將被推遲7個時鐘週期,即使FADD單元的空閒的。
有兩個方法避免上面例子中的問題。第一種可能性是重排指令,將MOVAPD移到兩條MULPD指令的前面。這將使MOVAPD的兩個巨集操作都在FADD與FMUL單元,在時刻0開始。後面的乘法與加法將執行在兩個流水線裡,彼此沒有干擾。
第二個可能的解決方案是用一個記憶體運算元替換XMM4。MOVAPD XMM3, [MEM]指令產生兩個使用FMISC單元的巨集操作,在這個例子裡它是空閒的。不管時延是否相同,在不同執行流水線中的巨集操作間沒有衝突。
當然,在K10上的吞吐率比K8上高,但對所有使用浮點暫存器、MMX暫存器或XMM暫存器的指令,死鎖問題仍然存在。作為一個一般性指引,可以這樣說,在一個2時延的巨集操作跟在至少兩個時延更長、排程到相同浮點執行單元的巨集操作後面,且這些巨集操作不需要等待彼此的結果時,會出現死鎖。可以將短時延的指令放到前面,或使用去往不同執行單元的指令,避免死鎖。
大多數巨集操作的時延與執行單元列出如下。完整的列表可以在手冊4“指令表”中找到。記住在K10上,一條128位指令通常產生一個巨集操作,在K8上是兩個巨集操作。
巨集操作型別 |
時延 |
執行單元 |
暫存器到暫存器移動 |
2 |
FADD/FMUL 交替 |
暫存器到記憶體移動 |
2 |
FMISC |
記憶體到暫存器移動, 64 位 |
4 |
任意 |
記憶體到暫存器移動, 128 位 |
4 |
FMISC |
整數加法 |
2 |
FADD/FMUL 交替 |
整數布林 |
2 |
FADD/FMUL 交替 |
偏移,封裝,拆包,混排 |
2 |
FADD/FMUL 交替 |
整數乘法 |
3 |
FMUL |
浮點加法 |
4 |
FADD |
浮點乘法 |
4 |
FMUL |
浮點除法 |
11 |
FMUL (not pipelined) |
浮點比較 |
2 |
FADD |
浮點 max/min |
2 |
FADD |
浮點倒數 |
3 |
FMUL |
浮點布林 |
2 |
FMUL |
型別轉換 |
2-4 |
FMISC |
表17.2. AMD中的執行單元
17.9. 64位與128位指令
在K10上使用128位指令是一個大優勢,但在K8上不是,因為K8上每條128位指令被分解為兩個64位巨集操作。
在K10上,128位記憶體寫指令被處理為兩個64位巨集操作,而128位記憶體讀由一個巨集操作完成(K8上是2)。
在K8上,128位記憶體讀指令僅使用FMISC單元,K10上是所有3個單元。因此,在K8上使用XMM暫存器將資料塊從一個記憶體位置移動到另一個沒有優勢,但在K10上有。
-
- 不同型別指令間的資料時延
根據預定的運算元型別,XMM指令有3個不同的型別:
- 整數指令。大多數這些指令有以P開頭的名字,表示封裝,例如POR。
- 單精度浮點指令。這些指令有以SS(標量單精度)或PS(封裝單精度)結尾的名字,例如ORPS。
- 雙精度浮點指令。這些指令有以SD(標量雙精度)或PD(封裝雙精度)結尾的名字,例如ORPD。
POR,ORPS與ORPD這3條指令實際上做相同的事情。它們可以互換,但在一條整數指令的輸出用作一條浮點指令的輸入時,有時延,反之亦然。對這個時延,有兩種可能的解釋:
解釋1:XMM暫存器有一些用於記錄浮點值是否為規範、次規範或零的標記位。在一條整數指令的輸出用作一條單精度或雙精度浮點指令的輸入時,必須設定這些標記位。這導致了所謂的重新格式化時延。
解釋2:在整數與浮點SIMD單元之間沒有快速資料轉發通道。這導致了類似於P4上執行單元間的時延。
單精度與雙精度浮點指令間沒有時延,但浮點到整數指令有時延的事實,支援解釋2。
在從記憶體讀且不進行計算的指令後,時延沒有差別。在寫記憶體且不進行計算的指令前,沒有時延。因此,對於記憶體讀寫,可以使用MOVAPS指令,而不是多1位元組的MOVAPD或MOVDQA。
在一條記憶體讀指令的輸出(不管型別)用作一條浮點指令的輸入時,通常有一個2時鐘週期的時延。這支援解釋1。
對算術操作,使用錯誤型別的指令是不合理的,但對僅移動資料或執行布林操作的指令,只要不導致時延,使用錯誤型別可能是有好處的。名字以PS結尾的指令比其他等效指令要短1位元組。
17.10. 暫存器的部分訪問
處理器總是將一個整數暫存器的不同部分放在一起。因此,AL與AH不被亂序執行機制視為無關。這會在寫暫存器一部分的程式碼中導致假的依賴性。例如:
; Example 17.3. AMD partial register access
imul ax, bx
mov [mem1], ax
mov ax, [mem2]
在這個情形裡,第三條指令有對第一條指令的一個假依賴,由EAX的高半部導致。第三條指令寫入EAX(或RAX)的低16位,在EAX新的值可以被寫入前,這16位必須與EAX的餘下部分合並。結果到AX的移動必須等待前面的乘法完成,因為不能分開EAX的不同部分。通過在到AX的移動前插入一條XOR EAX, EAX指令,或以MOVZX EAX, [MEM2]替代MOV AX, [MEM2],可以消除這個假依賴。
不管是在16位模式、32位模式,抑或64位模式中執行,上面例子的行為都相同。在64位模式中,要消除這個假依賴,EAX清零就足夠了。不需要清零整個RAX,因為寫入一個64位暫存器的低32位總是重置該64位暫存器的高半部。但寫入一個暫存器的低8位或低16位,不會重置該暫存器餘下的部分。
這個規則不適用於K8上的XMM暫存器,在K8上,每個128位暫存器被儲存為兩個無關的64位暫存器。
17.12. 標記暫存器的部分訪問
處理器將算術標記分為至少以下組:
- 零、符號、奇偶與輔助標記
- 進位標記
- 溢位標記
- 非算術標記
這意味著一條僅修改進位標記的指令沒有對零標記的假依賴,一條僅修改零標記的指令有對符號標記的假依賴。例如:
; Example 17.4. AMD partial flags access
add eax, 1 ; Modifies all arithmetic flags
inc ebx ; Modifies all except carry flag. No false dependence
jc L ; No false dependence on EBX
bsr ecx, edx ; Modifies only zero flag. False depend. on sign flag
sahf ; Modifies all except overflow flag
seto al ; No false dependence on AH
17.13. 寫轉發暫停
在寫入一個記憶體位置後,立即從該位置讀,如果讀比寫大,會有一個懲罰,因為在這個情形下,寫轉發機制不能工作。例如:
; Example 17.5. AMD store forwarding
mov [esi], eax ; Write 32 bits
mov bx, [esi] ; Read 16 bits. No stall
movq mm0, [esi] ; Read 64 bits. Stall
movq [esi], mm1 ; Write after read. No stall
如果讀沒有在與寫相同的地址開始,也有懲罰:
; Example 17.6. Store forwarding stall
mov [esi], eax ; Write 32 bits
mov bl, [esi] ; Read part of data from same address. No stall
mov cl, [esi+1] ; Read part of data from different address. Stall
如果寫源自AH,BH,CH或DH,也有懲罰:
; Example 17.7. Store forwarding stall for AH
mov [esi], al ; Write 8 bits
mov bl, [esi] ; Read 8 bits. No stall
mov [edi], ah ; Write from high 8-bit register
mov cl, [edi] ; Read from same address. Stall
17.14. 迴圈
AMD K8與K10的分支預測機制在第頁描述。
在AMD上,小迴圈的速度通常受指令獲取限制。不超過6個巨集操作的小迴圈可以在2個時鐘週期裡執行每次迭代,如果它包含不超過1個跳轉,且在K10上不包含32位元組邊界,在K8上不包含16位元組邊界。如果在K10上,程式碼中有一個32位元組邊界,每迭代將需要一個額外時鐘週期,因為它需要獲取一個額外的32位元組塊,或者在K7或K8上包含16位元組邊界。
最大獲取速度可由以下規則概括:
一個迴圈的每迭代最小執行時間,在K10上大致上等於程式碼中32位元組邊界數,或者在K8上16位元組邊界, 加上被採用分支及跳轉數的2倍。
例如:
; Example 17.8. AMD branch inside loop
mov ecx,1000
L1: test bl,1
jz L2
add eax,1000
L2: dec ecx
jnz L1
假定在JNZ L1指令處是32位元組邊界。那麼如果JZ L2不跳轉,該迴圈將需要3時鐘週期,如果JZ跳轉,需要5時鐘週期。在這個情形裡,我們可以通過在L1前插入一個NOP,使得32位元組邊界移動到L2,改進程式碼。那麼迴圈將分別需要3與4時鐘週期。在JZ L2跳轉的地方,我們節省了1時鐘週期,因為32位元組邊界被移到我們繞過的程式碼。
如果指令獲取是瓶頸,這些考慮才是重要的。如果在迴圈中別的東西,比計算的獲取時間,需要更多時間,沒有原因優化指令獲取。
17.15. 快取
1級程式碼快取與1級資料快取都是64K位元組,2路組相聯,每行64位元組。資料快取有兩個埠可用於讀或寫。這意味著在同一時鐘週期,它可以進行兩次讀或兩次寫,或者一次讀與一次寫。在K10上,每個讀埠是128位,K8上是64位。在K8與K10上,寫埠都是64位。這意味著一個128位寫操作要求兩個巨集操作。
在程式碼快取行中,每個64位元組行被分為4塊,每塊16位元組。在資料快取中,每個64位元組行被分為8個每個8位元組的庫(bank)。在同一時鐘週期,資料快取不能執行兩個記憶體操作,如果它們使用相同的庫,除了相同快取行的兩個讀:
; Example 17.9. AMD cache bank conflicts
mov eax, [esi] ; Assume ESI is divisible by 40H
mov ebx, [esi+40h] ; Same cache bank as EAX. Delayed 1 clock
mov ecx, [esi+48h] ; Different cache bank
mov eax, [esi] ; Assume ESI is divisible by 40H
mov ebx, [esi+4h] ; Read from same cache line as EAX. No delay
mov [esi], eax ; Assume ESI is divisible by 40H
mov [esi+4h], ebx ; Write to same cache line as EAX. Delay
(See Hans de Vries: Understanding the detailed Architecture of AMD's 64 bit Core, Chip Architect, Sept. 21, 2003. www.chip-architect.com . Dmitry Besedin: Platform Benchmarking with Right Mark Memory Analyzer, Part 1: AMD K7/K8 Platforms. www.digit-life.com .)
帶有記憶體訪問操作的執行使用3個有各自流水線的不同單元:(1)一個算術邏輯單元(ALU)或其中一個浮點單元,(2)地址生成單元(AGU),(3)一個讀寫單元(LSU)。ALU用於讀-修改與讀-修改-寫指令,但不用於僅讀或寫的指令。AGU與LSU用於所有的記憶體指令。對讀-修改-寫指令,LSU使用兩次。ALU與AGU微操作可以亂序執行,而LSU微操作在大多數情形裡順序處理。就我所知,這些規則如下:
- 1級程式碼快取滿足讀順序處理。
- 1級資料快取不命中讀以任意順序處理。
- 寫必須順序處理。
- 讀可以去到一個不同地址的之前寫的前面。
- 依賴於一個相同地址的之前寫的讀,只要轉發的資料可用,就可以處理。
- 直到所有之前的讀與寫操作的地址已知,讀或寫才能進行。
建議在程式碼中儘早讀或計算指標與索引暫存器的值,以避免後續記憶體操作的時延。記憶體操作必須等待所有之前記憶體操作的地址已知的事實,會導致假的依賴,例如:
; Example 17.10. AMD memory operation delayed by prior memory operation
imul eax, ebx ; Multiplication takes 3 clocks
mov ecx, [esi+eax] ; Must wait for EAX
mov edx, [edi] ; Read must wait for above
可以通過在ECX之前讀EDX,使得讀EDX無需等待慢的乘法,來改進這個程式碼。
如果資料跨了一個8位元組邊界,對非對齊記憶體引用有1時鐘週期的暫停。非對齊還阻止了寫到讀的轉發。例如:
; Example 17.11. AMD misaligned memory access
mov ds:[10001h], eax ; No penalty for misalignment
mov ds:[10005h], ebx ; 1 clock penalty when crossing 8-byte boundary
mov ecx, ds:[10005h] ; 9 clock penalty for store-to-load forwarding
2 級快取
2級快取有512KB或更多,16路組相聯,每行64位元組以及一條16位元組寬的匯流排。行由一個偽LRU方案逐出。
可以正或負步長自動預取資料流。資料僅被預取到2級快取,不到1級快取。
用於資料時,2級快取包括自動糾錯位元,但用於程式碼時沒有。程式碼是隻讀的,因此在校驗錯誤時可以從RAM重新讀入。節省下來的位元用於儲存來自1級快取的指令邊界與分支預測資訊。
3 級快取
K10有2MB的3級快取。有別的3級快取大小的版本也可能會出現。3級快取在所有的核之間共享,而每個核有自己的1級與2級快取。
17.16. AMD K8與K10中的瓶頸
在優化一段程式碼上,找出控制執行速度的限制因素是重要的。調整錯誤的因素不太可能有任何益處。在下面的段落中,我將解釋AMD微架構中每個可能的限制因素。
指令獲取
在K8及更早的處理器上,指令獲取被限制為每時鐘週期16位元組程式碼。在流水線其他部分每時鐘週期可以處理3條指令時,這會是一個瓶頸。在K10上,指令獲取不太可能是瓶頸。
被採用跳轉的吞吐率是每2個時鐘週期一個。在一個跳轉後的指令獲取被進一步推遲,如果在該跳轉後的頭3條指令中有16位元組邊界。建議把最關鍵例程入口與迴圈入口對齊到16位元組,或至少確保關鍵的跳轉目標不是在對齊的16位元組塊末尾附近。小迴圈中跳轉與16位元組邊界數應該儘可能少。參考上面第頁。
亂序排程
最大重排深度是24個整數巨集操作加上36個浮點巨集操作。記憶體操作不能亂序排程。
執行單元
執行單元有比可能的使用率大得多的能力。據稱,9個執行單元可以同時執行9個巨集操作,但幾乎不可能通過實驗驗證這個宣告,因為回收限制在每時鐘週期3個巨集操作。所有3條整數流水線都可以處理所有整數操作,除了乘法。因此,整數執行單元不會是瓶頸,除了乘法極多的程式碼。
在沒有執行單元收到超過三分之一巨集操作時,可以得到每時鐘週期3個巨集操作的吞吐率。至於浮點程式碼,很難在3個浮點單元間獲得巨集操作完美的均勻分佈。因此,建議混用浮點指令與整數指令。
浮點排程器不能在浮點執行單元間最優地分配巨集操作。一個巨集操作可能去往一個有長佇列的單元,而另一個單元則是空閒的。參考第頁。
所有的浮點單元都流水線化,吞吐率是每時鐘週期一個巨集操作,除了除法以及其他幾條複雜的指令。
混用的時延
混用不同時延的巨集操作,對同一個浮點單元進行排程會嚴重妨礙亂序執行。參考第頁。
依賴鏈
避免長依賴鏈以及在依賴鏈中避免記憶體立即數。可以通過寫一個暫存器或者在暫存器自身上執行以下指令,來破壞一個假依賴:XOR,SUB,SBB,PXOR,XORPS,XORPD。例如,XOR EAX, EAX,PXOR XMM0, XMM0,但不是XOR AX, AX,PANDN XMM0, XMM0,PSUBD XMM0, XMMO或比較指令。注意SBB有對進位標記的依賴。
訪問一個暫存器的部分導致對該暫存器餘下部分的一個假依賴,參考第頁。訪問標記暫存器的部分不會導致一個假依賴,除了罕見情形,參考第頁。
跳轉與分支
跳轉與分支的吞吐率是每2時鐘週期一個被採用的分支。如果緊跟著跳轉目標是16位元組邊界,吞吐率會更低。參考第頁。
分支預測機制允許每16位元組的對齊程式碼不超過3個被採用的分支。如果遵守這個規則,總是去往相同地方的跳轉被良好預測。參考第頁。
動態分支預測基於一個僅有8或12位元的歷史。另外,模式識別通常出於未知原因失敗。總是去往相同地方的分支不會汙染分支歷史暫存器。
回收
回收過程限制為每時鐘週期3個巨集操作。如果有產生多個巨集操作的指令,這很可能成為瓶頸。