程式效能優化探討(3)——儲存器層次結構與快取記憶體
連外行都大概清楚,目前硬體速度的瓶頸在硬碟而不是CPU。為了有效的克服不同器件之間的速度差,從CPU到硬碟引入了多級快取機制。由於快取影響程式讀取速度,因此是實現優化時必須考慮的內容。
一、儲存器層次結構
快取的思想可能存在於任何有速度差的儲存之間,現在看看經典的儲存器層次結構圖:
從L0到L3,都是針對CPU處理本身設計的快取結構,L4就是我們常說的記憶體,以下類似。分這麼多層次,說明在CPU指令執行過程中,資料被人為劃分成多個層次結構。最頂層的L0就是暫存器,用於CPU指令執行時儲存臨時值;L1~L3是SRAM,即靜態RAM,速度快但造價較DRAM要高得多,分別儲存跟CPU執行指令相關、由小到大的三個抽象層資訊。L4及之後的儲存器,為更大的抽象層,關於其中的細節,將作為虛擬儲存器內容,在隨後的章節中討論。為了方便起見,我們只針對L1快取記憶體進行原理級論述。
二、快取記憶體通用結構
在討論之前,我們再來確認一個老生常談的問題,我常說:“32位機最大可訪問4GB的地址空間”,啥意思?首先,32位是CPU的一個引數,指CPU匯流排的資料寬度。32位CPU顧名思義就是擁有32bit的匯流排資料寬度,一次操作最大可執行32位的計算。回憶下之前我們講過的32位乘法,由於乘法的結果有可能大於32位,因此會用兩個32位變數進行儲存。
那麼,當CPU要讀某個地址時,首先要先計算出該地址的值。假設地址是從00000000開始的,FFFFFFFF結束,在此範圍內的地址空間有多大呢?很明顯,00000000~FFFFFFFF一共有2^32= 4294967296種取值,也就是我們常說的4G。可惜小編我曾經腦子進水,問出這樣的問題:地址不是按位元組標識的麼?byte和bit不是按8位換算的麼?你這4G換算成位元組,不是隻有500MB大小麼?那00000000~FFFFFFFF何來4GB地址空間???餓,別笑話我,拿這個問題去問我的學霸朋友,居然把他也蒙了好一會兒!當然,要解釋這個也很容易,正因為地址是按位元組標識的,因此當我們訪問儲存器時,根本不需要對儲存器的逐個位進行讀取,找到位元組級,再用移位就能儲存空間的每個位了。比如,假設有一款特殊的CPU,它是2位的CPU,毫無疑問,他每次只可能處理00、01、10、11這四種值,於是這四個值可以代表四個地址,而每個地址代表8位儲存空間,也就是32位地址空間……想明白沒?CPU裡處理地址值時,每個數值都代表一個位元組(byte)大小的儲存空間,而不是一位(bit)大小的儲存空間。因此,4Gbit大小的數值空間,就能標識4GB大小的地址空間,如果說計算地址相當於數數,那麼我們是按位元組來數儲存器空間,而不是按位來數……你別說,腦子卡殼時,要是沒事先想清楚,還真有可能被人問到(⊙o⊙)。ps,這裡的“卡”應該讀qiǎ,哈哈!
所謂地址空間,其實是由地址來解釋空間,地址是數值,而每一個數值標識一個8位的儲存空間,這應該算是地址空間不太嚴格的定義了!
哈勒,好了,假設地址有m位,那麼就有M=2^m個位元組地址空間,M個不同的地址。結論先放在這,我們來淺析下什麼是地址多。來看兩則定義:
1.記憶體中每個用於資料存取的基本單位,都被賦予一個唯一的序號,稱為地址。
2.標識暫存器、儲存單元和儲存裝置的編號或名稱。
定義1太過狹隘,把地址的概念限制於記憶體之中,顯然不是我們想要的,還是定義2比較靠譜,用來標識暫存器和儲存的編號或名稱,它提供了在暫存器、快取、記憶體、硬碟等器件中的資料檢索依據。也就是說,M=2^m地址空間對這些器件的作用是類似的,只是具體策略有所不同。
事實上,現代作業系統所管轄的記憶體時,是不會讓程式設計師訪問到記憶體每個資料塊的實際硬體地址的,而是採用一種稱為“虛擬儲存器”的方法,將記憶體中連續或不連續的儲存塊,定義成連續的虛擬地址供程式設計師訪問,類似C語言中取地址&符號的出來的地址值,都是這種虛擬地址值,這樣有利於簡化操作,更有便於作業系統和記憶體硬體採用更合理的方案分配儲存空間。記憶體是快取硬碟資料的重要工具,這些屬於層次結構中L4和L5的規則原理,將在後面虛擬儲存器章節進行詳細討論。
CPU如何通過地址管理暫存器,我們也不關心,我們要關心的是嵌入cpu內部的快取記憶體L1以及下層的L2、L3的通用快取的地址結構。試想,快取記憶體總是比記憶體(教材上經常成為主存或者儲存器)小得多得多,L1一般就幾十上百KB而已,離4GB遠著呢,那麼CPU的這32位地址怎麼用呢?很明顯,快取記憶體既然小,那一定是被地址空間所共享,你4GB中任何一個位元組都可能被快取在L1~L3中,很顯然,我們得有辦法進行區分。要對快取記憶體進行有效的管理,就得給高快取分組,組裡面還可以有行,行裡面可以有不同的位元組串,針對這個想法,就能推出教材裡的通用結構圖:
品味這個結構圖時,憑直覺容易犯的錯誤,就是把下面的地址和上面的行混為一談。因此先明確下概念:
1.上面整個部分就是快取記憶體
2.這個快取記憶體被劃分成S個組
3.每個組有E行資料
4.每行中都有一個快取記憶體塊,
5.每個快取塊的大小是B個位元組
6.綜上所述,整個快取記憶體的儲存大小C = S*E*B,你滴明白?
我始終覺得,從大到小的計算方式更符合中國人的思維習慣,對C的一個簡單乘法計算,寫成這樣的順序更便於理解。
好了,既然清楚了快取記憶體的通用結構,那麼CPU是依據什麼來訪問快取中各位元組的呢?首先你得找到組吧,然後找到行吧,最後找到塊吧,最後再通過偏移找到具體的位元組吧?如果你能看懂到這,那(S,E,B,m)就灰常好理解了。
既然CPU在訪問快取記憶體時,需要找這麼多資訊,那麼在定義快取記憶體的地址概念時,就必須得有響應的欄位來標識。剛才我們有了M=2^m,說明地址有m位,標識了M位元組的地址空間。現在要標識組,要在S個組裡面找出唯一的一個組資訊,需要佔用地址中的多少位呢?如果S=2^s,那我們就需要在m位地址中劃分出s位來標識組資訊;好,接下來要找行,既然每組有E行,若E=2^t,那有要在m位中劃分t位來標識行資訊;最後是行中的快取塊位元組偏移,既然有B位元組,而B=2^b位元組,也就說還要從m中分配b位來標識塊位元組偏移……
總結一下:
完整的地址m位被弄得四分五裂,而且它還得保證剛好夠用,很顯然我們就能得到m = s+t+b這樣的等式。這樣的分配結果會產生一個問題:既然地址已經按照C=S*E*B和快取的各個位置都一一對應上了,既然m位地址可以標識2^m位元組的空間,那麼我的快取本身大小也是C=S*E*B= 2^s * 2^t * 2^b=2^(s+t+b)=2^m位元組……快取和被快取物件一樣大,豈不是多此一舉麼?
三、直接對映快取記憶體
於是我們先對t做做文章,讓他不表述行資訊,假如每個組只有一行如何?這就是所謂的直接對映快取記憶體!這種快取的特點是,每組只有一行,那每組也就只有一個快取塊。這樣一來,t就不在用於區分組中的行資訊了。它用來幹什麼呢?答案是,用來共享。在直接對映快取記憶體中,標記位t就是不同地址共享同一行快取空間的依據。
上圖解釋了地址位(下)於直接對映快取記憶體(上)的對應關係。
1.根據地址中的i已經找到了快取裡的i組
2.讀取了快取中第一位的有效位是1,說明該資料有效,
3. 判斷地址中標記位t的值是否與i組裡這唯一的一行紀錄中的標記位相等,若相等說明資料有效,快取命中,
若不相等呢?說明該組該行正快取其他標記位標識的資訊,也就是對當前地址不命中。
4.若快取命中,則讀取地址中塊偏移資訊,找到具體的位元組。既然塊偏移的取值範圍是000~111,也就是0~7,說明可以訪問8位元組塊中任意一個位元組。
我們發現,正是因為有標記位的存在,同樣大小的直接對映快取,在服務於m位地址時,可以減少塊偏移的位數,簡化偏移操作,增加組的數量。同時,因為標記位的存在,使得m地址所標識的資料被輪流載入快取,由標記位判斷快取是否命中。某個確定地址的資料可以暫時不在快取中儲存。
這裡詳細闡述教材中的例子:
假設有一個直接對映快取記憶體:(S,E,B,m)=(4,1,2,4)。根據上面的基礎可以得知,這個快取有4個組,每組一行(直接對映的特徵),每個塊有兩個位元組,地址有4位。可以得出如下資訊:
組索引位:s=2;塊偏移位數b=1;地址位數m=4,實體地址最大數量M=2^4=16位元組,標記位t = m - s - b = 1。
地址的劃分如下(組索引用s代替原圖中的i,方便討論):
標記位t | 組索引s | 組索引s | 塊偏移b |
直接對映快取記憶體結構:
組0:
有效位 | 標記位 | 快取記憶體塊 | 快取記憶體塊 |
組1:
有效位 | 標記位 | 快取記憶體塊 | 快取記憶體塊 |
組2:
有效位 | 標記位 | 快取記憶體塊 | 快取記憶體塊 |
組3:
有效位 | 標記位 | 快取記憶體塊 | 快取記憶體塊 |
如上所示,4個組,組索引取值00~11;每個組一行,高快取塊兩個位元組,標記位一個位元組;地址取值0~15(十進位制),
地址位 | 地址位 | 地址位 | ||
地址 | 標記位 (t=1) | 索引位 (s=2) | 偏移位 (b=1) | 塊號 |
0 | 0 | 00 | 0 | 0 |
1 | 0 | 00 | 1 | 0 |
2 | 0 | 01 | 0 | 1 |
3 | 0 | 01 | 1 | 1 |
4 | 0 | 10 | 0 | 2 |
5 | 0 | 10 | 1 | 2 |
6 | 0 | 11 | 0 | 3 |
7 | 0 | 11 | 1 | 3 |
8 | 1 | 00 | 0 | 4 |
9 | 1 | 00 | 1 | 4 |
10 | 1 | 01 | 0 | 5 |
11 | 1 | 01 | 1 | 5 |
12 | 1 | 10 | 0 | 6 |
13 | 1 | 10 | 1 | 6 |
14 | 1 | 11 | 0 | 7 |
15 | 1 | 11 | 1 | 7 |
可以說,能看懂上圖對於快取記憶體(S,E,B,m)=(4,1,2,4)的分配方案,就完全的理解直接對映快取記憶體了。
1.先從索引位和偏移位看起,組索引00~11標誌四個組,每個組有一個快取塊(每個組只有一行的嘛),每個快取塊有兩個位元組,因此偏移位是0~1,從上圖看出,每個索引位對應兩個偏移位,其實就是遍歷每組裡快取塊的各位元組,他們共有8種組合。
2.標記位t取值0~1,由於每組只有一行,因此標記位用於區分同組、同偏移位的可能儲存的不同位元組。也就說,每組中的快取塊的每個位元組,都可能對應兩個不同的取值,用標記位區分開來。因此我們看到,標記位0~1,將索引位和偏移位的組合數增大了一倍。
3.由於標記位對快取塊的擴充套件,本來只有C=S*E*B=4*1*2=8位元組大小的快取,增大一倍後就能處理16位元組資料了,你看看,剛好就能對應地址0~15這16個取值,每個取值代表一個位元組,就剛好是16位元組(聽起來很廢話,主要為了儘可能讓大家讀懂)
4.由於每個快取塊是兩個位元組大小,因此16個位元組就需要8個快取塊,因此就有了最後的“塊號”,剛好0~7,共8個塊。本來快取記憶體只有4個塊,現在通過標記位的引入,邏輯上便擴充套件成8個塊,只是在具體實現時,0、4塊共享同一個空間;1、5塊共享同一個空間;2、6塊共享同一個空間;3、7塊共享同一個空間。
綜上所述,這款直接對映快取記憶體實際具有4個快取塊,通過標記位在邏輯上擴充套件成8個邏輯塊,每個邏輯塊都唯一對應兩個計算機裡的位元組。
四、組相聯快取記憶體和全相聯快取記憶體
組相聯英文裡為set associative,比如我的CPU是8-way set associative,8路組相聯快取,說明每組中有8行。組相聯快取記憶體中,每組的行數E的範圍應該是1<E<C/B。每組多行的快取策略如何實現呢?想想直接對映快取記憶體,雖然每組只有一行,但是該行的索引位t並不唯一,如上例,索引位00有可能對應0塊和塊4,說明塊0和塊4共享組0,那如果組0剛好能容納兩行資料,則塊0和塊4就可以同時存在組0中,訪問時也很好區分,就用直接對映快取裡的標記位t,通過t的0、1不同取值來識別。
組0:
行0:
行1:
|
組1:
行0:
行1:
|
組2:
行0:
行1:
|
從這裡我們看出,直接對映快取記憶體和組相聯快取記憶體由於都有標記位t區分行,於是他們的地址劃分都類似於:
標記t位 | 組索引s位 | 塊偏移b位 |
最後來說明一個特例:全相聯快取記憶體:full associative cache,快取的所有行都在一個組裡,既然只有一個組,於是S=1,E=C/B。想想,S=1,那麼s就該等於0,於是地址劃分就類似於:
標記t位 | 塊偏移b位 |
由於沒有了組索引,除去塊偏移位後,所有的位都用於記錄標記位t,於是t就負責在這唯一一個組裡標識行。
行0:
行1:
行2:
行3:
行4:
|