cache一致性裡的MESI協議
前言
在有多個核的處理器的處理器中,每個核都有自己的cache,而如何確保多個核的cache內容的一致則是一個很容易遇到的問題,MESI協議就是一個專門用來解決cache一致性的協議。很多處理器使用的都是MESI協議或者MESI協議的變體,而MESI協議其實也是MSI協議的變種。MESI協議採用了回寫(write-back)的策略來更新cache,使得其效能進一步提高,但也帶來了額外的風險,回寫帶來的問題可以在編寫程式時使用記憶體屏障來規避。
MESI協議簡介
MESI協議名字的由來是由其描述的四個cache狀態組成的,分別是M(modified)、E(exclusive)、S(shared)和I(invalid)。各個狀態的描述具體如下:
狀態 | 描述 |
---|---|
Modified | 當前cache的內容有效,資料已被修改而且與記憶體中的資料不一致,資料只在當前cache裡存在 |
Exclusive | 當前cache的內容有效,資料與記憶體中的資料一致,資料只在當前cache裡存在 |
Shared | 當前cache的內容有效,資料與記憶體中的資料一致,資料在多個cache裡存在 |
Invalid | 當前cache無效 |
狀態轉移
MESI協議其實是一個狀態機,cache的狀態會跟根據外部事件的刺激而發生轉移,具體的事件分為兩類:處理器對cache的請求和匯流排對cache的請求:
- PrRd: 處理器請求讀一個快取塊
- PrWr: 處理器請求寫一個快取塊
- BusRd: 窺探器請求指出其他處理器請求讀一個快取塊
- BusRdX: 窺探器請求指出其他處理器請求寫一個該處理器不擁有的快取塊
- BusUpgr: 窺探器請求指出其他處理器請求寫一個該處理器擁有的快取塊
- Flush: 窺探器請求指出請求回寫整個快取到主存
- FlushOpt: 窺探器請求指出整個快取塊被髮到匯流排以傳送給另外一個處理器(快取到快取的複製)
而狀態之間的轉換如下圖:
初始狀態 | 操作 | 響應 |
---|---|---|
Invalid(I) | PrRd |
|
PrWr |
|
|
Exclusive(E) | PrRd |
|
PrWr |
|
|
Shared(S) | PrRd |
|
PrWr |
|
|
Modified(M) | PrRd |
|
PrWr |
|
初始狀態 | 操作 | 響應 |
---|---|---|
Invalid(I) | BusRd |
|
BusRdX/BusUpgr |
|
|
Exclusive(E) | BusRd |
|
BusRdX |
|
|
Shared(S) | BusRd |
|
BusRdX |
|
|
Modified(M) | BusRd |
|
BusRdX |
|
記憶體屏障的引入
MESI的設計比較簡單直接,但是其中有兩個地方會導致效能下降:一是更新invalidate狀態的cache時,需要嘗試從其他cpu甚至是記憶體獲取最新的資料;二是使一個cache變為invalidate時需要等待其他cpu的確認;這兩個操作都是比較耗時的,如果cpu在這兩個過程中一直等待的話,就會形成浪費。
store buffer
為了降低寫入invalidate狀態的cache的延時,可以引入store buffer。既然寫入操作無論如何一定會發生,那麼cpu就先發出訊號通知其他cpu這個cache已經失效,然後再將本次的寫操作更新到store buffer中,等到其他cpu都確認收到訊號後再將結果寫到記憶體中。
這樣就避免了更新cache時阻塞等待其他cpu確認的耗時,但是也會導致cpu的更新並沒有及時寫入cache,所以當cpu需要讀取cache時,它需要先確認store buffer中是否有所需的資料,這個機製成為store forwarding。值得注意的是,當cpu在讀寫自己的store buffer時,對應的資料變更其他cpu是感知不到的。
invalidate queue
當cpu收到使某個cache失效的訊息時,預期的行為是cpu馬上執行這個失效操作。但實際上cpu並不會馬上執行失效操作,而是先傳送確認收到的訊息,然後將失效操作加入到invalidate queue中,queue中的操作隨後會在適當的時刻執行(並不一定是馬上)。之所以需要invalidate queue同樣是因為invalidate操作開銷比較大,cpu為了執行invalidate操作必須丟棄cache,導致cache命中率下降。這樣的好處是能夠提高cpu的效能,但同時也導致cache中可能存在過期的資料。
記憶體屏障
針對store buffer和invalidate queue這兩個優化帶來的問題,我們又提供了記憶體屏障作為解決方案。記憶體屏障交給了編寫程式的人的手裡,利用它就可以規避上面提到的問題。
記憶體屏障分為寫屏障和讀屏障,編寫程式時可以在期望的地方加入記憶體屏障。寫屏障會強制cpu清空store buffer的內容,也就是將所有的變更都寫入cache,隨後變更也就寫入了記憶體,使其對其他cpu可見;讀屏障會強制cpu執行invalidate queue中的所有invalidate操作,使自身的cache內容失效,從而使cpu從記憶體或者其他cpu中獲取最新的cache資料。
其他
MESI協議乍一看和java裡的記憶體模型以及volatile關鍵字有些相似,後續再詳細展開了。