前言
五一小長假過完了,在刷了四天劇後又要開啟975養老生活了,收假第一天補一篇關於CPU快取記憶體的文章,錯誤之處歡迎大家指正。
正文
為什麼需要快取記憶體
我們都知道,程式是由一條條指令和資料組成的,CPU在執行時的工作也是周而復始的執行一條條用途各異的指令。
在一開始,程式載入到主存,在程式執行的過程中,將指令一條條的從主存中取出並執行,巨集觀上來說我們把主存看做是一個很大的一維位元組陣列,地址即可看作為陣列的下標。這樣的結構可以確保程式可以順利執行。可是存在一個問題,主存的讀取速度太慢,而CPU的運算速度卻非常快,CPU執行完一條指令後又等待十分漫長的時間才能等到下一條指令的到來,CPU的資源被嚴重浪費。
其實CPU一開始執行指令的速度也並不是特別快的,通過不斷的優化將其速度進行提升。過了一段時間,CPU執行指令的速度越來越快了,這時發現影響程式執行快慢的瓶頸不在於CPU了,而在於CPU執行指令與主存讀取指令的速度差距上,所以必須要想辦法讓讀取指令的速度快起來,這便出現了我們的快取記憶體。
快取記憶體的引入
引入快取記憶體後的儲存器層次結構圖如下所示,當然這只是一個簡化圖,利於我們理解整個儲存器的結構層次。其中L1,L2就是我們所說的快取記憶體, 這個結構類似於一個金字塔,越往上則越靠近CPU,讀取和寫入的速度越快,造價也越昂貴。
這裡我們假設L1與L2之間的快取塊大小為8KB,L2與主存之間的快取塊大小為64KB為例來闡述一下CPU讀取指令整個流程,定址器首先會到L1快取記憶體中去尋找指令,如果沒有CPU則等待定址器到L2快取記憶體中尋找指令,如果L2快取記憶體也沒有尋找到,那就從主存中尋找指令。尋找到指令後將命中的快取塊(64KB)的所有資料移動到L2,並將L2對應的快取塊(8KB)所有資料移動到L1。最終在L1中將對應的單條指令返回給CPU。
為何我們要提出快取塊這個概念?
我們的程式指令往往是連續的,程式訪問到某個資料時,那麼它和它周圍的資料會有很大可能在短時間內被再次訪問,這被稱之為區域性性原理。所以我們在訪問到某個資料時,索性將它和它周圍的資料也提到上層的快取記憶體中,下一次就可以直接從快取記憶體中命中資料。
如何判斷快取命中
上節中簡單描述了CPU取指令時資料在儲存器結構中的流動情況,我們說如果L1中沒有我們訪問的資料則會到L2中去尋找,我們稱這個為快取不命中,那麼如何判斷快取是否命中呢?我們將圖2的快取記憶體進行放大,觀摩一下它的結構
如果說你在疑惑t、b、s的含義,先不用管它,繼續往下看自然就知道了
如果說CPU現在要訪問某個地址為Adress的資料,定址器將地址進行如下劃分
-
根據組索引位定位到組
-
檢視快取塊有效位是否為1,如果是那麼比對標記位,如若標記位一致,則表示該快取命中。成功定位到快取塊
-
根據最後b位計算出偏移地址
這裡的t、b、s與圖3的資料一致。假設我們是64位機器,那麼Adress的長度應當等於64,即t+s+b=64
快取不命中的兩種
快取不命中的情況存在兩種,一種是快取為空,一種是快取衝突。前一種的發生的情況在計算機剛啟動時會比較常見,這是計算機的快取是空的,所以快取不命中,後一種發生的原因我們舉個很明顯的例子,假設現在有一個定址長度為16位的計算機,標記位t=3,索引位s=5,偏移位b=8。如果我們訪問這樣的兩個地址:
A=121 111 00021
B=131 111 00022
會發現A,B雖然地址不同,但是根據上述的步驟,它們會對映到同一個快取記憶體塊,但是由於標記位不一致導致快取不命中,這種情況稱之為快取衝突。
寫資料的情況
上面的大篇幅中我們都是讀資料的情況,那麼如果是寫資料快取記憶體改如何工作呢?由於寫的情況涉及到資料的修改,所以務必要比讀的情況更復雜一些, 假如我們現在要對地址A進行寫入,那我們存在兩種方案
1、摒棄快取記憶體,直接寫主存
2、寫入快取記憶體
如果我們選擇第一種情況,那麼直接寫入就ok,但是就回到了一開始的問題了,寫入的速度太慢,cpu要漫長的等待。那麼我們當然是採用第二種情況。先將資料讀入快取記憶體,再對快取進行寫入。那麼這種情況我們需要注意一個問題:不同層級的快取同步問題,也就是說當這個快取塊發生快取衝突,在資料覆蓋時需要將這個快取塊重新整理到它的下一級快取記憶體中。
小結
通過上面的講述我想快取記憶體工作的原理應該在腦海中已經有一個流程圖了。那麼知道了快取的工作原理在實際工作中有什麼用呢?這個就回到了我們一開始說的區域性性原理了,由於計算機總是將一段連續的地址進行快取(快取塊),所以如果我們編寫的程式碼符合區域性性原理,那麼執行效率將會有很大的提升,這為我們的程式效能優化提供了一個方面的指引。
舉個例子
如果遍歷一個長度相同陣列和一個連結串列,由於陣列在物理儲存上是連續的,遍歷陣列時效率會更快