前言
為什么需要CPU緩存?
答:CPU的頻率太快了,快到主存趕不上,這樣在處理器時鍾周期內,CPU常常要等待主存,浪費了資源。所以緩存的出現,是為了緩解CPU和內存之間速度不匹配的問題。(結構:cpu->cache->memory)
主體概要
-
CPU高級緩存-緩存一致性(MESI協議)概念
-
帶有高速緩存的CPU執行計算的流程
-
目前流行的多級緩存結構
-
MESI協議緩存狀態
-
多核緩存協同操作
-
MESI優化和他們引入的問題
-
CPU切換狀態阻塞解決-存儲緩存
- 硬件內存模型
主體內容
一、CPU多級緩存
1.CPU高級緩存-緩存一致性(MESI協議)概念
1.CPU緩存有什么意義?
答: 1) 時間局部性:如果某個數據被訪問,那么在不久的將來它有可能被再次訪問。
2)空間局部性:如果某個數據被訪問,那么與它相鄰的數據很快也可能被訪問。
2.緩存一致性(MESI):用於保證多個CPU cache之間緩存共享數據的一致。
M:Modifed(被修改) :該緩存行只被緩存在該CPU的緩存中,並且是被修改過的,因此,他與主存中的數據是不一致的,該緩存行中的內存需要在未來的某個時間點寫回主存,這個時間點我們是允許其他CPU讀取主存中相應的內存之前,當這里面的值被寫回主存之后,該緩存行的狀態會變成E這個狀態,也就是“獨享”。
E:Exclusive(獨享):它的緩存行只被緩存在該CPU的緩存中,它是未被修改過的,是與主存中的數據一致的,這個狀態是在任何時刻,當有其他CPU讀取該內存時,變成S,也就是變成“共享”狀態。
S:shared(共享) :它意味着該緩存行可能被多個CPU進行緩存,並且各緩存中的數據與主存數據是一致的,當有一個CPU修改該緩存行的時候,其他CPU中該緩存行是可以被作廢的,變成I,也就是“無效的”狀態。
I:Invalid(無效的):代表這個緩存是無效的,可能是有其他CPU修改了該緩存行。
還有種更便於理解的概念解釋:
M: 被修改(Modified)
該緩存行只被緩存在該CPU的緩存中,並且是被修改過的(dirty),即與主存中的數據不一致,該緩存行中的內存需要在未來的某個時間點(允許其它CPU讀取請主存中相應內存之前)寫回(write back)主存。
當被寫回主存之后,該緩存行的狀態會變成獨享(exclusive)狀態。
E: 獨享的(Exclusive)
該緩存行只被緩存在該CPU的緩存中,它是未被修改過的(clean),與主存中數據一致。該狀態可以在任何時刻當有其它CPU讀取該內存時變成共享狀態(shared)。
同樣地,當CPU修改該緩存行中內容時,該狀態可以變成Modified狀態。
S: 共享的(Shared)
該狀態意味着該緩存行可能被多個CPU緩存,並且各個緩存中的數據與主存數據一致(clean),當有一個CPU修改該緩存行中,其它CPU中該緩存行可以被作廢(變成無效狀態(Invalid))。
I: 無效的(Invalid)
該緩存是無效的(可能有其它CPU修改了該緩存行)。
local read:讀本地緩存中的數據。
local write:將數據寫到本地的緩存里面。
remote read:將內存中的數據讀取過來。
remote write:將數據寫回到主存里面去。
(狀態轉換關系圖)
2.帶有高速緩存的CPU執行計算的流程
-
程序以及數據被加載到主內存
-
指令和數據被加載到CPU的高速緩存
-
CPU執行指令,把結果寫到高速緩存
-
高速緩存中的數據寫回主內存

3.目前流行的多級緩存結構
由於CPU的運算速度超越了1級緩存的數據I\O能力,CPU廠商又引入了多級的緩存結構。
多級緩存結構

4.多核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 2、cache 3等x的cache line調整為S狀態(共享)或者調整為 I 狀態(無效)。
5.多核緩存協同操作
假設有三個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狀態(共享)。
6.MESI優化和他們引入的問題
緩存的一致性消息傳遞是要時間的,這就使其切換時會產生延遲。當一個緩存被切換狀態時其他緩存收到消息完成各自的切換並且發出回應消息這么一長串的時間中CPU都會等待所有緩存響應完成。可能出現的阻塞都會導致各種各樣的性能問題和穩定性問題。
7.CPU切換狀態阻塞解決-存儲緩存(Store Bufferes)
比如你需要修改本地緩存中的一條信息,那么你必須將I(無效)狀態通知到其他擁有該緩存數據的CPU緩存中,並且等待確認。等待確認的過程會阻塞處理器,這會降低處理器的性能。應為這個等待遠遠比一個指令的執行時間長的多。
Store Bufferes
為了避免這種CPU運算能力的浪費,Store Bufferes被引入使用。處理器把它想要寫入到主存的值寫到緩存,然后繼續去處理其他事情。當所有失效確認(Invalidate Acknowledge)都接收到時,數據才會最終被提交。
這么做有兩個風險
Store Bufferes的風險
第一、就是處理器會嘗試從存儲緩存(Store buffer)中讀取值,但它還沒有進行提交。這個的解決方案稱為Store Forwarding,它使得加載的時候,如果存儲緩存中存在,則進行返回。
第二、保存什么時候會完成,這個並沒有任何保證。
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的設計是非常相像的。
8.硬件內存模型
執行失效也不是一個簡單的操作,它需要處理器去處理。另外,存儲緩存(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;
}
現在確實安全了。完美無暇!
