Cache 簡介
Cache,即緩存。緩存能提升讀取性能,其原理是用性能更好的存儲介質存儲一部分高頻訪問的內容,獲得總體概率上的速度提升。
在開發中,我們口中的緩存可以是一個變量,或者是 redis。在計算機 CPU 內部,CPU 往往指的是 CPU 的各級緩存。
CPU Cache 原理
緩存的工作原理是當 CPU 要讀取一個數據時,首先從CPU緩存中查找,找到就立即讀取並送給 CPU 處理;沒有找到,就從速率相對較慢的內存中讀取並送給 CPU 處理,同時把這個數據所在的數據塊調入緩存中,可以使得以后對整塊數據的讀取都從緩存中進行,不必再調用內存。正是這樣的讀取機制使 CPU 讀取緩存的命中率非常高(大多數 CPU 可達 90% 左右),也就是說 CPU 下一次要讀取的數據 90% 都在 CPU 緩存中,只有大約 10% 需要從內存讀取。這大大節省了 CPU 直接讀取內存的時間,也使 CPU 讀取數據時基本無需等待。總的來說,CPU 讀取數據的順序是先緩存后內存。(摘自百科)
將模型簡化以后,如果 CPU 想訪問內存里的內容:
CPU Core1 --> L1 Cache --> L2 Cache --> L3 Cache --> RAM
CPU Core2 --> L1 Cache --> L2 Cache --> L3 Cache --> RAM
需要注意的是,簡單情況下,每個 CPU 核心都有自己的獨立的多級緩存,常見的有三級。訪問速度上,L1 > L2 > L3, 容量通常與速度成反比。通俗點說,你在某處聲明的變量 int foo = 1;在有緩存情況下,CPU 是從 L1~L3 中獲取 foo 的值,多級緩存無命中才去內存中取。
現如今 Intel 比較新的 CPU 型號,其緩存不再是彼此獨立的設計了,雙核會共享二級緩存,即“Smart cache” 共享緩存技術。
Cache Line
將 Cache 按照一定長度切割開,就有了許多的 Cache Line。Cache Line 是 Cache 的最小單位,通常是 64 bytes。如果 L1 緩存是 6400 bytes, 那他可以分成 100 個 Cache Line。在 C 語言中,你能感知到的內存最小單位應該是變量, int,long long 等,他們通常只有 4 字節或者 8 字節。CPU 的緩存為了性能,一般是以 Cache Line 為單位進行一口氣緩存一大塊內存。一個 Cache Line 中就會緩存很多個變量的值。如果 Cache Line 有了臟數據,也是以它為單位整塊更新。
Cache 的一致性
計算機必須保證在緩存中的數據永遠都是新的。如果內存值已經發生改變,CPU Cache 未及時同步,就出現了數據不一致。多核 CPU 架構下,一致性的保證就比較復雜,比如多個 CPU Cache 都緩存了某個變量的值,但那個變量被其中一個核修改了值,其他 CPU 核心內的緩存如何能及時感知並刷新緩存?
MESI 協議能解決多核 CPU Cache 一致性問題。
MESI(Modified Exclusive Shared Or Invalid)
摘自 https://www.cnblogs.com/shangxiaofei/p/5688296.html
MESI 也稱為伊利諾斯協議,是因為該協議由伊利諾斯州立大學提出 是一種廣泛使用的支持寫回策略的緩存一致性協議,該協議被應用在Intel奔騰系列的CPU中。
MESI協議中的狀態
CPU中每個緩存行(caceh line)使用4種狀態進行標記(使用額外的兩位(bit)表示):
M: 被修改(Modified)
該緩存行只被緩存在該CPU的緩存中,並且是被修改過的(dirty),即與主存中的數據不一致,該緩存行中的內存需要在未來的某個時間點(允許其它CPU讀取請主存中相應內存之前)寫回(write back)主存。
當被寫回主存之后,該緩存行的狀態會變成獨享(exclusive)狀態。
E: 獨享的(Exclusive)
該緩存行只被緩存在該CPU的緩存中,它是未被修改過的(clean),與主存中數據一致。該狀態可以在任何時刻當有其它CPU讀取該內存時變成共享狀態(shared)。
同樣地,當CPU修改該緩存行中內容時,該狀態可以變成Modified狀態。
S: 共享的(Shared)
該狀態意味着該緩存行可能被多個CPU緩存,並且各個緩存中的數據與主存數據一致(clean),當有一個CPU修改該緩存行中,
其它CPU中該緩存行可以被作廢(變成無效狀態(Invalid))。
I: 無效的(Invalid)
該緩存是無效的(可能有其它CPU修改了該緩存行)。
簡單總結:
CPU 各個核之間存在一種通訊的方式,用於通知其他核心某個 Cache Line 已經失效了。CPU 對 Cache 的讀寫操作之前,會判斷該 Cache Line 處於哪個狀態。
當 Cache 處於 Shared 狀態時,CPU 某核心執行寫操作,會廣播通知到其他 CPU 核心。
通過這種方式,保證了 Cache 的一致性。
Cache Miss
我們知道 CPU 需要 Cache 來提高數據讀取速度。如果 CPU 想訪問一塊內存,Cache 里沒有,我們管他叫 Cache Miss。Cache Miss 會讓你頭大,CPU 不得不花費大量的時間用在把內存數據加載進 Cache。一般來說, L1 的 miss 率在 10% 左右。倒過來想想,居然有 90% 左右的命中率,不得不佩服程序員大神的能力。
False Sharing 偽共享
偽共享即 MESI 中不健康的 Shared/Invalid 狀態。考慮這樣一個場景。
struct {
int thread1_data; // 線程1只讀寫它
int thread2_data; // 線程2只讀寫它
};
同時有兩個線程(thread1 和 thread2)只去讀寫屬於他自己的那個變量。看似各玩各的互不影響,實際上由於兩個變量挨得很近,往往會被放到一個 Cache Line 中。 thread1 對 thread1_data 的讀寫,會造成 core2 核上對 thread2_data 的緩存被標記為無效 Invalid,從而要刷新 Cache。我們知道將內存裝載進 Cache 是很費時的,如果過高頻繁地觸發,會造成性能下降。
在多線程讀寫數組上,尤其要注意這個偽共享問題。
偽共享的本質是,高等語言的概念上,看似變量間是獨立的,但是在 CPU Cache 層面, 兩個變量地址挨得太近(在一個 Cache Line 范圍中)就只能作為一個 Cache Line 整體來看。
CPU 指令亂序與 Barrier 屏障
由於存在 Cache Miss 等耗時工作,但是 CPU 可以在加載數據的同時,干一些別的事情,那么指令必然會被打亂。
總之, CPU 指令不按順序執行是為了更快的性能,更高的執行效率。
指令亂序就發生在我們身邊,舉個通俗的例子,
在 CPU 指令亂序的情況下,a 和 b 誰先被賦值是不知道的。如果多線程依賴了其先后順序,不加鎖的情況下會造成很嚴重的問題。
barrier 指令可以解決 CPU 指令亂序的問題。它告訴 CPU 某些地方不要亂序。這是個底層的指令,對於高級語言使用者來說,應該使用加鎖、原子操作來解決。