CPU 的高速緩存,通??梢苑譃?L1、L2、L3 這樣的三層高速緩存,也稱為一級緩存、二級緩存、三級緩存。
L1 高速緩存訪問速度幾乎和寄存器一樣快,大小在幾十 KB 到幾百 KB 不等。每個 CPU 核心都有一塊屬于自己的 L1 高速緩存。
L2 高速緩存同樣每個 CPU 核心都有,但是 L2 高速緩存位置比 L1 高速緩存距離 CPU 核心 更遠,它大小比 L1 高速緩存更大,CPU 型號不同大小也就不同,通常大小在幾百 KB 到幾 MB 不等,訪問速度則更慢。
L3 高速緩存通常是多個 CPU 核心共用的,位置比 L2 高速緩存距離 CPU 核心 更遠,大小也會更大些,通常大小在幾 MB 到幾十 MB 不等。
CPU Cache 是由很多個 Cache Line 組成的,CPU Line 是 CPU 從內存讀取數據的基本單位,而 CPU Line 是由各種標志(Tag)+ 數據塊(Data Block)組成,你可以在下圖清晰的看到:
多核CPU同時工作的時候,每個核心都會從內存中讀取一份數據并緩存到自己的Cache中,當發生寫操作的時候,有兩種情況
寫直達由于每次寫操作都會把數據寫回到內存,而導致影響性能,于是為了要減少數據寫回內存的頻率,就出現了寫回的方法。
寫回策略能夠減少寫回內存的次數,性能會比寫直達更高。當然,寫回策略在讀取的時候,有可能不是純粹的讀取了,因為還可能會觸發一次臟 Cache 塊的寫入。
這里還有一個設計: 在目標內存塊不在 Cache 中時,寫直達策略會直接寫入內存。而寫回策略會先把數據讀取到 Cache 中再修改 Cache 數據,這似乎有點多余?其實還是為了減少寫回內存的次數。雖然在未命中時會增加一次讀取操作,但后續重復的寫入都能命中緩存。否則,只要一直不讀取數據,寫回策略的每次寫入操作還是需要寫入內存。
在單核 CPU 中,我們通過寫直達策略或寫回策略保持了Cache 與內存的一致性。但是在多核 CPU 中,由于每個核心都有一份獨占的 Cache,就會存在一個核心修改數據后,兩個核心 Cache 不一致的問題。
舉個例子:
在寫直達策略中,新數據會直接寫回內存,此時,Cache 和內存塊一致。但由于之前 Core 2 已經讀過這塊數據,所以 Core 2 緩存的數據還是舊的。此時,Core 1 和 Core 2 不一致;
在寫回策略中,新數據會延遲寫回內存,此時 Cache 和內存塊不一致。不管 Core 2 之前有沒有讀過這塊數據,Core 2 的數據都是舊的。此時,Core 1 和 Core 2 不一致。
由于兩個核心的工作是獨立的,在一個核心上的修改行為不會被其它核心感知到,所以不管 CPU 使用寫直達策略還是寫回策略,都會出現緩存不一致問題。 所以,我們需要一種機制,將多個核心的工作聯合起來,共同保證多個核心下的 Cache 一致性,這就是緩存一致性機制。
緩存一致性機制需要解決的問題就是 2 點:
寫傳播和事務串行化在 CPU 中是如何實現的呢?
寫傳播 - 總線嗅探: 總線除了能在一個主模塊和一個從模塊之間傳輸數據,還支持一個主模塊對多個從模塊寫入數據,這種操作就是廣播。要實現寫傳播,其實就是將所有的讀寫操作廣播到所有 CPU 核心,而其它 CPU 核心時刻監聽總線上的廣播,再修改本地的數據;
可以發現,總線嗅探方法很簡單, CPU 需要每時每刻監聽總線上的一切活動,但是不管別的核心的 Cache 是否緩存相同的數據,都需要發出一個廣播事件,這無疑會加重總線的負載。
事務串行化 - 總線仲裁: 總線的獨占性要求同一時刻最多只有一個主模塊占用總線,天然地會將所有核心對內存的讀寫操作串行化。如果多個核心同時發起總線事務,此時總線仲裁單元會對競爭做出仲裁,未獲勝的事務只能等待獲勝的事務處理完成后才能執行。
基于總線嗅探和總線仲裁,現代 CPU 逐漸形成了各種緩存一致性協議,例如 MESI 協議。
MESI 協議其實是 CPU Cache 的有限狀態機,一共有 4 個狀態(MESI 就是狀態的首字母):
在 「獨占」 和 「共享」 狀態下,Cache 塊的數據是 “清” 的,任何讀取操作可以直接使用 Cache 數據;
在 「已失效」 和 「已修改」 狀態下,Cache 塊的數據是 “臟” 的,它們和內存的數據都可能不一致。在讀取或寫入 “已失效” 數據時,需要先將其它核心 “已修改” 的數據寫回內存,再從內存讀取;
「獨占」和「共享」的差別在于,獨占狀態的時候,數據只存儲在一個 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 沒有該數據。這個時候,如果要向獨占的 Cache 寫數據,就可以直接自由地寫入,而不需要通知其他 CPU 核心,因為只有你這有這個數據,就不存在緩存一致性的問題了,于是就可以隨便操作該數據。
另外,在「獨占」狀態下的數據,如果有其他核心從內存讀取了相同的數據到各自的 Cache ,那么這個時候,獨占狀態下的數據就會變成共享狀態。
那么,「共享」狀態代表著相同的數據在多個 CPU 核心的 Cache 里都有,所以當我們要更新 Cache 里面的數據的時候,不能直接修改,而是要先向所有的其他 CPU 核心廣播一個請求,要求先把其他核心的 Cache 中對應的 Cache Line 標記為「無效」狀態,然后再更新當前 Cache 里面的數據。
事實上,完整的 MESI 協議更復雜,但我們沒必要記得這么細。我們只需要記住最關鍵的 2 點:
提示: MESI 協議在 MSI 的基礎上增加了 E(獨占)狀態,以減少只有一份緩存的寫操作造成的總線通信。
MESI 協議保證了 Cache 的一致性,但完全地遵循協議會影響性能。 因此,現代的 CPU 會在增加寫緩沖區和失效隊列將 MESI 協議的請求異步化,以提高并行度:
由于在寫入操作之前,CPU 核心 1 需要先廣播 RFO 請求獲得獨占權,在其它核心回應 ACK 之前,當前核心只能空等待,這對 CPU 資源是一種浪費。因此,現代 CPU 會采用 “寫緩沖區” 機制:寫入指令放到寫緩沖區后并發送 RFO 請求后,CPU 就可以去執行其它任務,等收到 ACK 后再將寫入操作寫到 Cache 上。
由于其他核心在收到 RFO 請求時,需要及時回應 ACK。但如果核心很忙不能及時回復,就會造成發送 RFO 請求的核心在等待 ACK。因此,現代 CPU 會采用 “失效隊列” 機制:先把其它核心發過來的 RFO 請求放到失效隊列,然后直接返回 ACK,等當前核心處理完任務后再去處理失效隊列中的失效請求。
事實上,寫緩沖區和失效隊列破壞了 Cache 的一致性。
因為在未同步的情況下,程序可能會有多種執行順序。這也是為什么Java里還需要volatile關鍵字,因為引入寫緩沖區或失效隊列后就變成弱數據一致性,不能滿足 強數據一致性: 保證在任意時刻任意副本上的同一份數據都是相同的,或者允許不同,但是每次使用前都要刷新確保數據一致,所以最終還是一致。
縱向 - Cache 與內存的一致性問題: 在修改 Cache 數據后,如何同步回內存?
橫向 - 多核心 Cache 的一致性問題: 在一個核心修改 Cache 數據后,如何同步給其他核心 Cache?
寫直達策略: 始終保持 Cache 數據和內存數據一致,在每次寫入操作中都會寫入內存;
寫回策略: 只有在臟 Cache 塊被替換出去的時候寫回內存,減少寫回內存的次數;
寫傳播(總線嗅探): 每個 CPU 核心的寫入操作,需要傳播到其他 CPU 核心;
事務串行化(總線仲裁): 各個 CPU 核心所有寫入操作的順序,在所有 CPU 核心看起來是一致。
本文鏈接:http://www.tebozhan.com/showinfo-26-5716-0.html計算機底層原理~CPU緩存一致性
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
下一篇: 停止過度設計中等規模的前端應用程序