從緩存角度來提高程序性能(二):高速緩存Cache

目錄

1 高速緩存Cache機制

1.1 通用的高速緩存存儲器結構

1.2 直接映射高速緩存

1.3 組相聯高速緩存

1.4 全相聯高速緩存

2 編寫高速緩存友好的代碼


1 高速緩存Cache機制

1.1 通用的高速緩存存儲器結構

      通用的高速緩存存儲器的結構如圖所示:

       由圖可見,高速緩存存儲器實際上是由多個緩存行組成的,共分爲S個組,每個組中有E個緩衝行,對於每個緩衝行來說,其主要由三部分組成:有效位(1bit)、標記位(t bits)以及1個緩存塊組成,這個緩存塊由B個字節組成。因此,高速緩存存儲器的大小爲C=S×E×B。其中有效位用來表明當前緩衝行的緩存塊數據是否有意義,有意義即爲1;標記位用於判別是否緩存命中的關鍵信息。

       而另一方面,內存中每一個單元都擁有其地址,而每個地址則對應高速緩存存儲器中的一個塊。假設地址爲m位的,那麼這m位的地址構成如圖所示:

                     

       如圖所示,m位的地址中,低b位表示塊偏移,這b位共對應有2^b種偏移情況,即是0~B-1字節;中間的s位爲組索引,用來確定該地址映射到哪一個緩衝組裏的,s位共對應2^s種情況,相應的就是組0~組S-1;然後就是高t位標記位了,只有當高速緩存存儲器中某一緩存行的有效位爲1,標記位與該地址中的t個標記位完全相同時,才能認爲高速緩存命中,然後根據b位偏移來選擇起始字節。

        當CPU執行一條讀內存字w的命令時,它會向L1高速緩存請求這個字,來看w是否在L1緩存命中。這時就會先根據w的地址的中間s位來確定w是在哪一個緩存組中,然後再根據t位標記位來確定w是在哪一個緩存行中,最後根據b位來確定w的偏移量從而確定w的起始字節。因此,整個過程分爲三步:①組選擇;②行匹配;③字抽取

1.2 直接映射高速緩存

        所謂直接映射高速緩存,就是每個緩存組中只有1行,即E=1。如圖所示。

      

       由於每個緩存組中只有一行緩存行,因此根據地址的s位即可確定緩存行的位置,然後再通過低b位確定起始字節。現在假設CPU每次讀取一個字節的字,且高速緩存的描述爲(S,E,B,m)=(4,1,2,4),根據S=4,因此緩存組一共有4組,那麼就需要s=2位來選擇是哪一個組;根據B=2,那麼緩存塊有2個字節,因此就需要b=1位來選擇偏移字節。它的初始結構如下:

       此時如果CPU需要讀地址爲0處的字。CPU先像Cache發出請求。根據地址0的4位二進制表示爲0000,從最低開始取b(b=1)位爲0,因此塊偏移字節爲0;然後往左數s(s=2)位“00”,因此選擇組0。這樣也就確定了緩存行爲第0行,再往左數剩下的t位標記位爲“0”,爲標記位。然後再看第0行緩存行,由於第0行緩存行有效位爲0,發生冷不命中,因此Cache需要從內存中加載這個,而這個塊中包含了兩個字節的數據,對應地址爲0000~0001(將b位所有情況列出),因此,Cache將地址爲0和地址爲1的兩個字節加載到組0的緩存塊中;標記位與地址0的標記位相同,置爲0。然後Cache從緩存行的緩存塊中取得偏移字節爲0(由最低1位決定)的一個字節(由CPU一次讀取字節數決定)的值,返回給CPU,此時Cache結構如下所示。

        此時如果CPU需要讀地址爲1處的字。同樣的,CPU先像Cache發出請求,根據地址1的二進制表示爲0001,因此從最低取b(b=1)位爲1,表示偏移字節爲1;然後往左數s(s=2)位“00”,選擇組0。再往左數剩下的t位標記位爲“0”,標記位爲0。然後再看第0行緩存行,此時第0行的有效位爲1,並且標記位與地址1的標記位相同,因此緩存命中,直接從該行緩存塊中偏移1個字節取得m[1]返回給CPU,Cache結構不變。

        此時如果CPU需要讀地址13處的字。地址13的二進制表示爲1101,從最低開始取b(b=1)位爲1,表示偏移字節爲1;再接着往左數s(s=2)位“10”,選擇組2。再往左數剩下的t位標記位爲“1”,標記位爲1.再來看組2中的緩存行,發現有效位爲0,發生冷不命中,因此Cache從內存中加載該緩存行對應的塊,塊對應的地址爲1100~1101,即將地址12與地址13的字加載到塊中,標記位置爲1。然後Cache從緩存行的緩存塊中取得偏移字節爲1(由最低1位決定)的一個字節(由CPU一次讀取字節數決定)的值m[13],返回給CPU,此時Cache結構如圖所示。

       此時如果CPU需要讀地址8處的字。地址8的二進制表示爲1000,從最低開始取b(b=1)位爲0,表示偏移字節爲0;再接着往左數s(s=2)位“00”,選擇組0;再往左數剩下的t位爲“1”,標記位爲1。此時再看組0,有效位爲1,但是標記位爲0不爲1,因此發生衝突不命中,因此Cache從內存中加載該緩存行對應的塊,塊對應的地址爲1000~1001,即將地址8與地址9處的字加載到緩存塊中,並將標記位改爲1,然後再看偏移字節爲0的一個字節即是m[8]返回給CPU,此時Cache的結構如圖所示。

1.3 組相聯高速緩存

       組相聯高速緩存與直接映射高速緩存相比,其主要的差別在於每一個緩存組中的緩存行可以爲多個,即1<E<C/B,比如常見的二路緩存就是每個緩存組中有兩個緩存行,即E=2。如圖所示。

       給定一個地址,Cache根據這個地址依然可以得出標記位、組索引和塊偏移字節,根據組索引可以找到準確的緩存組,那麼怎麼確定是哪一個緩存行呢?其實很簡單,直接將標記位與該緩存塊中各個緩存行的標記位做對比,如果有一個緩存行的標記位與地址的標記位相同,並且有效位爲1,那麼就說明緩存命中,然後根據塊偏移字節到這個緩存行中找到數據返回。

       如果緩存不命中,那麼就需要去內存中取得相應的塊,然後執行某種替換策略(LFU、LRU、隨機選擇等)來選取替換行。

       其餘的操作參照直接映射高速緩存。

1.4 全相聯高速緩存

        全相聯是指整個高速緩存只有一個組,即S=1,所有緩存行都包含在這個組中,即E=C/B。如圖所示。

       對於全相聯高速緩存,在使用地址進行映射時組索引位直接默認爲0,m個地址位只由b位的塊偏移位與t=m-b位的標記位組成,依然是以有效位爲1,標記位相同爲標準來判斷是否緩衝命中。

2 編寫高速緩存友好的代碼

       簡單來說,緩存命中率越高,自然程序效率越高。那什麼是高速緩存友好的代碼呢?我們先來看看以下幾個例子。

struct algae_position{
    int x;
    int y;    
};

struct algae_position grid[16][16];
int total_x = 0,total_y = 0;
int i,j;

Eg1:

        對於這個例子,我們先來看結構體algae_position,它的大小爲8個字節,而Cache的大小爲1024B,並且緩存塊的大小爲16字節,即S=64,E=1,B=16,那麼一個緩存塊中可以存放兩個結構體變量,總共有64個緩存行(0~63)。因此最多隻能放下64×2=128個結構體變量,即grid數組的一半。

        先來看第一個雙層循環,訪問g[0][0]~g[7][15]之間,每兩個元素之間必定有一個元素爲緩存冷不命中,而另一個元素則緩存命中。舉個例子,訪問g[0][0]時會緩存冷不命中,然後此時將g[0][0]和g[0][1]加載到Cache中,然後下一次循環訪問g[0][1]時就緩存命中了。而在訪問g[8][0]~g[15][15]之間時,每兩個元素之間必定有一個元素緩存衝突不命中,而另一個元素則緩存命中。舉個例子,訪問g[8][0]時,必定是對應組0中的緩存行的,此時就會緩存衝突不命中,然後加載g[8][0]和g[8][1]到塊中,然後下一次循環訪問g[8][1]時就緩存命中了。因此第一個雙層循環的命中率爲50%。

         再來看第二個雙層循環,它實際上和第一個雙層循環是相類似的,不同的只是每次訪問的地址的偏移字節不同而已,但是並不影響緩存是否命中,因此第二個雙層循環的命中率依然爲50%。因此整個程序的不命中率就是50%。

         Cache及變量定義保持不變,再來看第二個例子:

Eg2:

       在這段程序中,是按列優先訪問的。在第一輪循環中(i=0),先訪問g[0][0].x,緩存冷不命中,此時加載g[0][0]和g[0][1]到塊0中,然後訪問g[0][0].y肯定是命中的,接着第二次循環訪問g[1][0],緩存冷不命中,此時加載g[1][0]和g[1][1]到塊8中...訪問g[7][0].x,緩存冷不命中,此時加載g[7][0]和g[7][1]到塊56中,然後訪問g[7][0].y肯定是命中的,接下來加載g[8][0].x~g[15][0].x時,就會和塊0、塊8、...、塊56緩存衝突不命中,但是跟着訪問的.y是命中的。因此,在第一輪循環中,所有.x訪問都是緩存不命中的,而.y的訪問是命中的;

        第二輪循環(i=1)中各個.x的訪問也是在塊0、塊8、..塊56緩衝衝突不命中,.y訪問命中;到了第3輪循環(i=2)時,此時的g[0][2].x、g[1][2].x...g[7][2].x則會在塊1、塊9...塊57冷不命中。由此可見,每一次循環中,.x的訪問要麼是冷不命中,要麼就是衝突不命中的,但是.y是肯定命中的,因此不命中率爲50%。

       如果高速緩存有兩倍大,即緩存行數加倍,S=128,此時高速緩存可以裝下整個數組,不會發生衝突不命中,因此只會冷不命中,因此不命中率只有25%。

       Cache及變量定義不變,繼續看第三個例子:

Eg3:

        這段循環是按行優先訪問的。這種形式其實是最有效率的,因爲g[0][0].x訪問冷不命中後,就會加載g[0][0]~g[0][1],那麼後面接着的三次訪問都是緩存命中的,然後g[0][2]也是一樣,直到g[7][15]訪問後Cache被填滿,此時再訪問g[8][0]就是緩存衝突不命中了,但是接着的三次訪問也都是緩存命中,因此不命中率爲25%。

       在這種情況下,即使將Cache的容量加倍,改變的只是將所有緩存衝突不命中變成了緩存冷不命中,但是不命中率依然是25%,即使擴大3倍、4倍、....n倍,不命中率還是25%不變。

       綜合以上三個例子可知,編寫高速緩存友好的程序,即是使Cache緩存命中率高,在第一個例子中,變量的訪問步長爲“2”(連續的x訪問相當於跳過了二者之間的y),在第二個例子中,變量的訪問步長在同一輪循環中爲“1”,在相鄰輪循環中爲“32”(g[0][0].x~g[1][0].x);而在第三個例子中,變量的訪問步長都是"1"。可見,訪問步長爲“1”的情況下,Cache的命中率是相對較高的,在編寫程序,尤其是循環時一定要注意這一點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章