1. 程式人生 > 其它 >計算機組成與設計-Cache基本原理

計算機組成與設計-Cache基本原理

Cache的基本原理

在學習Cache的基本原理之前,我們首先先介紹什麼是時間區域性性以及空間區域性性,我們會用一個例子說明儲存順序是如何對我們編寫的程式效能產生影響的。

時間區域性性和空間區域性性

我們仍然回到我們一開始講的那個圖書館的例子,如果你已經忘了,我們重新來回顧一下那個例子:

想象你正坐在圖書館中完成一份關於計算機硬體重要歷史性發展的論文,你可以從圖書館的書架上精心挑選一些經典的計算機書籍,並將它們放在書桌上。你從這些書中找到了需要寫的幾種重要的計算機,但是沒有找到關於EDSAC的,因此,你返回書架去尋找其他書,並在早期的英國計算機書籍中找到了一本有關EDSAC的書。一旦在你的書桌上有了選好的一些書,你就有可能從這些書中找到你需要的內容。這樣一來,你的大部分時間只需花在閱讀這些書上,而無需返回書架。

試比較這兩種情況:一種是在你的書桌上有好幾本書;另一種是書桌上只有一本書,你不得不頻繁的返回書架,進行還書後取另一本書。很明顯,在書桌前放一些常用的書籍會更加節省時間。

同樣,在計算機底層實現中,我們可以構建一個大容量的虛擬儲存器,它能像小容量的儲存器那樣被快速訪問。就像你不會同時以相同的概率查閱圖書館中的每一本書那樣,一個程式自然也不會同時以同樣的概率訪問全部的程式碼和資料。否則,不可能讓儲存器在保持大容量的同時又能快速訪問,就像你不能既要求你把圖書館中所有的圖書都放在書桌上,還要求你能保持快速查詢一樣。

這就是區域性性原理的理解,區域性性原理表明了在任何時間內,程式訪問的只是地址空間內較小的一段內容

  • 時間區域性性:如果某個資料被訪問,那麼在不久的將來它可能再次被訪問。
  • 空間區域性性:如果某個資料項被訪問,那麼在不久的將來,與它地址相鄰的資料項也可能被訪問。

程式的區域性性起源於簡單自然的程式結構。例如,大多數程式都包含了迴圈結構,因此這部分指令和資料被重複的訪問,呈現出來很高的時間區域性性。由於指令通常是順序執行的,因此也體現出很高的空間區域性性。

在我們用C語言編寫訪問二維陣列程式的時候,你有沒有考慮過如果我們先訪問陣列的列再訪問陣列的行,會對程式的效能產生何種影響呢?

在這個程式中,我們首先訪問陣列的行接著訪問陣列的列。

我們將這個程式改為先訪問列,再訪問行。

雖然得到的結果是完全一致的,但是程式的空間區域性性變差,程式的效能受到了較大影響。

一般而言,有良好區域性性的程式比區域性性差的程式執行的更快

先訪問列導致空間區域性性變差的原因是在記憶體中的儲存也是按照行有限的順序進行儲存的。(具體可看上節內容)

Cache的基本原理

在前面介紹的圖書館例子中,書桌就好比Cache(快取記憶體),快取在如今的計算機中幾乎無處不在,用途十分廣泛。

例如:圖中第 \(K+1\)層的儲存器被劃分成了16個大小固定的塊,每個塊都有唯一的地址 這裡我們用編號0-15來表示。第\(K\)層的儲存器有4個塊的空間,每個塊的大小與\(K+1\)層的塊一樣。

資料總是以塊為單元 在第\(K\)層和第\(K\)加一層之間來回複製。例如 當前第\(K\)層的儲存器包含了四個塊的副本,對於相鄰層之間的塊大小是固定的,然而 不相鄰的層次之間塊大小是不一樣的。一般來說 層次結構中離CPU越遠的裝置,訪問時間就越長。

接下來,我們將重新介紹在之前已經提到的幾個快取相關概念。

當程式需要讀取第\(K+1\)層的某個資料物件\(d\)時,它首先從第\(K\)層的資料塊中檢索是否包含目標資料d的副本,如果目標資料d剛好快取在第\(K\)層中 我們將這種情況成為快取命中。另一方面,如果第\(k\)層沒有快取目標資料d 我們將這種情況成為快取不命中

當發生不命中時,第\(k\)層的快取要從第\(k+1\)層取出包含目標資料的塊。如果第\(K\)層的快取已經滿了,這時包含目標資料的塊就會覆蓋現存的一個塊。我們把這個過程稱為替換。被替換的塊也稱為犧牲塊

決定替換哪個塊是由快取的替換策略來具體決定的

接下來 我們重點來看一下基於SRAM的快取記憶體。

早期計算機系統的儲存層次結構只有三層 分別是計算器 檔案 記憶體以及磁碟。

由於CPU與記憶體之間的效能差距逐漸增大,於是系統設計者在暫存器檔案和記憶體之間插入了SRAM的快取記憶體(L1 cache L2cache)。

Cache的內部結構

整個cache被劃分成一個或者多個set,這裡我們用變數S來表示set的個數,每個set包含一個或者多個cache line(高速緩衝行)。這裡我們用變數e來表示一個set中cache line的行數。

每個cache line由三部分組成 分別是有效位、標記、資料塊。

  • 其中有效位的長度是一個bit ,表示當前開始按儲存的資訊是否有效。當valid為1時表示資料有效 當valid為零時表示資料無效。
  • 標記是用來確定目標資料是否存在於當前的cache line中。
  • 資料塊就是一小部分記憶體資料的副本,大小用B來表示

通常來說,Cache的結構可以用元組\({S,E,B,m}\)來描述

Cache的大小是指所有資料塊的和,其中有效位和標記位不包括在內。

因此 開始的容量可以通過:

\[S×E×B \]

得到。

Cache的直接對映

我們如何確定資料在cache中的位置呢?我們需要引入一種叫做直接對映的方法。

在Cache中為主存的每個字分配一個位置的最簡單方法就是根據這個字的主存地址進行分配,這種Cache結構稱為直接對映。每個儲存器地址對應到Cache中一個確定的地址。我們可以使用以下的對映方法確定:

\[(塊地址)mod(cache中的塊數) \]

如果cache中的塊數是2的冪,那麼我們只主要取地址的低\(log_2\)位。因此,一個8塊的cache可以使用塊地址中的低三位

對標記的深入理解

標記中包含了地址資訊,這些地址資訊可以用來判斷cache中的字是否就是所請求的字。標記只需包含地址的高位,也就是沒有用來檢索cache的那些位。

Cache的尋找過程

搞清楚了cache的內部結構以及cache的索引原則,我們就可以開始瞭解cache的工作流程了,也就是cache是如何尋找對應的資料的。

首先 我們可以通過長度為s的組索引位來確定目標資料儲存在哪個set中,一旦我們知道了目標資料屬於哪個set。接下來 我們需要確定目標資料放在哪一行,確定具體的行是通過長度為t的標記來實現的。不過還需要注意一點 此時有效位必須為1。也就是說需要有效位和標記共同來確定目標資料屬於哪一行。最後 我們需要根據長度為b的塊偏移量來確定目標資料在資料塊中的確切地址。通過以上三步開始就能確定是否命中。

直接對映cache的工作原理

當每個sit只有一個cache line,也就是e等於1時,我們將這種結構的開始成為直接對映。

首先 我們先來看一下直接對映的開始是如何工作的。

判斷是否命中,獲取目標資料的過程一共分為三步:

  1. 組選擇
  2. 行匹配
  3. 字抽取

組選擇

這一步是根據組索引值來確定目標資料屬於哪個set

行匹配

而且當前cache line的有效位等於1,此時cache line中的資料是有效的。

然後我們需要對比cache line中的標記與地址中的標記位是否一致。如果一致,表示目標資料一定在當前的cache line中。另一方面,如果不一致或者有效位等於零,表示目標資料不在當前的cache line中。因此 行匹配最終的結果無非就是命中或者不命中

字抽取

一旦命中,就可以繼續執行第三步--字抽取。這一步需要根據偏移量來確定目標資料的確切位置。通俗來講就是從資料塊的什麼位置開始抽取資料。

當塊偏移等於100時,他表明目標資料的起始地址位於位元組4處。

經過以上三步開始就可以將目標資料返回給CPU,上述過程就是開始命中的情況。

Cache缺失

如果發生了不命中,那麼cache需要從儲存器層次結構的下一層取出被請求的塊。由於直接對映的每個sit只包含一行,因此替換策略十分簡單。直接用新取出的行來代替當前的行就可以了。

cache缺失:由於資料不在cache中而導致被請求的資料不能滿足

cache缺失處理由兩部分共同完成:處理器控制單元以及一個進行初始化主存訪問和重新填充cache的獨立控制器。當cache缺失,我們等待主存操作完成時(可以理解為替換),整個處理器阻塞,臨時暫存器和可見的暫存器內容被凍結。

讀操作中的衝突不命中

衝突不命中指的是A與B交替佔用了同一個快取空間,隨著時間的進行,A與B反覆對快取空間中的同一個區域進行替換操作。

我們看一個例子:

每個元素的長度為四個位元組,因此可以得到陣列x各個元素的起始地址。陣列y緊跟其後,\(y[0]\)的地址從32開始。

當程式開始執行時 迴圈在第一次迭代時引用了元素\(x[0]\)此時發生不命中,cache把包含\(x[0] - x[3]\)的塊載入到\(set_0\),接下來又立刻引用了陣列元素 \(y[0]\)又一次不命中,這時開始把包含\(y[0]-y[3]\)的塊載入到\(set 0\)

這裡需要注意的是,之前\(set0\)中儲存的內容是資料塊\(x[0] - x[3]\)的資料。那麼 這些資料會被\(y[0]-y[3]\)覆蓋。

實際上 後面每次對x和y的引用都會導致cache line的替換。

我們把這種現象稱為”抖動“

衝突不命中的原因是這些塊被對映到了同一個set中,我們可以將陣列的長度由8變為12即可解決問題。

此時 陣列y的起始地址發生了改變。這樣一來通過這種資料填充的方式就可以消除抖動,從而解決衝突不命中的問題。

組相聯 全相聯快取記憶體

之前我們講述的內容才用的是最簡單的定位機制:一個塊只能放到cache中一個明確的位置。實際上,有一整套放置塊的方法。直接對映是一種極端的情況,此時一個塊被精確地放到一個位置。

另一種極端方式是,一個塊可以被放置在cache中的任何位置,這種機制被稱為全相聯,因為儲存器的快可以與cache中任何一項相關。全相聯只適合塊數較少的cache。

介於直接對映和全相聯之間的設計是組相聯。在組相聯cache中,每個塊可被放置的位置數是固定的。每個塊有n個位置可放的cache被稱為n路組相聯cache。

組相聯包含儲存塊的組是這樣給出的:

\[(塊號)mod(cache中的組數) \]

由於塊可能被放在組中的任何位置,因此組中所有塊的標記都要被檢索。而在全相聯cache中,塊可能被放在任何位置,所以也都要被檢索。

提高相聯度的好處在於它通常能夠降低缺失率,缺點則是增加了命中時間。

組相聯全相聯查詢

組相聯

組相聯的查詢同樣需要執行三步

如果找不到符合條件的,cache line表示不命中。此時開始必須從記憶體中取出包含目標資料的塊,不過一旦開始取出 這個塊應該替換哪一行呢。

如果存在空行 也就是valid等於零的cache line,那麼這個空行就是不錯的選擇。

但是 如果這個set中沒有空行,這時我們需要從中選擇一個非空行作為被替換的物件

下面介紹了幾種常用的替換策略:

最近最少使用法也就是LRU演算法,是最常用的方法。

全相聯

這樣一來 地址只需要劃分成標記和塊偏移即可。

關於全相聯cache的行匹配和字選擇與組相聯cache是一樣的

寫操作處理

當CPU需要往記憶體中寫入資料時,需要考慮寫命中和不命中兩種情況。當發生寫命中時,有兩種策略,分別是寫穿透和寫回。

寫命中

寫直達:也譯為寫通過或寫穿。寫操作總是同時更新cache和下一儲存器層次,以保持二者一致性。

寫穿透是指CPU在寫cache的同時寫記憶體(更低一級cache),這種策略的好處是記憶體的資料永遠都是新的,cache替換時,直接扔掉舊的資料就可以。

寫回策略是指CPU只寫開始不寫記憶體,寫回的好處是寫開始時比較省事,不用關注是否與記憶體一致,只有當替換演算法要驅逐這個更新的塊時,再寫回到記憶體裡。不過,這種策略會增加cache的複雜性

為了表明每個資料塊是否被修改過,每一個cache line需要增加一個額外的修改位。

當發寫不命中時 也有兩種策略 分別是寫分配和寫不分配。

寫不命中

寫分配就是把目標資料所在的塊從記憶體載入到cache中,然後再往cache中寫。

寫不分配就是繞開cache,直接把要寫的內容寫到記憶體裡

通常情況下,寫分配與寫回搭配使用。寫不分配與寫穿透搭配使用。

第五章的基本內容到此就暫時結束,還有一個重要概念虛擬記憶體,我會在之後與深入理解計算機系統這一本書一起做總結。