1. 程式人生 > 程式設計 >cache一致性裡的MESI協議

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的請求:

  1. PrRd: 處理器請求讀一個快取塊
  2. PrWr: 處理器請求寫一個快取塊
  3. BusRd: 窺探器請求指出其他處理器請求讀一個快取塊
  4. BusRdX: 窺探器請求指出其他處理器請求寫一個該處理器不擁有的快取塊
  5. BusUpgr: 窺探器請求指出其他處理器請求寫一個該處理器擁有的快取塊
  6. Flush: 窺探器請求指出請求回寫整個快取到主存
  7. FlushOpt: 窺探器請求指出整個快取塊被髮到匯流排以傳送給另外一個處理器(快取到快取的複製)

而狀態之間的轉換如下圖:

處理器操作帶來的狀態轉化
初始狀態 操作 響應
Invalid(I) PrRd
  • 給匯流排發BusRd訊號
  • 其他處理器看到BusRd,檢查自己是否有有效的資料副本,通知發出請求的快取
  • 如果其他快取有有效的副本,狀態轉換為(S)Shared
  • 如果其他快取都沒有有效的副本,狀態轉換為(E)Exclusive
  • 如果其他快取有有效的副本,其中一個快取發出資料;否則從主存獲得資料
PrWr
  • 給匯流排發BusRdX訊號
  • 狀態轉換為(M)Modified
  • 如果其他快取有有效的副本,其中一個快取發出資料;否則從主存獲得資料
  • 如果其他快取有有效的副本,見到BusRdX訊號後無效其副本
  • 向快取塊中寫入修改後的值
Exclusive(E) PrRd
  • 無匯流排事務生成
  • 狀態保持不變
  • 讀操作為快取命中
PrWr
  • 無匯流排事務生成
  • 狀態轉換為(M)Modified
  • 向快取塊中寫入修改後的值
Shared(S) PrRd
  • 無匯流排事務生成
  • 狀態保持不變
  • 讀操作為快取命中
PrWr
  • 發出匯流排事務BusUpgr訊號
  • 狀態轉換為(M)Modified
  • 其他快取看到BusUpgr匯流排訊號,標記其副本為(I)Invalid.
Modified(M) PrRd
  • 無匯流排事務生成
  • 狀態保持不變
  • 讀操作為快取命中
PrWr
  • 無匯流排事務生成
  • 狀態保持不變
  • 寫操作為快取命中
不同匯流排操作帶來的狀態轉化
初始狀態 操作 響應
Invalid(I) BusRd
  • 狀態保持不變,訊號忽略
BusRdX/BusUpgr
  • 狀態保持不變,訊號忽略
Exclusive(E) BusRd
  • 狀態變為共享
  • 發出匯流排FlushOpt訊號併發出塊的內容
BusRdX
  • 狀態變為無效
  • 發出匯流排FlushOpt訊號併發出塊的內容
Shared(S) BusRd
  • 狀態變為共享
  • 可能發出匯流排FlushOpt訊號併發出塊的內容(設計時決定那個共享的快取發出資料)
BusRdX
  • 狀態變為無效
  • 可能發出匯流排FlushOpt訊號併發出塊的內容(設計時決定那個共享的快取發出資料)
Modified(M) BusRd
  • 狀態變為共享
  • 發出匯流排FlushOpt訊號併發出塊的內容,接收者為最初發出BusRd的快取與主存控制器(回寫主存)
BusRdX
  • 狀態變為無效
  • 發出匯流排FlushOpt訊號併發出塊的內容,接收者為最初發出BusRd的快取與主存控制器(回寫主存)

記憶體屏障的引入

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關鍵字有些相似,後續再詳細展開了。