深入理解處理器高速緩存的工作機制
一、為什麽要使用緩存
由於不同的存儲技術在存儲速度和造價上相差巨大,為了高效的訪問數據,現代計算機的存儲系統會把最常用的數據放在讀存速度快的存儲設備上,而把不常用的數據放在讀存速度慢的存儲設備上。存儲器系統是一個具有不同容量、成本和訪問時間的存儲設備的層級結構。從上往下容量越來越大,但訪問速度越來越慢。上一層做為下一層的緩存來存儲訪問頻率更高的數據,比如,cpu寄存器保存著最常用的數據。靠近CPU的小的、快速的高速緩存存儲器是內存上一部分數據和指令的緩沖區域。主存緩存磁盤上的數據,而這些磁盤又常常作為存儲在通過網絡連接的其他機器的磁盤或磁帶上的數據的緩沖區域。存儲層次如下:
二、緩存如何判斷哪些數據是更常用的
緩存上存儲的數據是那些計算機認為在接下來更有可能訪問到的數據,計算機如何判斷哪些數據接下來更有可能用到呢?系統對程序員編寫的代碼有兩種假設:
1. 時間局部性
時間局部性假設目前訪問的數據在接下來也更有可能再次訪問到,所以計算機會把剛剛訪問過的數據放入緩存中;
2. 空間局部性
空間局部性假設與目前訪問的數據相鄰的那些數據接下來也更有可能訪問到,所以會把當前數據周圍的數據放入緩存中;
一個編寫良好的代碼往往符合時間局部性和空間局部性。
三、數據在存儲器層次之間以塊為單位進行傳遞
存儲器層次結構的本質是,每一層存儲設備都是較低一層的緩存。為了利用空間局部性,存儲器上的數據都是按塊劃分的,每個塊包含多個字節的數據。第k層的緩存包含第k+1層塊的一個子集的副本。數據總是以塊為傳送單位在第k層和第k+1層來回復制的。在層次結構中任何一對相鄰的層次之間塊大小是一樣的。不過不同的層次之間塊大小可以不同,一般而言,越靠近底層,越傾向於使用較大的塊。
四、cup如何訪問數據
當程序需要第k+1層的某個數據時,它首先在第k層的一個塊中查找d,這裏會出現兩種情況:
1. 緩存命中
d剛好緩存在k層中,這裏稱之為緩存命中,該程序直接從k層讀取d,根據存儲器層次結構,這要比從k+1層取數據更快。
2. 緩存不命中
d沒有緩存在k中,這種情況稱之為緩存不命中,第k層的緩存從第k+1層中取出包含d的那個塊,放在k層中,然後從k層讀出d。這裏涉及一個問題,即從k+1層中取出的塊應如何放置在k層中,這裏需要某種放置策略。可用的策略如下:
(1)隨機放置,在k中隨機選擇一個位置進行放置,這種策略實現起來通常很昂貴,因為不好定位;
(2)分組放置,將第k+1層的某個塊放置在第k層塊的某個小組(子集)中;
五、從一個簡單的層次模型理解高速緩存是如何實現的
1. 高速緩存的結構及與地址的對應關系
高速緩存使用分組放置策略,它把存儲空間分為S組,每組E行,每行包含1個塊,每個塊的大小為B,它的結構可以用四元組(S,E,B,m)來表示,其中m代表地址位的個數。高速緩存的容量為C=S*E*B;
當訪問數據時,我們唯一知道的是數據的地址。通過地址我們如何從緩存中找到數據所在的組號,塊號和塊中的位置呢?這裏m個地址位分為了3個字段,其中高位的字段為t個標記位,它唯一的標記了組中的塊,中間的字段為s個索引位,它標記了組號,最後的字段為塊偏移,通過它可以訪問塊中具體的字節。另外緩存結構中每一行最前面還有一個有效位,它標誌了這一行有沒有存儲塊。緩存結構如下:
2. 一個簡單的存儲模型
下面定義一個簡單的存儲層次模型:
(1)cpu和內存之間只加了一層高速緩存;
(2)存儲地址為4位,即內存中最多存16個字節;
4位的地址被分成了3段:最高位的1段為標記位,中間2位為索引組,最後1位為塊偏移。從索引組的的位數可判斷緩存被分成了4組,1位的塊偏移說明塊的大小為2個字節。這裏假設每組只有1行,這種情況稱為直接映射高速緩存。從以上分段可知,緩存大小為 4*2*1=8 字節;
舉例:地址0101;其中高位的0為標記位,標記組中的1個塊,中間的兩位10為組索引,通過組索引可知當前地址的塊存在組2中,最後1位為偏移位,說明要取的字節存在塊中的第2個位置。
3. 緩存過程
(1)初始狀態緩存是空的,當cpu通過地址(假設為0001)要加載一個數據時,此時cpu會先查找高速緩存,通過中間的地址00找到第0組,通過緩存最前面的有效位判斷緩存不命中,然後從內存中取得地址0001處的值。註意此時不會從內存中只取0001處的值,而是根據緩存的塊的大小取得1個塊,即2個字節,即0000和0001兩處的值。然後會把這兩個值存儲在組索引為0,偏移量為0和1的地址處,並且把當前行的有效位設置為1,標記位設置為0,最後高速緩存返回新取出的高速緩存塊[0]處的值;
(2)接下來,cpu如果要取0000處的值,此時也會先查找緩存,通過中間2位00找到緩存的組數為0,通過第1個有效位(1)和標記位(0)判斷緩存命中,此時會直接返回偏移量0處的值,不需要再查找內存;
(3)如果接下來cpu要取1000處的值,此時會查找到組0,由於地址的標記位為1,和緩存組0中行的標記位(0)不符合,所以緩存不命中,cpu會從內存中取得相應的塊,並把緩存組0處的數據覆蓋掉,此時會把組的標識位設置為1。
(4)接下來再讀地址0000處的值,此時又會發生緩存不命中,因為上面引用地址1000時,把塊替換掉了。這種情況稱為沖突不命中,也就是說我們有足夠的高速緩存空間,但是卻交替地引用映射到同一個組的塊;
小結:
cpu加載機制為,通過地址先查找緩存,查找過程為,先通過地址中間的組索引位,找到組,然後根據組中行的有效位和標記位判斷是否緩存命中,如果命中,則直接從緩存中取數據;如果沒命中,則從內存中取出1個塊,並用某種放置策略放在緩存中,然後從緩存返回值。在直接映射高速緩中,內存中的空間和緩存中的空間是一種多對一的映射關系,比如本例中地址0和地址8所指的內存空間的內容,在緩存中都存在第0組偏移量為0的空間中;他們在緩存中的區分由地址的最高位即標記位決定。
六、解決緩存沖突問題
1. 什麽是緩存沖突
當每個組只有一行的情況下,映射為同一組的塊在緩存中將占用同一個存儲空間,當反復加載位於同一組的兩個塊時,由於每次加載都會把先前加載的塊覆蓋掉,導致cpu對緩存的命中率為0,雖然此時在其他組依然有大量的緩存空間。
2.組相聯高級緩存
為了降低緩存沖突,可以把每個組分為多行,每行存儲一個塊。cpu通過組索引查找塊所在的組,然後通過有效位和標記位檢查多行來判斷是否緩存命中。當在緩存中找不到時,會從內存中加載一個塊到緩存相對應的組中,當組內有空行時,會直接加載到空的行,否則緩存會使用某種替換策略來換掉組內的一行。常用的替換策略有隨機替換,最近最少使用原則,即替換掉組內最長時間未使用的行。當緩存中只有1個組,組中包含所有行時,稱為全相聯高速緩存。
七、如何編寫緩存友好的代碼
為了更好的利用緩存,提高代碼執行速度。需要編寫局部性良好的代碼,提高緩存命中率,下面是基本方法
1. 把註意力集中在核心函數的循環上;
2. 對局部變量的反復引用有良好的時間局部性;
3. 步長為1的引用模型有很好的空間局部性,當掃描二維數組時要一行一行的掃描,而不是一列一列的掃描;
深入理解處理器高速緩存的工作機制