做者丨smcdef
來源丨http://www.wowotech.net/memory_management/458.htmlhtml
今天探究的主題是cache。咱們圍繞幾個問題展開。爲何須要cache?如何判斷一個數據在cache中是否命中?cache的種類有哪些,區別是什麼?程序員
對於沒有接觸過底層技術的朋友來講,或許從未據說過cache。畢竟cache的存在對程序員來講是透明的。在接觸cache以前,先爲你準備段code分析。數組
int arr[10][128]; 緩存
for (i = 0; i < 10; i++)架構
for (j = 0; j < 128; j++);app
arr[i][j] = 1;性能
若是你曾經學習過C/C++語言,這段code天然不會陌生。如此簡單的將arr數組全部元素置1。你有沒有想過這段code還有下面的一種寫法。學習
int arr[10][128];大數據
for (i = 0; i < 128; i++)編碼
for (j = 0; j < 10; j++);
arr[j][i] = 1;
功能徹底同樣,可是咱們一直在重複着第一種寫法(或許不少的書中也是建議這麼編碼),你是否想過這其中的原因?文章的主角是cache,因此你必定猜到了答案。那麼cache是如何影響這2段code的呢?
爲何須要cache memory
在思考cache是什麼以前咱們首先先來思考第一個問題:咱們的程序是如何運行起來的?
咱們應該知道程序是運行在 RAM之中,RAM 就是咱們常說的DDR(例如 DDR三、DDR4等)。咱們稱之爲main memory(主存)當咱們須要運行一個進程的時候,首先會從Flash設備(例如,eMMC、UFS等)中將可執行程序load到main memory中,而後開始執行。在CPU內部存在一堆的通用寄存器(register)。若是CPU須要將一個變量(假設地址是A)加1,通常分爲如下3個步驟:
咱們將這個過程能夠表示以下:
其實現實中,CPU通用寄存器的速度和主存之間存在着太大的差別。二者之間的速度大體以下關係:
CPU register的速度通常小於1ns,主存的速度通常是65ns左右。速度差別近百倍。所以,上面舉例的3個步驟中,步驟1和步驟3實際上速度很慢。當CPU試圖從主存中load/store 操做時,因爲主存的速度限制,CPU不得不等待這漫長的65ns時間。若是咱們能夠提高主存的速度,那麼系統將會得到很大的性能提高。
現在的DDR存儲設備,動不動就是幾個GB,容量很大。若是咱們採用更快材料製做更快速度的主存,而且擁有幾乎差很少的容量。其成本將會大幅度上升。
咱們試圖提高主存的速度和容量,又指望其成本很低,這就有點難爲人了。所以,咱們有一種折中的方法,那就是製做一塊速度極快可是容量極小的存儲設備。那麼其成本也不會過高。這塊存儲設備咱們稱之爲cache memory。
在硬件上,咱們將cache放置在CPU和主存之間,做爲主存數據的緩存。當CPU試圖從主存中load/store數據的時候, CPU會首先從cache中查找對應地址的數據是否緩存在cache中。若是其數據緩存在cache中,直接從cache中拿到數據並返回給CPU。當存在cache的時候,以上程序如何運行的例子的流程將會變成以下:
CPU和主存之間直接數據傳輸的方式轉變成CPU和cache之間直接數據傳輸。cache負責和主存之間數據傳輸。
多級cache memory
cahe的速度在必定程度上一樣影響着系統的性能。通常狀況cache的速度能夠達到1ns,幾乎能夠和CPU寄存器速度媲美。可是,這就知足人們對性能的追求了嗎?並無。當cache中沒有緩存咱們想要的數據的時候,依然須要漫長的等待從主存中load數據。
爲了進一步提高性能,引入多級cache。前面提到的cache,稱之爲L1 cache(第一級cache)。咱們在L1 cache 後面鏈接L2 cache,在L2 cache 和主存之間鏈接L3 cache。等級越高,速度越慢,容量越大。可是速度相比較主存而言,依然很快。不一樣等級cache速度之間關係以下:
通過3級cache的緩衝,各級cache和主存之間的速度最萌差也逐級減少。在一個真實的系統上,各級cache之間硬件上是如何關聯的呢?咱們看下Cortex-A53架構上各級cache之間的硬件抽象框圖以下:
在Cortex-A53架構上,L1 cache分爲單獨的instruction cache(ICache)和data cache(DCache)。L1 cache是CPU私有的,每一個CPU都有一個L1 cache。一個cluster 內的全部CPU共享一個L2 cache,L2 cache不區分指令和數據,均可以緩存。全部cluster之間共享L3 cache。L3 cache經過總線和主存相連。
多級cache之間的配合工做
首先引入兩個名詞概念,命中和缺失。CPU要訪問的數據在cache中有緩存,稱爲「命中」 (hit),反之則稱爲「缺失」 (miss)。多級cache之間是如何配合工做的呢?咱們假設如今考慮的系統只有兩級cache。
當CPU試圖從某地址load數據時,首先從L1 cache中查詢是否命中,若是命中則把數據返回給CPU。若是L1 cache缺失,則繼續從L2 cache中查找。當L2 cache命中時,數據會返回給L1 cache以及CPU。若是L2 cache也缺失,很不幸,咱們須要從主存中load數據,將數據返回給L2 cache、L1 cache及CPU。
這種多級cache的工做方式稱之爲inclusive cache。某一地址的數據可能存在多級緩存中。與inclusive cache對應的是exclusive cache,這種cache保證某一地址的數據緩存只會存在於多級cache其中一級。也就是說,任意地址的數據不可能同時在L1和L2 cache中緩存。
直接映射緩存(Direct mapped cache)
咱們繼續引入一些cache相關的名詞。cache的大小稱之爲cahe size,表明cache能夠緩存最大數據的大小。咱們將cache平均分紅相等的不少塊,每個塊大小稱之爲cache line,其大小是cache line size。
例如一個64 Bytes大小的cache。若是咱們將64 Bytes平均分紅64塊,那麼cache line就是1字節,總共64行cache line。若是咱們將64 Bytes平均分紅8塊,那麼cache line就是8字節,總共8行cache line。如今的硬件設計中,通常cache line的大小是4-128 Byts。爲何沒有1 byte呢?緣由咱們後面討論。
這裏有一點須要注意,cache line是cache和主存之間數據傳輸的最小單位。什麼意思呢?當CPU試圖load一個字節數據的時候,若是cache缺失,那麼cache控制器會從主存中一次性的load cache line大小的數據到cache中。例如,cache line大小是8字節。CPU即便讀取一個byte,在cache缺失後,cache會從主存中load 8字節填充整個cache line。又是由於什麼呢?後面說完就懂了。
咱們假設下面的講解都是針對64 Bytes大小的cache,而且cache line大小是8字節。咱們能夠相似把這塊cache想一想成一個數組,數組總共8個元素,每一個元素大小是8字節。就像下圖這樣:
如今咱們考慮一個問題,CPU從0x0654地址讀取一個字節,cache控制器是如何判斷數據是否在cache中命中呢?cache大小相對於主存來講,可謂是小巫見大巫。因此cache確定是只能緩存主存中極小一部分數據。咱們如何根據地址在有限大小的cache中查找數據呢?如今硬件採起的作法是對地址進行散列(能夠理解成地址取模操做)。咱們接下來看看是如何作到的?
咱們一共有8行cache line,cache line大小是8 Bytes。因此咱們能夠利用地址低3 bits(如上圖地址藍色部分)用來尋址8 bytes中某一字節,咱們稱這部分bit組合爲offset。同理,8行cache line,爲了覆蓋全部行。咱們須要3 bits(如上圖地址黃色部分)查找某一行,這部分地址部分稱之爲index。
如今咱們知道,若是兩個不一樣的地址,其地址的bit3-bit5若是徹底同樣的話,那麼這兩個地址通過硬件散列以後都會找到同一個cache line。因此,當咱們找到cache line以後,只表明咱們訪問的地址對應的數據可能存在這個cache line中,可是也有多是其餘地址對應的數據。因此,咱們又引入tag array區域,tag array和data array一一對應。
每個cache line都對應惟一一個tag,tag中保存的是整個地址位寬去除index和offset使用的bit剩餘部分(如上圖地址綠色部分)。tag、index和offset三者組合就能夠惟一肯定一個地址了。
所以,當咱們根據地址中index位找到cache line後,取出當前cache line對應的tag,而後和地址中的tag進行比較,若是相等,這說明cache命中。若是不相等,說明當前cache line存儲的是其餘地址的數據,這就是cache缺失。
在上述圖中,咱們看到tag的值是0x19,和地址中的tag部分相等,所以在本次訪問會命中。因爲tag的引入,所以解答了咱們以前的一個疑問「爲何硬件cache line不作成一個字節?」。這樣會致使硬件成本的上升,由於本來8個字節對應一個tag,如今須要8個tag,佔用了不少內存。
咱們能夠從圖中看到tag旁邊還有一個valid bit,這個bit用來表示cache line中數據是否有效(例如:1表明有效;0表明無效)。當系統剛啓動時,cache中的數據都應該是無效的,由於尚未緩存任何數據。cache控制器能夠根據valid bit確認當前cache line數據是否有效。因此,上述比較tag確認cache line是否命中以前還會檢查valid bit是否有效。只有在有效的狀況下,比較tag纔有意義。若是無效,直接斷定cache缺失。
上面的例子中,cache size是64 Bytes而且cache line size是8 bytes。offset、index和tag分別使用3 bits、3 bits和42 bits(假設地址寬度是48 bits)。咱們如今再看一個例子:512 Bytes cache size,64 Bytes cache line size。根據以前的地址劃分方法,offset、index和tag分別使用6 bits、3 bits和39 bits。以下圖所示:
直接映射緩存的優缺點
直接映射緩存在硬件設計上會更加簡單,所以成本上也會較低。根據直接映射緩存的工做方式,咱們能夠畫出主存地址0x00-0x88地址對應的cache分佈圖:
咱們能夠看到,地址0x00-0x3f地址處對應的數據能夠覆蓋整個cache。0x40-0x7f地址的數據也一樣是覆蓋整個cache。咱們如今思考一個問題,若是一個程序試圖依次訪問地址0x00、0x40、0x80,cache中的數據會發生什麼呢?首先咱們應該明白0x00、0x40、0x80地址中index部分是同樣的。所以,這3個地址對應的cache line是同一個。
因此,當咱們訪問0x00地址時,cache會缺失,而後數據會從主存中加載到cache中第0行cache line。當咱們訪問0x40地址時,依然索引到cache中第0行cache line,因爲此時cache line中存儲的是地址0x00地址對應的數據,因此此時依然會cache缺失。而後從主存中加載0x40地址數據到第一行cache line中。
同理,繼續訪問0x80地址,依然會cache缺失。這就至關於每次訪問數據都要從主存中讀取,因此cache的存在並無對性能有什麼提高。訪問0x40地址時,就會把0x00地址緩存的數據替換。這種現象叫作cache顛簸(cache thrashing)。
針對這個問題,咱們引入多路組相連緩存。咱們首先研究下最簡單的兩路組相連緩存的工做原理。
兩路組相連緩存
咱們依然假設64 Bytes cache size,cache line size是8 Bytes。什麼是路(way)的概念。咱們將cache平均分紅多份,每一份就是一路。所以,兩路組相連緩存(Two-way set associative cache)就是將cache平均分紅2份,每份32 Bytes。以下圖所示:
cache被分紅2路,每路包含4行cache line。咱們將全部索引同樣的cache line組合在一塊兒稱之爲組。例如,上圖中一個組有兩個cache line,總共4個組。咱們依然假設從地址0x0654地址讀取一個字節數據。因爲cache line size是8 Bytes,所以offset須要3 bits,這和以前直接映射緩存同樣。
不同的地方是index,在兩路組相連緩存中,index只須要2 bits,由於一路只有4行cache line。上面的例子根據index找到第2行cache line(從0開始計算),第2行對應2個cache line,分別對應way 0和way 1。所以index也能夠稱做set index(組索引)。先根據index找到set,而後將組內的全部cache line對應的tag取出來和地址中的tag部分對比,若是其中一個相等就意味着命中。
所以,兩路組相連緩存較直接映射緩存最大的差別就是:第一個地址對應的數據能夠對應2個cache line,而直接映射緩存一個地址只對應一個cache line。那麼這究竟有什麼好處呢?
兩路組相連緩存優缺點
兩路組相連緩存的硬件成本相對於直接映射緩存更高。由於其每次比較tag的時候須要比較多個cache line對應的tag(某些硬件可能還會作並行比較,增長比較速度,這就增長了硬件設計複雜度)。
爲何咱們還須要兩路組相連緩存呢?由於其能夠有助於下降cache顛簸可能性。那麼是如何下降的呢?根據兩路組相連緩存的工做方式,咱們能夠畫出主存地址0x00-0x4f地址對應的cache分佈圖:
咱們依然考慮直接映射緩存一節的問題「若是一個程序試圖依次訪問地址0x00、0x40、0x80,cache中的數據會發生什麼呢?」。如今0x00地址的數據能夠被加載到way 1,0x40能夠被加載到way 0。這樣是否是就在必定程度上避免了直接映射緩存的尷尬境地呢?在兩路組相連緩存的狀況下,0x00和0x40地址的數據都緩存在cache中。試想一下,若是咱們是4路組相連緩存,後面繼續訪問0x80,也可能被被緩存。
所以,當cache size必定的狀況下,組相連緩存對性能的提高最差狀況下也和直接映射緩存同樣,在大部分狀況下組相連緩存效果比直接映射緩存好。同時,其下降了cache顛簸的頻率。從某種程度上來講,直接映射緩存是組相連緩存的一種特殊狀況,每一個組只有一個cache line而已。所以,直接映射緩存也能夠稱做單路組相連緩存。
全相連緩存(Full associative cache)
既然組相連緩存那麼好,若是全部的cache line都在一個組內。豈不是性能更好。是的,這種緩存就是全相連緩存。咱們依然以64 Byts大小cache爲例說明。
因爲全部的cache line都在一個組內,所以地址中不須要set index部分。由於,只有一個組讓你選擇,間接來講就是你沒得選。咱們根據地址中的tag部分和全部的cache line對應的tag進行比較(硬件上可能並行比較也可能串行比較)。哪一個tag比較相等,就意味着命中某個cache line。所以,在全相連緩存中,任意地址的數據能夠緩存在任意的cache line中。因此,這能夠最大程度的下降cache顛簸的頻率。可是硬件成本上也是更高。
一個四路組相連緩存實例問題
考慮這麼一個問題,32 KB大小4路組相連cache,cache line大小是32 Bytes。請思考一下問題:
總共4路,所以每路大小是8 KB。cache line size是32 Bytes,所以一共有256組(8 KB / 32 Bytes)。因爲cache line size是32 Bytes,因此offset須要5位。一共256組,因此index須要8位,剩下的就是tag部分,佔用35位。這個cache能夠繪製下圖表示:
Cache分配策略
cache的分配策略(Cache allocation policy)是指咱們什麼狀況下應該爲數據分配cache line。cache分配策略分爲讀和寫兩種狀況。
讀分配(read allocation)
當CPU讀數據時,發生cache缺失,這種狀況下都會分配一個cache line緩存從主存讀取的數據。默認狀況下,cache都支持讀分配。
寫分配(write allocation)
當CPU寫數據發生cache缺失時,纔會考慮寫分配策略。當咱們不支持寫分配的狀況下,寫指令只會更新主存數據,而後就結束了。當支持寫分配的時候,咱們首先從主存中加載數據到cache line中(至關於先作個讀分配動做),而後會更新cache line中的數據。
Cache更新策略
cache更新策略(Cache update policy)是指當發生cache命中時,寫操做應該如何更新數據。cache更新策略分紅兩種:寫直通和回寫。
寫直通(write through)
當CPU執行store指令並在cache命中時,咱們更新cache中的數據而且更新主存中的數據。cache和主存的數據始終保持一致。
寫回(write back)
當CPU執行store指令並在cache命中時,咱們只更新cache中的數據。而且每一個cache line中會有一個bit位記錄數據是否被修改過,稱之爲dirty bit(翻翻前面的圖片,cache line旁邊有一個D就是dirty bit)。咱們會將dirty bit置位。主存中的數據只會在cache line被替換或者顯示clean操做時更新。所以,主存中的數據多是未修改的數據,而修改的數據躺在cache line中。cache和主存的數據可能不一致。
同時思考個問題,爲何cache line大小是cache控制器和主存之間數據傳輸的最小單位呢?這也是由於每一個cache line只有一個dirty bit。這一個dirty bit表明着整個cache line是否被修改的狀態。
實例
假設咱們有一個64 Bytes大小直接映射緩存,cache line大小是8 Bytes,採用寫分配和寫回機制。當CPU從地址0x2a讀取一個字節,cache中的數據將會如何變化呢?假設當前cache狀態以下圖所示:
根據index找到對應的cache line,對應的tag部分valid bit是合法的,可是tag的值不相等,所以發生缺失。此時咱們須要從地址0x28地址加載8字節數據到該cache line中。可是,咱們發現當前cache line的dirty bit置位。所以,cache line裏面的數據不能被簡單的丟棄,因爲採用寫回機制,因此咱們須要將cache中的數據0x11223344寫到地址0x0128地址(這個地址根據tag中的值及所處的cache line行計算獲得)。這個過程以下圖所示:
當寫回操做完成,咱們將主存中0x28地址開始的8個字節加載到該cache line中,並清除dirty bit。而後根據offset找到0x52返回給CPU。