CPU緩存相關知識——cache

歡迎使用Markdown編輯器

主要分成這麼幾個部分:基礎知識、緩存的命中、緩存的一致性、相關的代碼示例和延伸閱讀。其中會講述一些多核 CPU 的系統架構以及其原理,包括對程序性能上的影響,以及在進行併發編程的時候需要注意到的一些問題。這篇文章我會盡量地寫簡單和通俗易懂一些,主要是講清楚相關的原理和問題,而對於一些細節和延伸閱讀我會在文章最後會給出相關的資源。

因爲無論你寫什麼樣的代碼都會交給CPU來執行,所以,如果你想寫出性能比較高的代碼,這篇文章中提到的技術還是值得認真學習的。另外,千萬別覺得這些東西沒用,這些東西非常有用,十多年前就是這些知識在性能調優上幫了我的很多大忙,從而跟很多人拉開了差距……

基礎知識

首先,我們都知道現在的CPU多核技術,都會有幾級緩存,老的CPU會有兩級內存(L1和L2),新的CPU會有三級內存(L1,L2,L3 ),如下圖所示:
在這裏插入圖片描述
其中:

  • L1緩分成兩種,一種是指令緩存,一種是數據緩存。L2緩存和L3緩存不分指令和數據。
  • L1和L2緩存在每一個CPU核中,L3則是所有CPU核心共享的內存。
  • L1、L2、L3的越離CPU近就越小,速度也越快,越離CPU遠,速度也越慢。

再往後面就是內存,內存的後面就是硬盤。我們來看一些他們的速度:

  • L1 的存取速度:4 個CPU時鐘週期
  • L2 的存取速度: 11 個CPU時鐘週期
  • L3 的存取速度:39 個CPU時鐘週期
  • RAM內存的存取速度:107 個CPU時鐘週期

我們可以看到,L1的速度是RAM的27倍,但是L1/L2的大小基本上也就是KB級別的,L3會是MB級別的。例如:Intel Core i7-8700K ,是一個6核的CPU,每核上的L1是64KB(數據和指令各32KB),L2 是 256K,L3有2MB(我的蘋果電腦是 Intel Core i9-8950HK,和Core i7-8700K的Cache大小一樣)。

我們的數據就從內存向上,先到L3,再到L2,再到L1,最後到寄存器進行CPU計算。爲什麼會設計成三層?這裏有下面幾個方面的考慮:

  • 一個方面是物理速度,如果要更大的容量就需要更多的晶體管,除了芯片的體積會變大,更重要的是大量的晶體管會導致速度下降,因爲訪問速度和要訪問的晶體管所在的位置成反比,也就是當信號路徑變長時,通信速度會變慢。這部分是物理問題。
  • 另外一個問題是,多核技術中,數據的狀態需要在多個CPU中進行同步,並且,我們可以看到,cache和RAM的速度差距太大,所以,多級不同尺寸的緩存有利於提高整體的性能。

這個世界永遠是平衡的,一面變得有多光鮮,另一面也會變得有多黑暗。建立這麼多級的緩存,一定就會引入其它的問題,這裏有兩個比較重要的問題,

  • 一個是比較簡單的緩存的命中率的問題。
  • 另一個是比較複雜的緩存更新的一致性問題。

尤其是第二個問題,在多核技術下,這就很像分佈式的系統了,要對多個地方進行更新。

緩存的命中

在說明這兩個問題之前。我們需要要解一個術語 Cache Line。緩存基本上來說就是把後面的數據加載到離自己近的地方,對於CPU來說,它是不會一個字節一個字節的加載的,因爲這非常沒有效率,一般來說都是要一塊一塊的加載的,對於這樣的一塊一塊的數據單位,術語叫“Cache Line”,一般來說,一個主流的CPU的Cache Line 是 64 Bytes(也有的CPU用32Bytes和128Bytes),64Bytes也就是16個32位的整型,這就是CPU從內存中撈數據上來的最小數據單位。

比如:Cache Line是最小單位(64Bytes),所以先把Cache分佈多個Cache Line,比如:L1有32KB,那麼,32KB/64B = 512 個 Cache Line。

一方面,緩存需要把內存裏的數據放到放進來,英文叫 CPU Associativity。Cache的數據放置的策略決定了內存中的數據塊會拷貝到CPU Cache中的哪個位置上,因爲Cache的大小遠遠小於內存,所以,需要有一種地址關聯的算法,能夠讓內存中的數據可以被映射到Cache中來。這個有點像內存地址從邏輯地址向物理地址映射的方法,但不完全一樣。

基本上來說,我們會有如下的一些方法。

  • 一種方法是,任何一個內存地址的數據可以被緩存在任何一個Cache
    Line裏,這種方法是最靈活的,但是,如果我們要知道一個內存是否存在於Cache中,我們就需要進行O(n)複雜度的Cache遍歷,這是很沒有效率的。
  • 另一種方法,爲了降低緩存搜索算法,我們需要使用像Hash Table這樣的數據結構,最簡單的hash
    table就是做“求模運算”,比如:我們的L1 Cache有512個Cache Line,那麼,公式:(內存地址 mod 512)* 64
    就可以直接找到所在的Cache地址的偏移了。但是,這樣的方式需要我們的程序對內存地址的訪問要非常地平均,不然衝突就會非常嚴重。這成了一種非常理想的情況了。
  • 爲了避免上述的兩種方案的問題,於是就要容忍一定的hash衝突,也就出現了 N-Way 關聯。也就是把連續的N個Cache
    Line綁成一組,然後,先把找到相關的組,然後再在這個組內找到相關的Cache Line。這叫 Set
    Associativity。如下圖所示。
    在這裏插入圖片描述
    對於 N-Way 組關聯,可能有點不好理解,這裏個例子,並多說一些細節(不然後面的代碼你會不能理解),Intel 大多數處理器的L1 Cache都是32KB,8-Way 組相聯,Cache Line 是64 Bytes。這意味着,
  • 32KB的可以分成,32KB / 64 = 512 條 Cache Line。
  • 因爲有8 Way,於是會每一Way 有 512 / 8 = 64 條 Cache Line。
  • 於是每一路就有 64 x 64 = 4096 Byts 的內存。

爲了方便索引內存地址,

  • Tag:每條 Cache Line 前都會有一個獨立分配的 24 bits來存的 tag,其就是內存地址的前24bits
  • Index:內存地址後續的6個bits則是在這一Way的是Cache Line 索引,2^6 = 64 剛好可以索引64條Cache Line
  • Offset:再往後的6bits用於表示在Cache Line 裏的偏移量

當拿到一個內存地址的時候,先拿出中間的 6bits 來,找到是哪組。
在這裏插入圖片描述
然後,在這一個8組的cache line中,再進行O(n) n=8 的遍歷,主是要匹配前24bits的tag。如果匹配中了,就算命中,如果沒有匹配到,那就是cache miss,如果是讀操作,就需要進向後面的緩存進行訪問了。L2/L3同樣是這樣的算法。而淘汰算法有兩種,一種是隨機一種是LRU。現在一般都是以LRU的算法(通過增加一個訪問計數器來實現)在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章