原打算從新學習一下 volatile
的實現原理,其中涉及到指令調度重排和數據可見性保證,這二者的理解離不開對 CPU Cache的掌握,所以,先重溫一下CPU Cache,便有了本文。算法
CPU的發展呈現出摩爾定律(近期愈來愈多的聲音以爲結束了),發展速度迅猛,每18-24個月性能翻番。而內存的發展相較之下顯得十分緩慢,與CPU的性能差距愈來愈大。爲了緩衝二者的速度差,引入了採用SRAM作Cache的三級緩存(L一、L二、L3),以提升CPU的計算效率。固然了,其實內存並不是沒法提速,只是出於成本和容量的平衡。緩存
1965 年,英特爾聯合創始人戈登·摩爾提出以本身名字命名的「摩爾定律」,意指集成電路上可容納的元器件的數量每隔 18 至 24 個月就會增長一倍,性能也將提高一倍。多線程
這就好像我們去超市購物,所購買的東西常常就那麼幾樣,真正購物的時間很短,可是交通耗時、買單排隊耗時一般就已經佔據了大部分的時間。出於成本考慮,一個小區配備一個超市顯然不太可能。因而引入了住宅樓下的自動售賣機、社區的便利店等。如此一來,購物效率天然提升了。ide
三級緩存集成在CPU中,組成以下,每一個CPU核都擁有本身的L1 Cache、L2 Cache,而L3 Cache爲全部核心共享。其中,L1距離Execution Units計算單元距離最近,計算速度一般十分接近;L2 、L3分別次之。另外,L1 Cache 通常分爲 L1d 數據緩存 和 L1i 指令集緩存,用以減小CPU多核心/多線程競搶緩存引發的衝突。性能
讀取數據時,逐級訪問,即執行單元訪問L1,若不存在該數據,則L1訪問L2,L2若一樣沒有則訪問L3,最後L3訪問內存。學習
三級緩存的大小一般不大,以本機i5-8259u
爲例:.net
i5-8259u:線程
L1 Data Cache :32.0 KB x 4code
L1 Instruction Cache :32.0 KB x 4cdn
L2 Cache :246 KB x 4
L3 Cache :6.00 MB
L4 Cache :0.00 B
Memory :16.0 GB 2133MHz LPDDR3
一個Cache分爲N個Cache Line, 通常大小爲32byte或64byte,是和內存進行數據交換的最小單位。一個Cache Line 至少有valid、tag、block三個部分,其中block用以存儲數據,tag用於指示內存地址,valid則用於表示該數據的有效性。
CPU內核訪問數據時,發現該數據處於某個Cache Line中,且valid狀態爲有效,則成爲cache hit,不然,成爲cache miss。一般,緩存命中和未命中對於內核的效率影響相差幾百個時鐘。所以,爲了緩存命中率,採用合理有效的緩存數據設置和替換策略對於CPU的計算效率相當重要。就比如社區便利店根據居民的購物習慣,提供高頻消費的商品,而且,根據客戶喜愛的演變進行調整。
CPU的緩存數據設置主要根據空間局部性、時間局部性。
空間局部性:若一個存儲位置的數據被訪問,那麼它附近位置的數據很大可能也會被訪問。
時間局部性:若一個存儲位置的數據被訪問,那麼它在未來的時間很大可能被重複訪問。
而緩存置換策略通常有三種,FIFO 先進先出,LRU 最近最少使用,和 LFU 最不常使用:
FIFO:First In First Out,根據進入緩存時間,淘汰最先的。
LRU:Least Recently Used,對緩存數據進行使用量統計,淘汰最少使用的。該算法使用最多。
LFU:Least Frequently Used,一段時間內,根據使用量,淘汰最少使用的。
(LUR和LFU算法練習:Leetcode-LRU、Leetcode-LFU)
引入多級緩存後,結合行之有效的數據設置和替換策略,大大提升了CPU的計算效率,可是同時也帶來了緩存一致性問題。
爲了保證 Cache 一致性,CPU 底層提供了兩種操做:Write invalidate 和 Write update。
Write invalidate 操做指:當一個內核修改了一份數據,其它內核若是有這份數據,就把 valid 標識爲無效。
Write update 操做指:當一個內核修改了一份數據,其它內核若是有這份數據,就都更新爲新值。
Write invalidate 操做實現起來更爲簡單,加上其它內核後續並不須要使用到改數據。缺點在於一個valid標識對應個Cache Line,如此一來,其它本來有效的數據也被設置爲無效。Write update 操做會產生大量的更新操做,不過只須要更新修改的數據,而非一個Cache Line。大多數處理器採用的操做都是 Write invalide。
這兩個操做也是咱們碼農經常使用的緩存操做,修改緩存數據後,將緩存置爲無效,或者直接更新。固然,除此以外,對於實時性要求不高的緩存數據,咱們還常常採用按期時間,自動過時的策略。
Write invalidate 提供了緩存一致性的簡單解決思路,具體的實施還須要一套完整的協議,其中比較經典,常做爲教材的就是MESI協議,後續許多協議都是基於MESI進行擴展。
與前文講到的 Cache Line 普通結構不一樣的是,MESI 協議中,Cache Line 用頭兩個 bit 來表示 MESI 的四個狀態:
這四個狀態分別爲:
M(Modified):數據被修改了,屬於有效狀態,可是數據只處於本Cache,和內存不一致。
E(Exclusive):數據獨佔,屬於有效狀態,數據僅在本Cache,和內存一致。
S(Shared):數據非獨佔,屬於有效狀態,數據存於多個Cache,和內存一致。
I(Invalid):數據無效。
下面用畫圖示意四個狀態,便於理解,圖片取自《大話處理器》:
須要注意的是:當狀態爲E/S時,數據才與緩存一致。而修改某內核Cache的數據後,並不會當即寫回內存,而是將該Cache Line標示爲M,其它內核的該份數據表示爲I,此時的數據是不一致的。
下面爲四個狀態的轉化示意圖:
從狀態轉化圖中,能夠注意到的是:不管當前Cache Line處於什麼狀態,對於兩個修改操做——Local Write 將本Cache Line 狀態變動爲 Modified,並將其它Cache Line統一設置爲 Invalid(若其它核處於S),等待觸發寫回內存;而 Remote Write 則將全部存有該份數據的 Cache Line 狀態統一變動爲 Invalid 失效,至關於從新構建該數據的緩存。
下面分別對四種狀態的轉化進行具體說明:
當狀態爲 Invalid 時:
當前狀態 | 事件 | 下個狀態 | 說明 |
---|---|---|---|
Invalid | Local Read | Exclusive | 在其它Cache中找不到該數據 |
Invalid | Local Write | Shared | (1)若存有該數據的Cache Line處於M,則將該數據更新至內存;(2)若存有該數據的Cache Line處於E,則讀取數據,並將該兩個Cache Line都設置爲S;(3)若存有該數據的Cache Line處於S,則讀取該數據,將本Cache Line設置爲S |
Invalid | Remote Read | Invalid | 其它核不讀該數據 |
Invalid | Remote Write | Invalid | 其它核不寫該數據 |
當狀態爲 Exclusive 時:
當前狀態 | 事件 | 下個狀態 | 說明 |
---|---|---|---|
Exclusive | Local Read | Exclusive | 讀本身獨佔的數據,狀態天然不變 |
Exclusive | Local Write | Modified | 設置爲M,由於與內存數據不一致,等觸發回寫 |
Exclusive | Remote Read | Shared | 其它核不讀該數據 |
Exclusive | Remote Write | Invalid | 其它核不寫該數據 |
當狀態爲 Shared 時:
當前狀態 | 事件 | 下個狀態 | 說明 |
---|---|---|---|
Shared | Local Read | Shared | 讀共享數據,無變動數據,狀態天然不變 |
Shared | Local Write | Modified | 設置爲M,由於與內存數據不一致,等觸發回寫 |
Shared | Remote Read | Shared | 讀共享數據,無變動數據,狀態天然不變 |
Shared | Remote Write | Invalid | 其它核修改不屬於本身的Cache,採用統一失效策略 |
當狀態爲 Modified 時:
當前狀態 | 事件 | 下個狀態 | 說明 |
---|---|---|---|
Modified | Local Read | Modified | 讀本身獨有的數據,狀態天然不變 |
Modified | Local Write | Modified | 寫本身獨有的數據,狀態一樣不變 |
Modified | Remote Read | Shared | 讀共享數據,無變動數據,狀態天然不變 |
Modified | Remote Write | Invalid | 其它核修改不屬於本身的Cache,採用統一失效策略 |