一、什么是CPU緩存
1. CPU緩存的來歷
眾所周知,CPU是計算機的大腦,它負責執行程序的指令,而內存負責存數據, 包括程序自身的數據。在很多年前,CPU的頻率與內存總線的頻率在同一層面上。內存的訪問速度僅比寄存器慢一些。但是,這一局面在上世紀90年代被打破了。CPU的頻率大大提升,但內存總線的頻率與內存芯片的性能卻沒有得到成比例的提升。並不是因為造不出更快的內存,只是因為太貴了。內存如果要達到目前CPU那樣的速度,那么它的造價恐怕要貴上好幾個數量級。所以,CPU的運算速度要比內存讀寫速度快很多,這樣會使CPU花費很長的時間等待數據的到來或把數據寫入到內存中。所以,為了解決CPU運算速度與內存讀寫速度不匹配的矛盾,就出現了CPU緩存。
2. CPU緩存的概念
CPU緩存是位於CPU與內存之間的臨時數據交換器,它的容量比內存小的多但是交換速度卻比內存要快得多。CPU緩存一般直接跟CPU芯片集成或位於主板總線互連的獨立芯片上。
為了簡化與內存之間的通信,高速緩存控制器是針對數據塊,而不是字節進行操作的。高速緩存其實就是一組稱之為緩存行(Cache Line)的固定大小的數據塊組成的,緩存存儲數據的單元,典型的一行是64
字節。
3. CPU緩存的意義
CPU往往需要重復處理相同的數據、重復執行相同的指令,如果這部分數據、指令CPU能在CPU緩存中找到,CPU就不需要從內存或硬盤中再讀取數據、指令,從而減少了整機的響應時間。所以,緩存的意義滿足以下兩種局部性原理:
- 時間局部性(Temporal Locality):如果一個信息項正在被訪問,那么在近期它很可能還會被再次訪問。
- 空間局部性(Spatial Locality):如果一個存儲器的位置被引用,那么將來他附近的位置也會被引用。
二、CPU的三級緩存
1. CPU的三級緩存
隨着多核CPU的發展,CPU緩存通常分成了三個級別:L1
,L2
,L3
。級別越小越接近CPU,所以速度也更快,同時也代表着容量越小。L1 是最接近CPU的, 它容量最小(例如:32K
),速度最快,每個核上都有一個 L1 緩存,L1 緩存每個核上其實有兩個 L1 緩存, 一個用於存數據的 L1d Cache(Data Cache),一個用於存指令的 L1i Cache(Instruction Cache)。L2 緩存 更大一些(例如:256K
),速度要慢一些, 一般情況下每個核上都有一個獨立的L2 緩存; L3 緩存是三級緩存中最大的一級(例如3MB),同時也是最慢的一級, 在同一個CPU插槽之間的核共享一個 L3 緩存。
下面是三級緩存的處理速度參考表:
從CPU到 | 大約需要的CPU周期 | 大約需要的時間(單位ns) |
---|---|---|
寄存器 | 1 cycle | |
L1 Cache | ~3-4 cycles | ~0.5-1 ns |
L2 Cache | ~10-20 cycles | ~3-7 ns |
L3 Cache | ~40-45 cycles | ~15 ns |
跨槽傳輸 | ~20 ns | |
內存 | ~120-240 cycles | ~60-120ns |
下圖是Intel Core i5-4285U的CPU三級緩存示意圖:
就像數據庫緩存一樣,獲取數據時首先會在最快的緩存中找數據,如果緩存沒有命中(Cache miss) 則往下一級找, 直到三級緩存都找不到時,那只有向內存要數據了。一次次地未命中,代表取數據消耗的時間越長。
2. 帶有高速緩存CPU執行計算的流程
1.將程序和數據從硬盤加載到內存中
2.將程序和數據從內存加載到緩存中(目前多三級緩存,數據加載順序:L3->L2->L1)
3.CPU將緩存中的數據加載到寄存器中,並進行運算
4.CPU會將數據刷新回緩存,並在一定的時間周期之后刷新回內存
三、CPU緩存一致性協議(MESI)
多核CPU的情況下有多個一級緩存,如何保證緩存內部數據的一致,不讓系統數據混亂。解決方案比較常見的緩存一致性(MESI), 當然也有鎖住總線。這里就引出了一個一致性的協議MESI。
MESI(Modified Exclusive Shared Or Invalid
)(也稱為伊利諾斯協議,是因為該協議由伊利諾斯州立大學提出的)是一種廣泛使用的支持寫回策略的緩存一致性協議。為了保證多個CPU緩存中共享數據的一致性,定義了緩存行(Cache Line)的四種狀態,而CPU對緩存行的四種操作可能會產生不一致的狀態,因此緩存控制器監聽到本地操作和遠程操作的時候,需要對地址一致的緩存行的狀態進行一致性修改,從而保證數據在多個緩存之間保持一致性。
1. MESI協議中的狀態
CPU中每個緩存行(Caceh line)使用4
種狀態進行標記,使用2bit
來表示:
狀態 | 描述 | 監聽任務 | 狀態轉換 |
---|---|---|---|
M 修改 (Modified) | 該Cache line有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。 | 緩存行必須時刻監聽所有試圖讀該緩存行相對就主存的操作,這種操作必須在緩存將該緩存行寫回主存並將狀態變成S(共享)狀態之前被延遲執行。 | 當被寫回主存之后,該緩存行的狀態會變成獨享(exclusive)狀態。 |
E 獨享、互斥 (Exclusive) | 該Cache line有效,數據和內存中的數據一致,數據只存在於本Cache中。 | 緩存行也必須監聽其它緩存讀主存中該緩存行的操作,一旦有這種操作,該緩存行需要變成S(共享)狀態。 | 當CPU修改該緩存行中內容時,該狀態可以變成Modified狀態 |
S 共享 (Shared) | 該Cache line有效,數據和內存中的數據一致,數據存在於很多Cache中。 | 緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。 | 當有一個CPU修改該緩存行時,其它CPU中該緩存行可以被作廢(變成無效狀態 Invalid)。 |
I 無效 (Invalid) | 該Cache line無效。 | 無 | 無 |
注意:
對於M和E狀態而言總是精確的,他們在和該緩存行的真正狀態是一致的,而S狀態可能是非一致的。如果一個緩存將處於S狀態的緩存行作廢了,而另一個緩存實際上可能已經獨享了該緩存行,但是該緩存卻不會將該緩存行升遷為E狀態,這是因為其它緩存不會廣播他們作廢掉該緩存行的通知,同樣由於緩存並沒有保存該緩存行的copy的數量,因此(即使有這種通知)也沒有辦法確定自己是否已經獨享了該緩存行。
從上面的意義看來E狀態是一種投機性的優化:如果一個CPU想修改一個處於S狀態的緩存行,總線事務需要將所有該緩存行的copy變成invalid狀態,而修改E狀態的緩存不需要使用總線事務。
MESI狀態轉換圖:
下圖表示了當一個緩存行(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
狀態(無效)。
2. 多核緩存協同操作
(1) 內存變量
假設有三個CPU A、B、C,對應三個緩存分別是cache a、b、c。在主內存中定義了x
的引用值為0。
(2) 單核讀取
執行流程是:
- CPU A發出了一條指令,從主內存中讀取
x
。 - 從主內存通過 bus 讀取到 CPU A 的緩存中(遠端讀取 Remote read),這時該 Cache line 修改為 E 狀態(獨享)。
(3) 雙核讀取
執行流程是:
- 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狀態(共享)。
(4) 修改數據
執行流程是:
- CPU A 計算完成后發指令需要修改
x
. - CPU A 將
x
設置為M狀態(修改)並通知緩存了x
的 CPU B, CPU B 將本地 cache b 中的x
設置為I
狀態(無效) - CPU A 對
x
進行賦值。
(5) 同步數據
那么執行流程是:
- CPU B 發出了要讀取x的指令。
- CPU B 通知CPU A,CPU A將修改后的數據同步到主內存時cache a 修改為E(獨享)
- CPU A同步CPU B的x,將cache a和同步后cache b中的x設置為S狀態(共享)。
3. CPU 存儲模型簡介
MESI協議為了保證多個 CPU cache 中共享數據的一致性,定義了 Cache line 的四種狀態,而 CPU 對 cache 的4
種操作可能會產生不一致狀態,因此 cache 控制器監聽到本地操作和遠程操作的時候,需要對地址一致的 Cache line 狀態做出一定的修改,從而保證數據在多個cache之間流轉的一致性。
但是,緩存的一致性消息傳遞是要時間的,這就使得狀態切換會有更多的延遲。某些狀態的切換需要特殊的處理,可能會阻塞處理器。這些都將會導致各種各樣的穩定性和性能問題。比如你需要修改本地緩存中的一條信息,那么你必須將I
(無效)狀態通知到其他擁有該緩存數據的CPU緩存中,並且等待確認。等待確認的過程會阻塞處理器,這會降低處理器的性能。因為這個等待遠遠比一個指令的執行時間長的多。所以,為了為了避免這種阻塞導致時間的浪費,引入了存儲緩存(Store Buffer
)和無效隊列(Invalidate Queue
)。
(1) 存儲緩存
在沒有存儲緩存時,CPU 要寫入一個量,有以下情況:
- 量不在該 CPU 緩存中,則需要發送 Read Invalidate 信號,再等待此信號返回,之后再寫入量到緩存中。
- 量在該 CPU 緩存中,如果該量的狀態是 Exclusive 則直接更改。而如果是 Shared 則需要發送 Invalidate 消息讓其它 CPU 感知到這一更改后再更改。
這些情況中,很有可能會觸發該 CPU 與其它 CPU 進行通訊,接着需要等待它們回復。這會浪費大量的時鍾周期!為了提高效率,可以使用異步的方式去處理:先將值寫入到一個 Buffer 中,再發送通訊的信號,等到信號被響應,再應用到 cache 中。並且此 Buffer 能夠接受該 CPU 讀值。這個 Buffer 就是 Store Buffer。而不須要等待對某個量的賦值指令的完成才繼續執行下一條指令,直接去 Store Buffer 中讀該量的值,這種優化叫Store Forwarding。
參考:https://blinkfox.github.io/2018/11/18/ruan-jian-gong-ju/cpu-duo-ji-huan-cun/