存儲器 - 高速緩存(CPU Cache):為什么要使用高速緩存


存儲器 - 高速緩存(CPU Cache):為什么要使用高速緩存?

計算機組成原理目錄:https://www.cnblogs.com/binarylei/p/12585607.html

1. 為什么需要高速緩存

CPU 和內存訪問性能的差距非常大。如今,一次內存的訪問,大約需要 120 個 CPU Cycle。這也意味着,在今天,CPU 和內存的訪問速度已經有了 120 倍的差距。

  • CPU:按照摩爾定律,CPU 的訪問速度每 18 個月便會翻一番,相當於每年增長 60%。比如我的筆記本是 Intel Core-i5-8250U 1.6GHz,也就是每秒可以訪問 16 億(= 1.6G)次。
  • 內存:每年只增長 7% 左右。內存響應時間大概是 100us,也就是極限情況下,大概每秒可以訪問 1000 萬(= 1s / 100ns)次。
  • HDD:磁盤尋道時間約 10ms,大概每秒可以訪問 100 次。
圖1:隨着時間變遷,CPU 和內存之間的性能差距越來越大

為了彌補兩者之間的性能差異,充分利用 CPU,現代 CPU 中引入了高速緩存(CPU Cache)。高速緩存分為 L1/L2/L3 Cache,不是一個單純的、概念上的緩存(比如使用內存作為硬盤的緩存),而是指特定的由 SRAM 組成的物理芯片。下圖是一張 Intel CPU 的放大照片。這里面大片的長方形芯片,就是這個 CPU 使用的 20MB 的 L3 Cache,可以看到現代 CPU 中大量的空間已經被 SRAM 占據。

圖2:現代 CPU 中大量的空間已經被 SRAM 占據

程序運行的時間主要花在將對應的數據從內存中讀取出來,加載到 CPU Cache 里。CPU 從內存中讀取數據到 CPU Cache 的過程中,是一小塊一小塊來讀取數據的。這樣一小塊一小塊的數據,在 CPU Cache 里面,我們把它叫作緩存行(Cache Line)。在我們日常使用的 Intel 服務器或者 PC 里,Cache Line 的大小通常是 64 字節。

現在總結一下,為了平衡 CPU 和內存的性能差異,現在 CPU 引入高速緩存:

  • 高速緩存(CPU Cache):用於平衡 CPU 和內存的性能差異,分為 L1/L2/L3 Cache。其中 L1/L2 是 CPU 私有,L3 是所有 CPU 共享。
  • 緩存行(Cache Line):高速緩存的最小單元,一次從內存中讀取的數據大小。常用的 Intel 服務器 Cache Line 的大小通常是 64 字節。

知道了為什么需要 CPU Cache,接下來我們就來看一看,CPU 究竟是如何訪問 CPU Cache 的,以及 CPU Cache 是如何組織數據,使得 CPU 可以找到自己想要訪問的數據的。

2. 高速緩存讀操作

CPU Cache 和 Redis 緩存訪問類似,都是先訪問緩存,如果數據不存在再訪問內存。在各類基准測試(Benchmark) 和實際應用場景中,CPU Cache 的命中率通常能達到 95% 以上。

圖3:CPU Cache 和 Redis 緩存架構類似

CPU 如何知道要訪問的內存數據,存儲在 Cache 的哪個位置呢?CPU 訪問 Cache 的訪問邏輯有以下幾種。

  1. 直接映射 Cache(Direct Mapped Cache)
  2. 全相連 Cache(Fully Associative Cache)
  3. 組相連 Cache(Set Associative Cache)

2.1 直接映射(Direct Mapped Cache)

CPU 如何知道要訪問的內存數據,存儲在 Cache 的哪個位置呢?接下來,我就從最基本的直接映射 Cache(Direct Mapped Cache) 說起,帶你來看整個 Cache 的數據結構和訪問邏輯。

CPU 訪問內存數據時按緩存行大小讀寫,通常是 64 byte。我們將內存將緩存行大小切分,每個內存地址必定會落到某個內存塊上,數據在這個內存塊的偏移量也是確定的。問題是如何將這個 "內存塊 block" 和 "緩存行 Cache Line" 索引號映射關系確定下來。

  • 索引號:CPU 直接映射是通過求余運算來實現,並且要求緩存行的個數必須是 2n。這樣可以直接使用低位來表示索引號。
  • 偏移量:可以根據塊大小確定偏移量。

現在,我們知道了 Cache Line 的索引號和在這個 Cache Line 的偏移量,CPU 就可以在緩存行中讀出這個數據了。通過索引號和偏移量建立內存映射關系,這在軟件工程中很常見,比如內存的虛擬地址和物理地址之間的映射關系,再比如 innodb 中數據地址定位(頁號 + 偏移量)。

比如說,我們的主內存被分成 0~31 號這樣 32 個塊。我們一共有 8 個緩存塊。用戶想要訪問第 21 號內存塊。如果 21 號內存塊內容在緩存塊中的話,它一定在 5 號緩存塊(21 % 8 = 5)中。

圖4:Cache 通過求余把內存塊映射到對應的Cache Line

了解 HashMap 的都知道,通過求余算法一定會出現哈希碰撞。同樣的道理,此時也會出現多個內存塊映射同一個緩存行的情況,CPU 如何判斷這是不是我們想要訪問的數據呢?

  • 組標記(Tag):最簡單的辦法,當然是在緩存行中存儲完整真實的物理內存地址,但有點浪費空間。前面已經說了緩存行的個數是 2n,可以直接使用低位表示索引號,也就是每個緩存行對應的低位地址是固定的,緩存行中只需要保存高位地址即可。

    如 21 的低 3 位 101,緩存塊本身的地址已經涵蓋了對應的信息、對應的組標記,我們只需要記錄 21 剩余的高 2 位的信息,也就是 10 就可以了。

  • 有效位(valid bit):標記對應的緩存塊中的數據是否是有效的,確保不是機器剛剛啟動時候的空數據。如果有效位是 0,無論其中的組標記和 Cache Line 里的數據內容是什么,CPU 都不會管這些數據,而要直接訪問內存,重新加載數據。

現在我們總結一下:一個內存的訪問地址,最終包括高位代表的組標記低位代表的索引,以及在對應的 Data Block 中定位對應字的位置偏移量

圖5:內存地址到 Cache Line 的關系

而內存地址對應到 Cache 里的數據結構,則多了一個有效位和對應的數據,由 "索引 + 有效位 + 組標記 + 數據" 組成。如果內存中的數據已經在 CPU Cache 里了,那一個內存地址的訪問,就會經歷這樣 5 個步驟:

  1. 根據內存地址的低位,計算在 Cache 中的索引;
  2. 判斷有效位,確認 Cache 中的數據是有效的;
  3. 對比內存訪問地址的高位,和 Cache 中的組標記,確認 Cache 中的數據就是我們要訪問的內存數據,從 Cache Line 中讀取到對應的數據塊(Data Block);
  4. 根據內存地址的 Offset 位,從 Data Block 中,讀取希望讀取到的字。
  5. 如果在 2、3 這兩個步驟中,CPU 發現,Cache Line 並不是要訪問的內存地址的數據,那 CPU 就會訪問內存,並把對應的 Block Data 更新到 Cache Line 中,同時更新該 Cache Line 對應的有效位和組標記的數據。

總結: 要想確定一個內存地址在緩存中的映射關系,主要是確定索引號和偏移量。 CPU Cache 直接映射類似 HashMap,也是通過求余來建立映射關系,當然也無法避免哈希碰撞。CPU 判斷是不是我們想要訪問的數據時,最簡單的辦法當然是在緩存中存儲完整真實的物理內存地址,但這樣太浪費空間了,CPU 巧妙地將內存地址拆分成高位和低位,用高位代表組標記,低位代表索引號。最終通過 "索引 + 組標記 + 偏移量" 建立映射關系,使得我們可以將很大的內存地址,映射到很小的 CPU Cache 地址里。

3. 高速緩存寫操作

在搞清楚從內存加載數據到 Cache,以及從 Cache 里讀取到想要的數據之后,我們又要面臨一個新的挑戰。CPU 不僅要讀數據,還需要寫數據,我們不能只把數據寫入到 Cache 里面就結束了。CPU 要寫入數據的時候,怎么既不犧牲性能,又能保證數據的一致性。

3.1 寫操作挑戰

圖6:高速緩存結構

CPU 數據寫入時會有以下兩個挑戰:

  • 什么時候寫入主存?緩存什么時候失效?寫直達 vs 寫回策略。寫入 Cache 的性能也比寫入主內存要快,那我們寫入的數據,到底應該寫到 Cache 里還是主內存呢?如果我們直接寫入到主內存里,Cache 里的數據是否會失效呢?CPU 提供了寫直達 vs 寫回兩種策略。
  • 多核 CPU 緩存一致性的問題。無論是寫直達還是寫回策略都不能解決多核 CPU 緩存一致性的問題。現在計算機采用緩存一致性協議 MESI 解決一致性問題。

下面先介紹這兩種寫入策略。

  • 寫直達(Write-Through):每一次數據都要寫入到主內存里面。
  • 寫回(Write-Back):數據寫到 CPU Cache 就結束。只有當 CPU Cache 是臟數據時,才把數據寫入主內存。

3.2 寫直達(Write-Through)

圖7:CPU 兩種寫策略

寫直達策略中,每一次數據都要寫入到主內存里面。寫入前,先判斷數據是否已經在 Cache 里面了。

  1. 命中緩存。如果數據已經在 Cache 里,先把數據寫入更新到 Cache 里面,再寫入到主內存里面;
  2. 未命中緩存。如果數據不在 Cache 里,只需要更新主內存。

寫直達的這個策略很直觀,但是問題也很明顯,那就是這個策略很慢。

3.3 寫回(Write-Back)

既然可以從 CPU Cache 里面加載數據,那么寫入時能否只寫入 CPU Cache 中,不用同步到主內存里呢?當然是可以的。CPU 提供了寫回策略,不再是每次都把數據寫入到主內存,而是只寫到 CPU Cache 里。只有當 CPU Cache 里面的數據要被“替換”的時候,我們才把數據寫入到主內存里面去。

  1. 命中緩存。如果要寫入的數據,就在 CPU Cache 里面,那么只是更新 CPU Cache 里面的數據。同時標記 CPU Cache 里的這個 Block 是臟(Dirty)的。所謂臟的,就是指這個時候,我們的 CPU Cache 里面的這個 Block 的數據,和主內存是不一致的。
  2. 未命中緩存。如果要寫入的數據所對應的 Cache Block 里,放的是別的內存地址的數據,需要判斷 Cache Block 里面的數據有沒有被標記成臟的。
    • 如果是臟數據,我們要先把這個 Cache Block 里面的數據,寫入到主內存里面。然后,再把當前要寫入的數據,寫入到 Cache 里,同時把 Cache Block 標記成臟的。
    • 如果沒有被標記成臟數據,那么我們直接把數據寫入到 Cache 里面,然后再把 Cache Block 標記成臟的就好了。
  3. 加載緩存。加載內存數據到 Cache 里面的時候,也要多出一步同步臟 Cache 的動作。如果加載內存里面的數據到 Cache 的時候,發現 Cache Block 里面有臟標記,我們也要先把 Cache Block 里的數據寫回到主內存,才能加載數據覆蓋掉 Cache。

可以看到,在寫回這個策略里,如果我們大量的操作,都能夠命中緩存。那么大部分時間里,我們都不需要讀寫主內存,自然性能會比寫直達的效果好很多。

參考:

  • What Every Programmer Should Know About Memory:深入了解 CPU 和內存之間的訪問性能。
  • Fixing Java Memory Model:JSR-133 為什么增強 volatile 的內存語義。
  • CPU 高速緩存的讀操作處理:《計算機組成與設計:硬件 / 軟件接口》的 5.4.1 小節。現代 CPU 已經很少使用直接映射 Cache 了,通常用的是組相連 Cache(set associative cache)。
  • CPU 高速緩存的寫操作處理:《計算機組成與設計:硬件 / 軟件接口》的 5.3.3 小節。

每天用心記錄一點點。內容也許不重要,但習慣很重要!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM