程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 深入理解Intel Core Microarchitecture

深入理解Intel Core Microarchitecture

編輯:關於.NET

Core 2的2級Cache

1級Cache分為 32KB L1i Cache和32KB L1d Cache,都是8路組相聯write back buffer,64bytes per line,每個core擁有獨立的L1 Cache,共享L2 Cache和總線接口, L2 Cache為16路組相聯,64bytes per line,與L1 Cache之間的數據帶寬為256bit。兩個Core的L1d Cache之間可以互相傳輸數據,L1 Cache擁 有幾個數據和指令硬件預取器,L2 Cache的預取器是基於L1 Cache預取器的訪問模式和密集程度來工作的 ,使用了一個改進的Round-Robin算法動態的在兩個處理器之間分配帶寬。前端總線接口也采用了類似的 方式以確保平衡。L1 Cache和L2 Cache采用了獨立訪問設計,也就是說Core可以直接從L2 L1或Main Memory中直接取數據,無須逐級上升。intel cache使用了mainly inclusive設計,相比AMD使用的是 exclusive設計。

數據和指令地址對齊的重要性

現代微處理器架構在Load和Store內存時,如果訪問地址為N的整數倍時,則可以一次操作N個字節(N = 2^M),但如果操作的N個字節處於的地址對N取余不為0,則需要2個或以上個時鐘周期,特別是在地址跨越 了Cache Line後,性能影響更為嚴重。比如讀取4 BYTE的int變量,如果此變量所在內存物理地址(注意 是物理地址,不是線性地址)正好為4的整數倍,則能高效的完成操作(處於同樣的BUFFER中時)。而且 數據按其自然長度對齊還有一個好處就是將數據跨越Cache line的幾率降到了最低(後面我們會詳細講到 Core微處理器架構的Cache工作原理)。既然對齊那麼重要,那我們如何讓數據位於對齊的地址上呢?

對於全局變量我們可以使用__declspec(align(N))來達到目的,當然也可以使用INTEL相關的一些類型 比如__m128,比如:

typedef __declspec(align(16)) int A16INT;

A16INT i;

現在這個變量i的地址就是16字節對齊的了。

對於位於堆棧中的變量特別復雜,當然我們同樣可以使用__declspec(align(N))來對齊變量地址,但 需要提醒的是,microsoft visual c++ compiler 的默認堆棧對齊是4BYTE,但官方文檔稱編譯器會智能 的判斷數據組織,進行必要對齊轉換,堆棧默認對齊相當詭異,我測試過跟編譯優化選項有關系,實際上 變量在堆棧中的排列順序與定義順序通常是不一致的,而且也不敢保證struct的字節對齊,double在8字 節對齊除非手動加上__declspec(align(N)),在優化條件為/O2時,測試中顯示堆棧都是自然長度對齊,僅 僅限於我的個人測試,我沒有找到相關文檔明確說明/O2條件下堆棧一定是數據自然對齊的,所以如果在 明確要求數據對齊的地方,還是要加上對齊標志。

動態內存分配的對齊

參考_aligned_malloc、_mm_malloc,如果用malloc分配的內存大小大於8,則內存是8字節對齊的,否 則為最小2的多次方對齊,比如如果分配7字節,則4字節對齊。

Pentium Pro, 2 and 3 pipeline

取指令

取指單元將指令代碼從代碼緩沖的16字節對齊地址處一次取16bytes到另外一個32bytes緩沖中,取到 一個2倍緩沖的主要目的就是隨後可以解碼跨越16byte邊界的指令。隨後代碼會從這個32字節緩沖中傳送 到解碼器,而傳送的代碼塊我們給它取個名字叫 IFETCH塊 ,這個IFETCH塊最多為16字節。大多數情況下 ,取指單元會讓會讓IFETCH塊從一個指令邊界開始,而不是16字節,但要做到這點,取指單元就需要知道 指令的邊界在哪裡,如果此時不知道,它就讓IFETCH塊從16字節對齊邊界開始。

32字節緩沖沒有足夠的大小來緩沖JUMP後的代碼塊,所以JUMP都有延遲,如果IFETCH塊中包含的JUMP 指令跨越了中間的16字節邊界,那麼32字節邊界就必須保存2個CHUNK來滿足JUMP的解碼,這樣在這個時鐘 周期中就不能取一個代碼塊到32字節緩沖中,如果JUMP後的代碼塊的第一個指令又是跨越了內存的16字節 邊界,那麼完了,又要多等1個周期,因為要取2個塊後才能進行第一個指令的解碼(所謂延遲就是因為本 單元阻塞了整個流水線)。取指單元每個時鐘周期能取16字節,如果解碼一個IFETCH塊大於一個時鐘周期的 話,這樣在多余的時鐘周期中可以進行額外的取代碼操作,這樣可能抵消前面產生的延遲.如果在指令跳轉 之後32字節緩沖只有時間取16字節,那麼第一個IFETCH塊就是16字節對齊的那塊,而不是從真實指令開始的 地方,如果有時間取2個16字節的代碼,則實際取到32字節緩沖的是代碼塊就是真實從JUMP後第一個指令開 始的.

32字節緩沖只有時間在跳轉後從16字節對齊的CACHE地址取16字節出來,因為跳轉後沒有指令長度信息 可用,所以JUMP後的第一個IFETCH塊就是這個塊了,很顯然JUMP後的第一條指令並不開始於這個IFETCH塊的 首端,但游戲JUMP跨越了本16字節,還有更高地址的16字節,完整的IFETCH快必須等待,32字節緩沖填充滿32 字節後才能產生.

指令長度包含了從1到15字節的范圍,因此,我們無法確定,16字節的IFETCH塊包含了完整的指令數目,很 可能最前或最後的指令就跨越了16字節的邊界,延伸到前16字節代碼塊或後16字節代碼快,那麼取指單元必 須知道16字節塊中的最後一條指令在哪裡結束,這樣取指單元就能正確的生成下一個IFETCH塊了,而前一個 IFETCH塊中最後一個不完整指令將不會被解碼,所以不占用時鐘周期.指令的長度信息可以由指令長度解碼 器來完成,這個單元處於IFU2處,也就是第2個取指單元.每個時鐘周期,它能獲得3條指令長度信息,比如,如 果一個IFETCH塊包含了10條指令,那麼在產生下一個IFETCH塊之前必須等待3個時鐘周期.

譯碼單元

指令長度解碼器是一個高度串行的單元,因為在知道後一條指令的開始之前,你必須依次知道前面所有 指令的結束,這嚴重降低了整個系統的並行度,我們總是盡力做到一次取多條指令,一次解碼多條指令,一次 執行多條微指令,來獲得盡可能快的速度.簡單的指令長度解碼器一次(一個時鐘周期)只能知道一條指令的 長度,但PPRO微處理器架構可以一次確定3條指令的長度,甚至同時可以將結果傳回給取指單元,讓取指單元 在同一時刻來正確的從32字節緩沖中產生IFETCH塊,這是個相當振奮的功能,我相信它是通過並行同時解析 所有的16個可能開始的地方做到這點的.

當IFETCH塊產生後被送到譯碼單元,在這裡指令會被翻譯為微指令,這裡有3個譯碼單元並行工作,這樣 一個時鐘周期最多能有3條指令被解碼,同時被界碼的幾條條指令(最多3條)被稱為一個解碼組,3個解碼單 元被分別成為D0,D1,D2,D0能力最強,它可以在一個時鐘周期解碼能分解成4個微指令的指令,D1D2相對較弱 ,只能解碼產生一個UOP的指令,並且指令長度不得大於8字節,IFETCH塊中的第一條指令總是被派發到D0,余 下的指令如果D1D2可以搞定,則派發到D1或D2,否則必須等待D0完成手頭工作,再來解碼余下指令.如 下:

mov [esi], eax    ;2 uops

add ebx, [edi]    ;2 uops

sub eax, 1      ;1 uop

cmp ebx, ecx     ;1 uop

je L1         ;1 uop

第一條指令被派發到D0,而第二條指令發現沒地方解碼,因為他產生2條必須在D0,所以從它開始必須延 遲一個周期,等D0完成第一條指令解碼後再開始,第二個時鐘周期,I2 I3 I4分別被派發到D0 D1 D2,第3個 時鐘周期I5到D0.所以在PPRO中,最好的指令組合是1條2~4 uops的指令跟2條1 uop的指令.大於4uop的指令 ,必須由D0來解碼,並且需要2個或2個以上時鐘周期來完成解碼,並且在這個過程中,其他任何指令無法同時 並行解碼.塊的邊界問題,最復雜的就是IFETCH塊中的第一條指令總是進入D0,如果指令長度是411模式,並 且不幸的是總是1uop指令在第1個,那麼4-1-1模式被破壞,解碼141這樣的組合總是要花費2個時鐘周期,如 果所有的指令都是141 141 141模式,那麼完蛋了,每次都要延遲1周期,但很顯然,只要簡單的將模式後移一 個指令就OK了,但取指單元是絕對無法知道哪個指令產生多少條uop,相信這個結果要在在接下來2個階段後 才能知道.

這個問題非常難處理,因為要知道指令邊界很困難,最好的方法就是好好組織代碼,讓代碼平均在一個時 鐘周期能產生3條uops,流水線的RAT和RRF階段一個時鐘周期最多只能處理3條UOPS,如果我們按411模式來 組織,這樣我們就可能有如下情況

114 3UOPS/C 411 6UOPS/C 141 3UOPS/C

這樣平均下來我們可以達到4UOPS/C,這樣我們就能補償可能在IFETCH邊界1個時鐘的延遲,這樣維持解 碼器3UOPS/C的效率.

另外一個解決方法就是讓一個IFETCH塊中的指令盡可能的多,這樣在16字節邊界被打斷的次數也會少很 多.比如用指針來代替絕對地址可以縮短指令長度.

; Example 5.2. Instruction fetch blocks

address  instruction   length   uops    expected decoder

---------------------------------------------------------------------

1000h   mov ecx, 1000      5     1        D0

1005h LL: mov [esi], eax      2     2        D0

1007h   mov [mem], 0       10     2        D0

1011h   lea ebx, [eax+200]    6     1        D1

1017h   mov byte ptr [esi], 0  3     2        D0

101Ah   bsr edx, eax       3     2        D0

101Dh   mov byte ptr [esi+1],0  4     2        D0

1021h   dec edx         1     1        D1

1022h   jnz LL          2     1        D2

我們來上面這個例子

假設IFETCH塊從1000H開始到1010H結束,那麼在這個IFETCH塊中就存在一個不完整的"mov [mem], 0"指 令,那麼第二個IFETCH塊就要從1007H開始在1017H結束,結果如下:

IFETCH1 -------> 1000H ~ 100FH = 16 bytes

IFETCH2 -------> 1007H ~ 1016H = 16 bytes

很顯然第2個IFETCH塊正好在lea指令末結束,那麼第三個FETCH塊為:

IFETCH3 -------> 1017H ~ 1022H = 11 bytes

所有的指令都被FETCH了,現在我們來看看第一個循環需要多少個時鐘周期來解碼,跳轉目標在1005h處, 很顯然沒有跨越16字節邊界,如果分支預測正確,IFETCH塊的產生將不會發生延遲,IFETCH塊1為1005H到 1014H,顯然末尾包含了一個不完整的LEA指令,第一個FETCH塊解碼需要耗費2個時鐘周期,因為都是2UOPS的 指令,都需要等待D0解碼,而第2個IFETCH塊從1011H到1020H,正好包含4條指令,需要花費4個時鐘周期來解 碼,太痛苦了,lea指令因為是IFETCH塊的開始,被安排到了D0解碼,第3個IFETCH塊為1021H到1022H,需要1個 解碼周期,分別在D0和D1解碼.第一輪循環的解碼周期總共需要7.

指令前綴

指令前綴同樣可能給指令解碼單元帶來延遲.

1.如果指令中包含16或32位立即數,操作數前綴可能會帶來一些時鐘延遲,因為這種情況下,前綴會改變 操作數的表示長度,從而讓指令長度改變.

2.地址長度屬性修改前綴會給解碼帶來延遲,因為前綴改變了指令的R/M位的解釋方式.但帶隱式內存操 作數的指令,比如字符串操作指令則不會有延遲.

3.段修飾前綴不會給解碼帶來性能損失.

4.重復修飾前綴和LOCK前綴不會給解碼帶來延遲.

5.如果一個指令帶多個前綴,那麼通常會給解碼部分帶來延遲,一般每多一個產生一個時鐘延遲.

Register renaming

寄存器別名表(RAT)控制寄存器換名,解碼生成的UOPS通過一個隊列來到RAT,隨後就到了ROB階段,最後 是RS,RAT一個時鐘周期可以處理3個UOPS,也就是說整個系統的吞吐量平均不會操作3UOPS/C,被換名的寄存 器數量基本沒有限制,通常一個時鐘周期能將3個寄存器換名,甚至能將一個寄存器換名3次.

寄存器換名的原理很簡單,雖然我們程序中能直接用的寄存器數量非常有限,但在微處理器內部確有大 量的隱藏寄存器,CPU能用它們替換程序中出現的邏輯寄存器,這樣能讓UOPS能最大限度的並行運行,每次程 序修改一個邏輯寄存器,為處理器就為這個邏輯寄存器分配一個隱藏寄存器.同時這個階段還計算相對跳轉 地址分支,將計算機結果返回到BTB0階段使用.所謂隱藏寄存器可能就是ROB ENTRY

reorder buffer read

當隱藏寄存器被換名為通用寄存器之後,就需要將通用寄存器的值寫到已換名的隱藏寄存器中,如果這 些值可用,則他們就存儲在ROB ENTRY中,每個ROB ENTRY最多可擁有2個輸入寄存器和2個輸出寄存器,對於 輸入寄存器中的值有如下幾種情況.

1.通用寄存器中的值可用,即永久積存器文件.那麼直接READ值到ROB ENTRY中

2.值被修改過了,但修改的UOP還沒有RETIRED,也就是沒有寫回PERMANENT REGISTER FILE,那麼直接從 not-yet-retired rob ENTRY中讀值到響應的ROB ENTRY中

3.值還不能用,因為有依賴關系的UOP還沒有執行,只能先等待,當值可用後,會馬上寫入到ROB ENTRY.

1看起來好象是最簡單的也是最沒問題的,但奇特的是1情況是唯一能在ROB-READ階段引起延遲的,原因 是PRF只有2個讀端口,而ROB-READ階段每個CLOCK卻能從RAT階段收個3個UOPS,而每個UOPS有2個輸入積存器 ,這樣1個時鐘周期就有6個積存器等待輸入,ROB-READ階段不得不用3個時鐘周期來完成輸入任務,而當RAT 與解碼器之間的隊列滿了後,解碼器和取指單元也不得不停下來,而RAT與解碼器之間的隊列只有大概10個 單元,所以馬上隊列就會被裝滿.

反而第2 3種情況在ROB-READ階段不會有延遲,如果積存器還沒有通過ROB-WRITEBACK階段,則ROB-READ 讀這個積存器就不會有延遲,積存器至少需要3個時鐘周期來完成 換名 讀ROB 執行 和 ROB寫回,所以如果 在一個TRIPLET中的UOP中寫過一個積存器,則在之後3個TRIPLET中讀這個積存器都不會有延遲,如果寫回階 段因為REORDERING 慢指令執行 依賴鏈 CACHE MISS或其他情況所延遲,那麼就有更多的時間來讀這個積存 器了.

 

不要將解碼組和UOPTRIPLET弄混了,一個解碼組可以產生1~6條UOPS,即使解碼組被解碼出來有 3個UOPS,也不敢保證這3個UOPS是一起傳送到RAT的.而且解碼器和RAT之間的隊列緩沖非常的短,我們無法 假設積存器讀延遲不會阻塞解碼器,或則解碼器的UOP流量波動不會阻塞RAT.除非解碼器和RAT之間的隊列 是空的,否則很難預測哪些UOPS同時經過RAT階段,分支預測失敗後隊列應該是空的.同一條指令產生的UOPS 也不一定要一起傳送到RAT,而RAT取指令也是簡單的從隊列去取出來,一次3條,跳轉預測不會打斷隊列.只 有預測失敗後,隊列中的UOP才會被丟棄掉,然後從頭開始取指譯指,這時3個連續的UOPS才一起進入RAT階段 .

一個讀寄存器延遲可以通過監視0A2H計數器數,不幸的是不能將這種阻塞情況與其他情況分開.如果3條 連續的UOPS讀取超過2個不同寄存器,你當然不希望他們一起進入RAT階段,他們一起進入的可能性為1/3,否 則將延遲1個周期,

亂序執行

ROB可以保存40UOPS以及40個臨時寄存器,而RESERVATION STATION則可以保存20條 UOPS,UOP待到操作數都准備好,並且有空閒的執行單元後,就被執行了.寫內存之間不能亂序執行,參閱投機 執行.

PM的管線

PM是PENTIUM M,CORE單核和CORE雙核的縮寫,但不包括CORE2.PM的的架構與PPRO P2 P3差 不多,主要的流水線處理階段為:分支預測,取指,譯碼,寄存器換名,填寫重組緩沖,UOPS緩沖站,亂序執行, 結果寫回重組緩沖,CPU狀態蛻變.PM的管線INTEL並沒有公布詳細情況,只是簡單的說比PPRO的長,所以以下 結論都是AGNER FOG的個人測試猜測.

PM的整個流水線長度,基於分支預測失敗後的延遲,猜測大概比 PPRO要多3~4階段,PM的分之預測好象比PPRO要復雜,大概使用了3個階段,比PPRO的多一個,取指單元也明顯 復雜了,因為在JUMP過程中16字節邊界問題不再會有任何延遲,所以估計IFU需要3~4個階段.

還有新的堆 棧引擎階段,很可能加在了解碼階段之後,因為在只能產生1條UOP的D1~D2之後,不耗費任何多余的時間,還 會有一條堆棧同步UOP產生.

還有一個就是UOP融合操作,好象也不用耗費額外的階段來分離這些UOPS,猜 測可能他們公用同一個ROB ENTRY,這個ENTRY可以被注冊到2個不同的執行PORT.所以在執行後也可能沒有 必要在RETIREMENT站之前來融合分開的UOPS.

CORE2的強悍登場

取指令和預解碼

CORE2在分之預測與指令預取之間加入了一個隊列,這個隊列主 要用來解決被預測分支中的各種延遲.指令解碼單元被分割為預解碼單元和解碼單元,預解碼單元主要是來 檢測每條指令從IFETCH塊的哪裡開始,還能分辨指令前綴和其他一些成分,預解碼器的帶寬為16字節或者6 條指令每時鐘周期,取較小者.管線的其他單元通常是4條指令每周期,或者5條如果有MACRO-FUSION的話.很 顯然如何16BYTE中的指令數少於4條的話,預解碼器就是瓶頸了.還有個更糟糕的情況,在預解碼器沒有完成 之前16BYTE字節代碼的處理之前,它是不會FETCH新的16字節的,比如,如果前16字節中有7條指令,那麼在第 一個周期完成6條指令的處理,在第2個周期完成剩下一條指令的處理,全部完成後才來處理接下來的新16字 節.任何跨越16字節邊界的指令將被留到下一個16字節做處理.

循環代碼緩沖

在CORE2中解碼器的代碼隊列可以當作尺寸為64字節的循環代碼緩沖來用,緩沖中被預 解碼的循環代碼可以被解碼器重復使用.所以如果一個循環的代碼都包含在64BYTE內,當然就一直用不著寓 解碼了,64字節緩沖可以當L0 CACHE使用,被組織為4行16字節.

解碼器

CORE2擁有4個解碼器,第一個 解碼器最多可以在一個周期內解碼一條產生4條UOPS的指令,其他的都只能解碼產生一條UOP的指令,如果指 令產生4條以上UOPS,則D0需要借助微碼ROM花費多個周期來解碼.解碼器可以在一個時鐘周期內從64字節緩 沖中讀2個16字節.因此32字節能在一次全部解碼完畢,但因為預解碼器的吞吐量最多只有16字節/C,只有當 解碼器在上周期處理的字節數少於16字節時,在下周期才可能處理多余16字節的代碼,因為上周期有積余, 而高達32字節/C只能在小的循環緩沖中得到,因為這時候的代碼的預解碼信息可以復用.

INTEL早期的處 理器在一個周期內能解碼的指令有前綴數量的要求,但在CORE2中這個限制消除了,唯一的限制就是指令長 度加上指令前綴長度不得超過15字節,除此之外,任何一個解碼器都能在一個周期內解碼任意多前綴的指令 (當然對響應UOPS數量有要求),當然沒有哪個指令需要多達14個前綴,但多余前綴可以用NOP操作代替,用來 作為循環入口16字節對齊.

微碼熔合

有些指令會被解碼為2條UOPS,在使用微碼熔合技術,可以上2條UOPS熔合為1條,這樣可以降 低內部帶寬使用,但微碼派發器會將熔合微碼當作2條UOPS分別發送不到不同的執行單元中去,但之後它依 然是單獨的一條微碼指令.微碼熔合有兩種情況,一種是讀-修改操作熔合,一種是寫熔合.比如ADD EAX, [MEM],這條指令包含2條微碼,一條讀內存,一條加操作,這2條微碼可以被熔合成1條,MOV [ESI+EDI], EAX, 著條寫操作指令也包含2條微碼,一條計算寫的物理地址,一條寫操作,它們也可以被熔合.CORE2比PM能更多 的熔合微碼,比如一條讀-操作-寫指令可以同時使用2種熔合,大部分XMM指令都能熔合,1條熔合的UOP有3個 輸入依賴,而普通的UOP只有2個,寫操作有2條UOPS,而讀操作只有1條?為什麼?

對於#pragma pack(n)和__declspec(align(n))的一些看法,其他pack和align本來在概念上就完全不 一樣,align是用來指定數據的對齊的位置,而pack指定的是為了達到對齊目的,所允許編譯器填充的最大空 白數目,注意pack只對結構性數據有影響.

看如下例子:

#pragma pack(4)
struct A
{
char c;
double d1;
short s;
}

首先我們來看MSDN上的一局話:

Unless overridden with __declspec(align(#)), the alignment of a scalar structure member is the minimum of its size and the current packing.

如何沒有 聲明__declspec(align(#)), 結構體中的數據成員對齊地址為其本身長度和pack長度中較小的那 個.

Unless overridden with __declspec(align(#)), the alignment of a structure is the maximum of the individual alignments of its member(s).

如果沒有聲明__declspec(align(#)), 結構體本身的對齊地址為起所有成員中對齊長度最大的那個.

A structure member is placed at an offset from the beginning of its parent structure which is the smallest multiple of its alignment greater than or equal to the offset of the end of the previous member.

一個結構 體類型的成員被放置在 上父結構體一個成員之後的地址 這個地址是結構體本身對齊長度的整數 倍.

The size of a structure is the smallest multiple of its alignment larger greater than or equal the offset of the end of its last member.

結構體尺寸大於或等於其最後一個成員的偏 移,為其對齊的最小倍數

如果我沒理解錯的話,這個A結構的對齊地址應該是8的倍數,而與pack(n)無關.在來看看他的成員c的地 址是8的倍數,d1按道理應該也是8的倍數,這樣在d1之前必須填充7個空白,但是我們指定了最多只能填4個 空白,沒辦法,編譯填了3個空白,讓位置對齊地址為4的整數倍.

參考:

http://msdn.microsoft.com/en- us/library/aa290049.aspx

http://msdn.microsoft.com/en-us/library/83ythb65 (VS.80).aspx#vclrfalignexamples

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved