1. 程式人生 > >程式效能優化探討(3)——儲存器層次結構與快取記憶體

程式效能優化探討(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:

效位 標記位 快取記憶體塊 快取記憶體塊
        從以上三組快取策略,我們能看出:                 1、無論怎麼劃分地址,最終都可以達到這樣一個目的:任意地址鎖代表的值都可能對應到快取的某個位置,也就是都可以被快取處理。                 2、不同的快取策略,一定使用於不同的應用環境。比如全相聯快取記憶體,只有行判斷,邏輯處理非常簡單,但如果行數過多,耗費時間也越多,因此全相聯快取記憶體只適合做小的快取記憶體,例如翻譯備用緩衝器(TLB),該知識點在虛擬儲存章節將會涉及到。                 3、事實上,程式設計師很難利用快取記憶體替換策略來提高程式碼的效率,但有些時候是存在例外的,比如直接對映快取記憶體,還有後面會出現一些例子。本節先就介紹到這