「計算機原理」| CPU 緩存 & 緩存一致性 & 偽共享

點贊關注,不再迷路,你的支持對我意義重大!

?? Hi,我是丑丑。本文 「計算機組成原理」| 導讀 —— 已收錄,這里有 Android 進階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長。(聯(lián)系方式在 GitHub)


前言

  • CPU 緩存是計算機組成原理中比較基礎,同時也是比較常用的知識,面試中也可能會有一定延伸;
  • 在這篇文章里,我將總結(jié)CPU 緩存 & 緩存一致性 & 偽共享 等問題。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。

目錄


1. CPU 三級緩存

  • 背景: CPU 處理器的運算速度與內(nèi)存存取速度、磁盤 I/O 速度不匹配(相差了幾個數(shù)量級);
  • 目的: 提高 CPU 吞吐量;
  • 方案: 增加一個緩存層來協(xié)調(diào)兩者的速度差,即:在 CPU 和內(nèi)存中間增加一層 「高速緩存」,緩存的存取速度盡可能接近。

數(shù)據(jù)加載的流程如下:

  • 1、將程序和數(shù)據(jù)從磁盤加載到內(nèi)存中;
  • 2、將程序和數(shù)據(jù)從內(nèi)存加載到緩存中(三級緩存,數(shù)據(jù)加載順序:L3->L2->L1);
  • 3、CPU將緩存中的數(shù)據(jù)加載到寄存器中(L0),并進行運算;
  • 4、CPU將數(shù)據(jù)同步回緩存,并在一定的時間周期之后同步回內(nèi)存。

經(jīng)驗表明,CPU 往往需要重復讀取同一個數(shù)據(jù)塊,而緩存容量的增大,可以大幅度提升 CPU 內(nèi)部讀取數(shù)據(jù)的命中率,以此提升 CPU 吞吐量。但是出于 CPU 芯片體積和價格因素來考慮,緩存都不會很大?,F(xiàn)代 CPU 芯片使用的是三級的高速緩存:

—— 圖片引用自網(wǎng)絡

最開始是寄存器(也稱為 L0 緩存),接下來是 L1,L2,L3 三級緩存,最后是內(nèi)存,本地磁盤,遠程存儲。從上到下空間越大,速度越慢,但是成本越低。需要注意的是:在現(xiàn)代 CPU 里,L0、L1、L2、L3 都集成在一顆 CPU 內(nèi)部,其中 L0、L1、L2 是每個處理核心獨立的,而 L3 是一顆 CPU 的多個處理器共用的。

—— 圖片引用自網(wǎng)絡


2. 緩存一致性問題

現(xiàn)代 CPU 通常有多個核心,每個核心也都有自己獨立的緩存(L1、L2 緩存),當多個核心同時操作同一個數(shù)據(jù)時,如果核心 2 在核心 1 還未將更新的數(shù)據(jù)同步回內(nèi)存之前讀取了數(shù)據(jù),就出現(xiàn)了緩存不一致問題。

舉個例子,假設線程 A 和線程 B 同時對一個變量執(zhí)行 i++,就可能存在緩存不一致問題:

  • 1、核心 A 和核心 B 從內(nèi)存中加載了 i 的值,并且緩存到各自的高速緩存中,此時兩個副本都為0;
  • 2、核心 A 進行加一操作,副本值變成了 1,最后回寫到主存中,主存中的值為 1;
  • 3、核心 B 進行加一操作,副本值變成了 1,最后回寫到主存中,主存中的值為 1;
  • 4、最終主存的值為 1,而不是期望的 2。

為了解決緩存不一致性問題,通常來說有兩種解決方法:

  • 1、鎖總線

早期的 CPU 是通過在鎖總線來解決緩存不一致的問題。鎖總線是對整個內(nèi)存加鎖,在鎖總線期間,其他處理器無法訪問內(nèi)存,可想而知會嚴重降低 CPU 性能。

  • 2、緩存一致性協(xié)議(MESI)

緩存一致性協(xié)議提供了一種高效的內(nèi)存數(shù)據(jù)管理方案。「鎖內(nèi)存方案」相當于保證了整塊內(nèi)存的一致性,而「緩存一致性協(xié)議方案」本質(zhì)上相當與一致性保護范圍,從整塊內(nèi)存縮小為單個緩存行(緩存行是緩存的基本單元)。

簡單來說:當 CPU 核心準備寫數(shù)據(jù)時,如果發(fā)現(xiàn)操作的變量是共享變量(即在其他核心中也存在該變量的副本),就會通知其他核心該變量「緩存行」無效,需要重新從內(nèi)存讀取。

具體來說,MESI 協(xié)議會將緩存數(shù)據(jù)定義為四種狀態(tài):

狀態(tài) 描述
E(Exclusive) 獨享 / 互斥
S(Shared) 共享
M(Modify) 修改
I(Invalid) 無效

詳細工作原理:

  • 1、核心 A 從內(nèi)存中加載變量 i,并將緩存行設置為 E(獨享),隨后通過總線嗅探檢查內(nèi)存中對變量 i 的操作;
  • 2、核心 B 從內(nèi)存中加載變量 i,總線嗅探機制會將核心 A 與核心 B 的緩存行設置為 S(共享);
  • 3、核心 A 對變量 i 進行修改,緩存行設置為 M(修改),而核心 B 被通知修改緩存行為 I(無效)。如果存在高并發(fā),則交給總線裁決;
  • 4、核心 A 將修改后數(shù)據(jù)同步回內(nèi)存,并將變量設置為 E(獨享);
  • 5、核心 B 重新刷新緩存行,并將緩存行核心 A 和核心 B 的緩存行設置為 S(共享)。

易混淆: MESI 協(xié)議是 CPU 的協(xié)議,JMM 也有自己的協(xié)議來保證緩存一致性。


3. 偽共享(False Sharing)

在 CPU 緩存中,緩存管理的基本單位并不是「字節(jié)」,而是「緩存行(Cache Line)」。緩存行的大小取決于 CPU,一般是 64 字節(jié)。

緩存行的設計源于:“CPU 讀取一個數(shù)據(jù)之后,往往還需要重復讀取附近的數(shù)據(jù)”。使用緩存行一次將一小塊數(shù)據(jù)加載進高速緩存,有助于提高運算效率。

—— 圖片引用自網(wǎng)絡

當然,緩存也不是完美的,也存在副作用 —— 偽共享。偽共享是指多個線程同時讀寫同一個緩存行中的變量,而導致緩存行失效的問題。盡管兩個線程分別訪問的是不同的數(shù)據(jù),但由于它們存在同一個緩存行中,只要任何一方修改都會使得緩存失效,降低了運算效率。

解決偽共享的方法是 「字節(jié)填充」,即通過在兩個變量中間的內(nèi)容填充額外的字節(jié),使得兩個變量存放在到不同緩存行中,從而規(guī)避偽共享問題。

在使用字節(jié)填充時,需要先考慮哪些變量是獨立變化的,哪些變量是協(xié)同變化的。協(xié)同變化的變量放在一組,而無關的變量分到不同組。這樣當修改變量時,不會導致無關變量的緩存行無效。

在 Java 中,Java 8 前后的處理方式不同:

  • Java 8 之前:通過填充 long 變量來分組
public class DataPadding{
    long a1,a2,a3,a4,a5,a6,a7; 防止與前一個對象產(chǎn)生偽共享
    int value1;
    long value2;
    long b1,b2,b3,b4,b5,b6,b7; 防止兩組變量偽共享;
    boolean flag;
    long d1,d2,d3,d4,d5,d6,d7; 防止與下一個對象產(chǎn)生偽共享
}
  • Java 8:通過 @sun.misc.Contended 分組

@Contended 注解是 JDK 1.8 新增的注解,用于將變量劃分到不同的緩存行。

例如:
Java 8 Thread.java

 /** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;

/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;

/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;

Java 8 ConcurrentHashMap.java

@sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

提示: 需要 JVM 開啟字節(jié)填充功能 -XX:-RestrictContended


4. 總結(jié)

  • 由于 CPU 處理器的運算速度與內(nèi)存存取速度、磁盤 I/O 速度不匹配,所以 CPU 中會增加一層緩存來協(xié)調(diào)兩者的速度差。CPU 緩存是一個三級結(jié)構(gòu),其中 L0、L1、L2 是每個處理核心獨立的,而 L3 是一顆 CPU 的多個處理器共用的;

  • 由于 CPU 每個核心也都有自己獨立的緩存(L1、L2 緩存),當多個核心同時操作同一個數(shù)據(jù)時,如果核心 2 在核心 1 還未將更新的數(shù)據(jù)同步回內(nèi)存之前讀取了數(shù)據(jù),就出現(xiàn)了緩存不一致問題。解決方案有「鎖總線」&「緩存一致性協(xié)議」;

  • 由于 “CPU 讀取一個數(shù)據(jù)之后,往往還需要重復讀取附近的數(shù)據(jù)”,所以 CPU 設計了緩存行(Cache Line)作為基本單位。當然,緩存頁存在副作用 —— 偽共享,即:當多個線程同時讀寫同一個緩存行中的變量,而導致緩存行失效的問題。解決方案是「字節(jié)填充」,使得兩個變量存放在到不同緩存行中。Java 8 之前采用填充 long 變量,而 Java 8 之后采用 @sun.misc.Contended 注解。


創(chuàng)作不易,你的「三連」是丑丑最大的動力,我們下次見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容