1. 程式人生 > 程式設計 >一篇文章讓你明白CPU快取一致性協議MESI

一篇文章讓你明白CPU快取一致性協議MESI

CPU快取記憶體(Cache Memory)

CPU為何要有快取記憶體

CPU在摩爾定律的指導下以每18個月翻一番的速度在發展,然而記憶體和硬碟的發展速度遠遠不及CPU。這就造成了高效能能的記憶體和硬碟價格及其昂貴。然而CPU的高度運算需要高速的資料。為瞭解決這個問題,CPU廠商在CPU中內建了少量的快取記憶體以解決I\O速度和CPU運算速度之間的不匹配問題。 在CPU訪問儲存裝置時,無論是存取資料抑或存取指令,都趨於聚集在一片連續的區域中,這就被稱為區域性性原理。 時間區域性性(Temporal Locality):如果一個資訊項正在被訪問,那麼在近期它很可能還會被再次訪問。比如迴圈、遞迴、方法的反覆呼叫等。 空間區域性性(Spatial Locality):
如果一個儲存器的位置被引用,那麼將來他附近的位置也會被引用。比如順序執行的程式碼、連續建立的兩個物件、陣列等。

帶有快取記憶體的CPU執行計算的流程

  1. 程式以及資料被載入到主記憶體
  2. 指令和資料被載入到CPU的快取記憶體
  3. CPU執行指令,把結果寫到快取記憶體
  4. 快取記憶體中的資料寫回主記憶體

目前流行的多級快取結構

由於CPU的運算速度超越了1級快取的資料I\O能力,CPU廠商又引入了多級的快取結構。 多級快取結構

多核CPU多級快取一致性協議MESI

多核CPU的情況下有多個一級快取,如何保證快取內部資料的一致,不讓系統資料混亂。這裡就引出了一個一致性的協議MESI。

MESI協議快取狀態

MESI 是指4中狀態的首字母。每個Cache line有4個狀態,可用2個bit表示,它們分別是: 快取行(Cache line):快取儲存資料的單元。
注意: 對於M和E狀態而言總是精確的,他們在和該快取行的真正狀態是一致的,而S狀態可能是非一致的。如果一個快取將處於S狀態的快取行作廢了,而另一個快取實際上可能已經獨享了該快取行,但是該快取卻不會將該快取行升遷為E狀態,這是因為其它快取不會廣播他們作廢掉該快取行的通知,同樣由於快取並沒有儲存該快取行的copy的數量,因此(即使有這種通知)也沒有辦法確定自己是否已經獨享了該快取行。 從上面的意義看來E狀態是一種投機性的優化:如果一個CPU想修改一個處於S狀態的快取行,匯流排事務需要將所有該快取行的copy變成invalid狀態,而修改E狀態的快取不需要使用匯流排事務。

MESI狀態轉換


理解該圖的前置說明: 1.觸發事件
2.cache分類: 前提:所有的cache共同快取了主記憶體中的某一條資料。 本地cache:指當前cpu的cache。 觸發cache:觸發讀寫事件的cache。 其他cache:指既除了以上兩種之外的cache。 注意:本地的事件觸發 本地cache和觸發cache為相同。 上圖的切換解釋:
下圖示意了,當一個cache line的調整的狀態的時候,另外一個cache line 需要調整的狀態。
舉個栗子來說: 假設cache 1 中有一個變數x = 0的cache line 處於S狀態(共享)。 那麼其他擁有x變數的cache 2、cache 3等x的cache line調整為S狀態(共享)或者調整為 I 狀態(無效)。

多核快取協同操作

假設有三個CPU A、B、C,對應三個快取分別是cache a、b、 c。在主記憶體中定義了x的引用值為0。

單核讀取

那麼執行流程是: CPU A發出了一條指令,從主記憶體中讀取x。 從主記憶體通過bus讀取到快取中(遠端讀取Remote read),這是該Cache line修改為E狀態(獨享).

雙核讀取

那麼執行流程是: CPU A發出了一條指令,從主記憶體中讀取x。 CPU A從主記憶體通過bus讀取到 cache a中並將該cache line 設定為E狀態。 CPU B發出了一條指令,從主記憶體中讀取x。 CPU B試圖從主記憶體中讀取x時,CPU A檢測到了地址衝突。這時CPU A對相關資料做出響應。此時x 儲存於cache a和cache b中,x在chche a和cache b中都被設定為S狀態(共享)。

修改資料

那麼執行流程是: CPU A 計算完成後發指令需要修改x. CPU A 將x設定為M狀態(修改)並通知快取了x的CPU B,CPU B將本地cache b中的x設定為I狀態(無效) CPU A 對x進行賦值。

同步資料

那麼執行流程是: CPU B 發出了要讀取x的指令。 CPU B 通知CPU A,CPU A將修改後的資料同步到主記憶體時cache a 修改為E(獨享) CPU A同步CPU B的x,將cache a和同步後cache b中的x設定為S狀態(共享)。

MESI優化和他們引入的問題

快取的一致性訊息傳遞是要時間的,這就使其切換時會產生延遲。當一個快取被切換狀態時其他快取收到訊息完成各自的切換並且發出迴應訊息這麼一長串的時間中CPU都會等待所有快取響應完成。可能出現的阻塞都會導致各種各樣的效能問題和穩定性問題。


CPU切換狀態阻塞解決-儲存快取(Store Bufferes)

比如你需要修改本地快取中的一條資訊,那麼你必須將I(無效)狀態通知到其他擁有該快取資料的CPU快取中,並且等待確認。等待確認的過程會阻塞處理器,這會降低處理器的效能。應為這個等待遠遠比一個指令的執行時間長的多。


Store Bufferes

為了避免這種CPU運算能力的浪費,Store Bufferes被引入使用。處理器把它想要寫入到主存的值寫到快取,然後繼續去處理其他事情。當所有失效確認(Invalidate Acknowledge)都接收到時,資料才會最終被提交。 這麼做有兩個風險


Store Bufferes的風險

第一、就是處理器會嘗試從儲存快取(Store buffer)中讀取值,但它還沒有進行提交。這個的解決方案稱為Store Forwarding,它使得載入的時候,如果儲存快取中存在,則進行返回。 第二、儲存什麼時候會完成,這個並沒有任何保證。
alue = 3;
void exeToCPUA(){
  value = 10;
  isFinsh = true;
}
void exeToCPUB(){
  if(isFinsh){
    //value一定等於10?!
    assert value == 10;
  }
}複製程式碼
試想一下開始執行時,CPU A儲存著finished在E(獨享)狀態,而value並沒有儲存在它的快取中。(例如,Invalid)。在這種情況下,value會比finished更遲地拋棄儲存快取。完全有可能CPU B讀取finished的值為true,而value的值不等於10。 即isFinsh的賦值在value賦值之前。 這種在可識別的行為中發生的變化稱為重排序(reordings)。注意,這不意味著你的指令的位置被惡意(或者好意)地更改。 它只是意味著其他的CPU會讀到跟程式中寫入的順序不一樣的結果。 順便提一下NIO的設計和Store Bufferes的設計是非常相像的。


硬體記憶體模型

執行失效也不是一個簡單的操作,它需要處理器去處理。另外,儲存快取(Store Buffers)並不是無窮大的,所以處理器有時需要等待失效確認的返回。這兩個操作都會使得效能大幅降低。為了應付這種情況,引入了失效佇列。它們的約定如下:
  • 對於所有的收到的Invalidate請求,Invalidate Acknowlege訊息必須立刻傳送
  • Invalidate並不真正執行,而是被放在一個特殊的佇列中,在方便的時候才會去執行。
  • 處理器不會傳送任何訊息給所處理的快取條目,直到它處理Invalidate。
即便是這樣處理器已然不知道什麼時候優化是允許的,而什麼時候並不允許。 乾脆處理器將這個任務丟給了寫程式碼的人。這就是記憶體屏障(Memory Barriers)。
寫屏障 Store Memory Barrier(a.k.a. ST,SMB,smp_wmb)是一條告訴處理器在執行這之後的指令之前,應用所有已經在儲存快取(store buffer)中的儲存的指令。 讀屏障Load Memory Barrier (a.k.a. LD,RMB,smp_rmb)是一條告訴處理器在執行任何的載入前,先應用所有已經在失效佇列中的失效操作的指令。
void executedOnCpu0() {
    value = 10;
    //在更新資料之前必須將所有儲存快取(store buffer)中的指令執行完畢。
    storeMemoryBarrier();
    finished = true;
}
void executedOnCpu1() {
    while(!finished);
    //在讀取之前將所有失效佇列中關於該資料的指令執行完畢。
    loadMemoryBarrier();
    assert value == 10;
}複製程式碼

最後

歡迎大家關注我的公種浩【程式設計師追風】,整理了1000道2019年多家公司java面試題400多頁pdf檔案,文章都會在裡面更新,整理的資料也會放在裡面。喜歡文章記得點個贊,感謝支援!