併發研究之CPU緩存一致性協議(MESI)

CPU緩存一致性協議MESI

CPU高速緩存(Cache Memory)

CPU爲什麼要有高速緩存

CPU在摩爾定律的指導下以每18個月翻一番的速度在發展,然而內存和硬盤的發展速度遠遠不及CPU。這就形成了高性能能的內存和硬盤價格及其昂貴。然而CPU的高度運算須要高速的數據。爲了解決這個問題,CPU廠商在CPU中內置了少許的高速緩存以解決I\O速度和CPU運算速度之間的不匹配問題。html

在CPU訪問存儲設備時,不管是存取數據抑或存取指令,都趨於彙集在一片連續的區域中,這就被稱爲局部性原理。java

時間局部性(Temporal Locality):若是一個信息項正在被訪問,那麼在近期它極可能還會被再次訪問。程序員

好比循環、遞歸、方法的反覆調用等。數組

空間局部性(Spatial Locality):若是一個存儲器的位置被引用,那麼未來他附近的位置也會被引用。緩存

好比順序執行的代碼、連續建立的兩個對象、數組等。安全

帶有高速緩存的CPU執行計算的流程

  1. 程序以及數據被加載到主內存性能

  2. 指令和數據被加載到CPU的高速緩存優化

  3. CPU執行指令,把結果寫到高速緩存設計

  4. 高速緩存中的數據寫回主內存3d

目前流行的多級緩存結構

因爲CPU的運算速度超越了1級緩存的數據I\O能力,CPU廠商又引入了多級的緩存結構。

多級緩存結構

多核CPU多級緩存一致性協議MESI

多核CPU的狀況下有多個一級緩存,如何保證緩存內部數據的一致,不讓系統數據混亂。這裏就引出了一個一致性的協議MESI。

MESI協議緩存狀態

MESI 是指4中狀態的首字母。每一個Cache line有4個狀態,可用2個bit表示,它們分別是:

緩存行(Cache line):緩存存儲數據的單元。

狀態 描述 監放任務
M 修改 (Modified) 該Cache line有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。 緩存行必須時刻監聽全部試圖讀該緩存行相對就主存的操做,這種操做必須在緩存將該緩存行寫回主存並將狀態變成S(共享)狀態以前被延遲執行。
E 獨享、互斥 (Exclusive) 該Cache line有效,數據和內存中的數據一致,數據只存在於本Cache中。 緩存行也必須監聽其它緩存讀主存中該緩存行的操做,一旦有這種操做,該緩存行須要變成S(共享)狀態。
S 共享 (Shared) 該Cache line有效,數據和內存中的數據一致,數據存在於不少Cache中。 緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。
I 無效 (Invalid) 該Cache line無效。

注意:
對於M和E狀態而言老是精確的,他們在和該緩存行的真正狀態是一致的,而S狀態多是非一致的。若是一個緩存將處於S狀態的緩存行做廢了,而另外一個緩存實際上可能已經獨享了該緩存行,可是該緩存卻不會將該緩存行升遷爲E狀態,這是由於其它緩存不會廣播他們做廢掉該緩存行的通知,一樣因爲緩存並無保存該緩存行的copy的數量,所以(即便有這種通知)也沒有辦法肯定本身是否已經獨享了該緩存行。

從上面的意義看來E狀態是一種投機性的優化:若是一個CPU想修改一個處於S狀態的緩存行,總線事務須要將全部該緩存行的copy變成invalid狀態,而修改E狀態的緩存不須要使用總線事務。

MESI狀態轉換

理解該圖的前置說明:
1.觸發事件

觸發事件 描述
本地讀取(Local read) 本地cache讀取本地cache數據
本地寫入(Local write) 本地cache寫入本地cache數據
遠端讀取(Remote read) 其餘cache讀取本地cache數據
遠端寫入(Remote write) 其餘cache寫入本地cache數據

2.cache分類:
前提:全部的cache共同緩存了主內存中的某一條數據。

本地cache:指當前cpu的cache。
觸發cache:觸發讀寫事件的cache。
其餘cache:指既除了以上兩種以外的cache。
注意:本地的事件觸發 本地cache和觸發cache爲相同。

上圖的切換解釋:

狀態 觸發本地讀取 觸發本地寫入 觸發遠端讀取 觸發遠端寫入
M狀態(修改) 本地cache:M
觸發cache:M
其餘cache:I
本地cache:M
觸發cache:M
其餘cache:I
本地cache:M→E→S
觸發cache:I→S
其餘cache:I→S
同步主內存後修改成E獨享,同步觸發、其餘cache後本地、觸發、其餘cache修改成S共享
本地cache:M→E→S→I
觸發cache:I→S→E→M
其餘cache:I→S→I
同步和讀取同樣,同步完成後觸發cache改成M,本地、其餘cache改成I
E狀態(獨享) 本地cache:E
觸發cache:E
其餘cache:I
本地cache:E→M
觸發cache:E→M
其餘cache:I
本地cache變動爲M,其餘cache狀態應當是I(無效)
本地cache:E→S
觸發cache:I→S
其餘cache:I→S
當其餘cache要讀取該數據時,其餘、觸發、本地cache都被設置爲S(共享)
本地cache:E→S→I
觸發cache:I→S→E→M
其餘cache:I→S→I
當觸發cache修改本地cache獨享數據時時,將本地、觸發、其餘cache修改成S共享.而後觸發cache修改成獨享,其餘、本地cache修改成I(無效),觸發cache再修改成M
S狀態(共享) 本地cache:S
觸發cache:S
其餘cache:S
本地cache:S→E→M
觸發cache:S→E→M
其餘cache:S→I
當本地cache修改時,將本地cache修改成E,其餘cache修改成I,而後再將本地cache爲M狀態
本地cache:S
觸發cache:S
其餘cache:S
本地cache:S→I
觸發cache:S→E→M
其餘cache:S→I
當觸發cache要修改本地共享數據時,觸發cache修改成E(獨享),本地、其餘cache修改成I(無效),觸發cache再次修改成M(修改)
I狀態(無效) 本地cache:I→S或者I→E
觸發cache:I→S或者I →E
其餘cache:E、M、I→S、I
本地、觸發cache將從I無效修改成S共享或者E獨享,其餘cache將從E、M、I 變爲S或者I
本地cache:I→S→E→M
觸發cache:I→S→E→M
其餘cache:M、E、S→S→I
既然是本cache是I,其餘cache操做與它無關 既然是本cache是I,其餘cache操做與它無關

下圖示意了,當一個cache line的調整的狀態的時候,另一個cache line 須要調整的狀態。

M E S I
M × × ×
E × × ×
S × ×
I

舉個栗子來講:

假設cache 1 中有一個變量x = 0的cache line 處於S狀態(共享)。
那麼其餘擁有x變量的cache 二、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的風險

第1、就是處理器會嘗試從存儲緩存(Store buffer)中讀取值,但它尚未進行提交。這個的解決方案稱爲Store Forwarding,它使得加載的時候,若是存儲緩存中存在,則進行返回。
第2、保存何時會完成,這個並無任何保證。

value = 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;
}

如今確實安全了。完美無暇!

後記

然而,對於程序員來講簡直是一個災難。不想和平臺耦合咱們要跨平臺。Write One,Run Everywhere!
幸虧java解決了這個問題,至於如何解決的請關注JMM(JavaMemoryMode)與物理內存相愛相殺。

引用文章

深刻Java內存模型

相關文章
相關標籤/搜索