JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

一致性協定:MESI 與窺探

第二篇說明了為什麼幾個核心各自編輯同一頁的影印本會彼此不一致。這一篇要把讓它們保持誠實的「規則手冊」搭起來:窺探如何把共享匯流排變成一條八卦頻道,而四個小小的狀態字母——MESI——又如何讓每一條被快取的列都確切知道,還有誰可能握著一份副本。

從問題到一本規則手冊

第二篇把快取一致性(cache coherence)問題以最尖銳的形式留給我們:每個核心都有自己私有的快取,所以同一個記憶體位置可能同時躺在好幾個快取裡,每一份都是同一張共享頁面的獨立影印本。只要一個核心一寫它那份副本,其他每一份副本就悄悄地過時了——而另一個核心稍後的一次讀取,會把舊值交回去,彷彿什麼都沒發生。一份一致性協定就是嚴禁這種事的規則手冊。它的承諾,非正式地說,很簡單:不論是哪個核心寫的,一次讀取永遠會回傳對那個位置「最近一次寫入」的值。

要在不讓每次記憶體存取都爬行的前提下兌現這個承諾,有兩個設計選擇至關重要。第一,當一個核心寫一條共享的列時,你怎麼辦?你可以把新資料推送到其他每一份副本(寫入更新,write-update),也可以叫其他每一份副本把自己丟掉(寫入失效,write-invalidate),日後若還在乎再重新抓回來。幾乎所有真實機器都選寫入失效,因為對同一條列連續寫多次時,只需付一次失效,而不是一連串更新。第二,一個核心怎麼「得知」有人寫了?這就輪到窺探上場了。

窺探:把共享匯流排當成八卦頻道

在一台小型共享記憶體機器裡,所有快取都掛在同一條通往記憶體的共享匯流排上——一組大家都聽得見的線。窺探(snooping)正是利用這一點:每個快取控制器都監聽(窺探)匯流排上的「每一筆」交易,而不只是自己的那筆。當核心 0 需要寫一條列時,它不會悄悄地寫——它會先在匯流排上廣播一個意圖,等於喊出「我要修改位址 X 了」。其他每個快取都聽到,檢查自己有沒有握著 X,若有就照規則反應。不需要任何中央裁判;匯流排本身就是讓大家保持知情的八卦頻道。

這簡單得漂亮,也正是窺探在核心數不多時稱霸的原因。但請注意藏在「廣播」這個詞裡的陷阱:每一則一致性訊息都被所有人聽到,所以匯流排是一項所有核心都在爭搶的共享資源。當你加進更多核心,那一組線就成了瓶頸——正是那個單一道門的老問題,只是現在換成一致性流量。窺探能優雅地擴展到大約十幾個、頂多幾十個核心;再多,向所有人廣播就太昂貴了,於是機器改用目錄協定(directory protocol),替每一條列保存一份「究竟哪些快取握有副本」的清單,這樣一次寫入只需點對點通知那少數幾個,而不必朝整個房間喊叫。

MESI:追蹤誰握有副本的四個字母

窺探告訴一個快取「何時」有事發生;它還需要知道「該怎麼反應」。那套邏輯住在一個貼在每一條快取列上的小型狀態機裡——而最經典的就是MESI,得名自一條列可能處於的四種狀態。可以把它們想成一個核心貼在自己影印本上的四張誠實標籤,記錄著它能多信任這份副本、又欠了其他人什麼。

MESI states for one cache line
  M  Modified   I am the ONLY copy, and I have changed it.
                Memory is stale; I must write back before others read.
  E  Exclusive  I am the ONLY copy, and it still matches memory (clean).
  S  Shared     I have a clean copy; OTHER caches may also have it.
  I  Invalid    My copy is junk; a real read/write must miss and re-fetch.

The two questions each state answers:
           am I the only owner?     have I modified it?
  M             yes                       yes
  E             yes                       no
  S             no (maybe shared)         no
  I             ---                       ---  (no valid data)
一框看懂 MESI:每條列的狀態編碼了核心需要的兩個事實——它是否可能與別人共享這條列,以及它的副本是否比記憶體還髒。

為什麼是四個,不是兩個?聰明之處在於 E(Exclusive,獨占)以及 M/S 的區分。獨占意思是「我握有唯一的副本,而且它是乾淨的」——所以如果這個核心現在要寫,它確知沒有別人握著這條列,可以悄悄跳到 Modified(已修改),完全不必廣播任何東西。對於「一個核心讀了再寫自己私有的資料」這個極其常見的模式,光這一項最佳化就威力巨大:讀取把列以 E 帶進來,寫入免費把它升級成 M,匯流排上完全沒有一致性訊息來往。Shared(共享)對上 Modified 則捕捉另一個軸向——別人是否可能有副本(所以寫入得先讓他們失效)以及記憶體是否過時(所以這條列得先寫回,別人才能讀到)。

走一遍寫入穿越各狀態的旅程

讓我們追蹤一個具體場景,就是第二篇用過的那種:兩個核心共享一個住在同一條快取列上的計數器。看看標籤如何移動、窺探又如何傳遞消息。要去感受的關鍵轉換有兩個:寫入時的失效廣播,以及當有人試圖讀取另一核心已修改的列時的按需寫回(write-back-on-demand)

  1. 核心 0 讀取計數器;沒有別人握著它。這條列從記憶體載入,核心 0 把它標為 E(獨占)——唯一擁有者、乾淨的。
  2. 核心 0 寫入(counter++)。因為它原本是獨占,它知道自己是唯一的——它無聲地翻成 M(已修改),匯流排上連一則訊息都沒有。記憶體現在過時了。
  3. 核心 1 現在讀取同一個計數器。它的快取未命中,於是在匯流排上廣播一個讀取請求。核心 0 窺探到這件事,發現自己以 M 握著這條列,便介入:它提供最新的值並把它寫回記憶體。兩個核心現在都以 S(共享)握著這條列。
  4. 核心 1 想要寫入計數器。它只以共享握著這條列,所以必須先廣播一個失效。核心 0 窺探到,把自己的副本降為 I(失效)。直到這時,核心 1 才把它的列升級為 M 並執行寫入。
  5. 如果核心 0 再次讀取計數器,它的副本是失效的,於是未命中、廣播,而核心 1(現在處於 M)提供最新的值——循環就此重來。一條列在兩個核心之間這樣反覆彈跳,正是一致性逼你為「共享」付出的代價。

把最後那句再讀一遍,因為那就是重點所在。一致性不是免費的——它是「正確」的,但每當兩個核心輪流寫一條共享的列,那條列就在它們的快取之間乒乓來回,每一輪都付出一次幾十個週期的跨核心傳輸。協定永遠不會讓你讀到舊值;它只是讓「真正的共享」變得昂貴。這正是恰當的取捨,也鋪好了接下來的兩個想法:偽共享(不小心付出這個代價)與同步(刻意、正確地付出它)。

偽共享:糊里糊塗付了帳

現在是陷阱。一致性協定運作的單位是整條快取列——通常 64 位元組——從來不是個別變數。它看不到、也無法看到兩個核心碰的是「不同」的位元組;它只看到它們碰的是「同一條列」。所以想像一個陣列 `count[NUM_CORES]`,每個核心只更新自己的那一格。邏輯上「零」共享——沒有任兩個核心碰過同一個計數器。但如果那些格子很小又緊挨著連續排列,其中好幾格就落在同一條 64 位元組的列上,於是硬體把每次更新都當成對一條共享列的寫入。這就是偽共享(false sharing):核心為一條它們其實並未共享的列而爭鬥。

效果既殘酷又隱形。每個核心無害的 `count[me]++` 都廣播一個失效,把其他核心那份列的副本打掉;這條列就在每個核心的快取之間彈跳,跟我們追蹤的那一幕一模一樣,只是現在「完全沒有任何邏輯上的理由」。一個本該完美隨核心數擴展的迴圈,反而可能在你「加」核心時變「慢」,因為它們把時間都花在挪動一條被爭搶的列上。修法純粹是佈局:把每個核心的資料填補或對齊到它自己的快取列上(這正是上一階最後一篇講的對快取友善習慣),讓私有的資料在物理上也私有。同樣的答案,沒有爭用。

一致性承諾了什麼、又沒承諾什麼

進到下一篇之前,先劃一條誠實的界線。一致性保證的是:對「單一」記憶體位置,所有核心最終都同意一個單一、最新的值——絕不是過時的影印本。它本身「不」保證的,是跨核心所見、對「不同」位置之寫入的「順序」。那個更大的問題——一個核心的儲存以什麼順序對另一個核心變得可見、以及你是否需要一道記憶體屏障來強制某個順序——叫做記憶體一致性模型(memory consistency model),是疊在 coherence 之上的另一層。Coherence 是「逐位置的真相」;consistency 是「跨位置的排序」。把兩者搞混是經典的初學者陷阱,所以要把它們分開。

並且把代價放在眼前,因為它從不消失。MESI 讓「私有、未共享」的資料基本上免費(就是那個獨占的把戲),但「真正的共享」——一把鎖、一個共享計數器、一個生產者–消費者佇列——總得為每一次跨核心交接付費。這就是為什麼把更多核心丟向一個問題,很少換來等比例的加速:一致性流量與爭用會隨共享而增長,預示了兩篇之後的 Amdahl 定律。下一篇要在這一切之上搭起你「真正會用到」的工具——原子操作、比較並交換(compare-and-swap),以及用它們造出的鎖——而每一個,骨子裡,都是一支穿過這些 MESI 狀態、精心編排的舞。