計算機系統結構總結_Cache Optimization


Textbook:

《計算機組成與設計——硬件/軟件接口》    HI

《計算機體系結構——量化研究方法》         QR


Ch4. Cache Optimization

本章要討論的問題就是 How to Improve Cache Performance? 

前面講過 Average memory access time = HitTime + (MissRate * MissPenalty)

那么我們的方向就是Reduce MissRate / HitTime / MissPenalty

 

1. 6 Basic Cache Optimization(PPT P3)

 Reducing hit time

1. Giving Reads Priority over Writes
E.g., Read complete before earlier writes in write buffer  ??

 

2. Avoiding Address Translation during Cache Indexing

Cache中使用虛擬地址,這樣就可以同時Access TLB和Cache / Access Cache firstly

 

Reducing Miss Penalty

3. Multilevel Caches

AMAT = Hit TimeL1 + Miss RateL1 x Miss PenaltyL1
Miss PenaltyL1 = Hit TimeL2 + Miss RateL2 x Miss PenaltyL2

原來Miss PenaltyL1要訪問內存,很慢。現在多了L2

 

Reducing Miss Rate

4. Larger Block size (Compulsory misses)

...

5. Larger Cache size (Capacity misses)

...

6. Higher Associativity (Conflict misses)

...

2. 11 Advanced Cache Optimizations (PPT P12)

• Reducing hit time

1. Small and simple caches(QR  P59)

如果僅考慮Cache Hit Time,那么結構越簡單、容量越小、組相連路數越少的緩存肯定是越快的。

所以出於速度考慮,CPU的L1緩存都是很小的。比如從Pentium MMX到Pentium 4,L1緩存的容量都沒有增長。

不過太少了肯定也是不行的。。。所以這也是一個Trade off 

 

2. Way prediction

直接相連的Hit Time是很快的,但conflict miss多。組相連可以減少conflict miss,但結構復雜功耗也高一些,hit time也多一點。那么有什么方法能兩者兼得呢?

因為組相連緩存中,每一個組里面的N個路(block)是全相連的。也就是相當於讀的時候,每次映射好一個set之后,要遍歷一遍N個block,當N越大的時候費的時間就越多。有一種黑科技方法叫做路預測(way prediction),它的思想就是在緩存的每個塊中添加預測位,來預測在下一次緩存訪問時,要訪問該組里的哪個塊。當下一次訪問時,如果預測准了就節省了遍歷的時間(相當於直接相連的速度了);如果不准就再遍歷唄。。。

好在目前這個accuracy還是很高的,大概80+%了。

不過有個缺點就是Hit Time不再是確定的幾個cycle了(因為沒命中的時候要花的cycle多嘛),不便於后面進行優化(參考CPU pipeline)。 

 

3. Trace caches

這個只針對instruction cache。在讀取指令緩存時,要不斷jump來讀取不同的指令(也就是比較random的Access pattern),這樣就不如sequential的access快了。

在牙膏廠的Pentium 4中,使用了trace caches的黑科技。它會嘗試找出相鄰被訪問的指令(比如A jump to B),然后把這些block放到鄰近的位置,這樣就可以access instruction cache sequentially。

但因為現在code reuse rate不高了(程序太多了,很多程序可能一段時間內只執行一次),再加上這個黑科技implement比較復雜,后來就放棄了。

 

Increasing cache bandwidth

4. Pipelined caches

在本科的計組課上我們學過pipeline的思想。在cache訪問中也可以使用pipeline技術。

但pipeline是有可能提高overall access latency的(比如中間有流水線氣泡),而latency有時候比bandwidth更重要。所以很多high-level cache是不用pipeline的

 

5. cache with Multiple Banks

對於Lower Level Cache(比如L2),它的read latency還是有點大的。假設我們有很多的cache access需要訪問不同的數據,能不能讓它們並行的access呢?

可以把L2 Cache分成多個Bank(也就是多個小分區),把數據放在不同Bank上。這樣就可以並行訪問這幾個Bank了。

那么如何為數據選一個合適的Bank來存呢?一個簡單的思路就是sequential interleaving:Spread block addresses sequentially across banks. E,g, if there 4 banks, Bank 0 has all blocks whose address modulo 4 is 0; bank 1 has all blocks whose address modulo 4 is 1; ...... 因為數據有locality嘛,把相鄰的塊存到不同bank,就可以盡量並行的訪問locality的塊了

 

6. Nonblocking caches

假設要執行下面一段程序:

1 Reg1:=LoadMem(A);
2 Reg2:=LoadMem(B);
3 Reg3:=Reg1 + Reg2;

當執行第一行時,cpu發現地址A不在cache中,就需要去內存讀。但讀內存的時間是很長的,此時CPU也不會閑着,就去執行了第二行。然后發現B也不在cache中。那么此時cache會怎么做呢?

  • (a). cache阻塞,等着先把A讀進來,然后再去讀B。這種叫做Blocking Cache
  • (b). cache同時去內存讀B,最終B和A一起進入Cache。這種叫做Non-Blocking Cache

可以看出Non-Blocking Cache應該是比較高效的一種方法。在這種情況下,兩條語句的總執行時間就只有一個miss penalty了:

 (圖中只是大概的描述,不是精確的時間計算。。。如果用了上面介紹的multiple bank cache,那么hit時間可能也只需要一次了,很棒棒吧!)

 

Reducing Miss Penalty

7. Early Restart and Critical word first

相對一個Word來說,cache block size一般是比較大的。有時候cpu可能只需要一個block中的某一個word,那么如果cpu還要等整個block傳輸完才能讀這個word就有點慢了。因此我們就有了兩種加速的策略:

  1. Critical Word First:首先從存儲器中讀想要的word,在它到達cache后就立即發給CPU。然后在載入其他目前不急需的word的同時,CPU就可以繼續運行了
  2. Early Restart:或者就按正常順序載入一整個block。當所需的word到達cache后就立即發給CPU。然后在載入其他目前不急需的word的同時,CPU就可以繼續運行了

大概就是這個意思:

根據locality的原理,一般來說CPU接下來要訪問的也就是這個block中的剩余內容。所以沒毛病!

 

8. Merging write buffers

 ?????(QR P65)

 

Reducing Miss Rate

9. Compiler optimizations

這是最喜聞樂見的一種方法了hhhh

這里的reducing miss rate又可以分為Instruction miss和data miss兩類:

Instruction Miss:

• Reorder procedures in memory so as to reduce conflict misses
• Profiling to look at conflicts(using tools they developed) (之前面試還被問到過Linux profiling了......)

 

Data Miss:這個是比較重要的一種方式了。網上很多大神所說的黑科技優化C代碼的原理就是這個。

  • 1. Merging Arrays: improve spatial locality by single array of compound elements vs. 2 arrays

 假設有下面兩個定義(他們的功能都是一樣的,只是寫法不同):

/* Before: 2 sequential arrays */
int val[SIZE];
int key[SIZE];


/* After: 1 array of stuctures */
struct merge {
    int key;
    int val;
};
struct merge merged_array[SIZE];

我們可以比較一下對於這兩種定義方式,它們在內存中的組織方式:

 好的現在我們要對index k,分別訪問key[k]和val[k]。

/* Before: Miss Rate = 100% */
int k=rand(k);
int _key=key[k];
int _val=val[k];


/* After: Miss Rate = 50% */
int k=rand(k);
int _key=dat[k].key;
int _val=dat[k].val;

可以看出第二種方式充分利用了spatial locality。對於同一個index k,讀取key_k的同時,val_k也被讀進cache啦,這樣就節省了一次訪問內存的時間。

上面這個還可以引申出另一個話題,叫做結構體對齊

 

  • 2. Loop Interchange: change nesting of loops to access data in order stored in memory

還是下面兩種程序,它們只是循環次序改變了:

int x[][];   //very large
//Assume a cacheline could contain 2 integers.

/*
Before */ for (j = 0; j < 100; j = j+1) for (i = 0; i < 5000; i = i+1) x[i][j] = 2 * x[i][j];
/* After */ for (i = 0; i < 5000; i = i+1) for (j = 0; j < 100; j = j+1) x[i][j] = 2 * x[i][j];

我們知道在C語言中,二維數組在內存中的存儲方式是Row Major Order的,也就是這樣:

那么對於第一種寫法,訪問順序是x[0][0], x[1][0], x[2][0], ......。Miss Rate達到了100%

第二種寫法,訪問順序是x[0][0], x[0][1], x[0][2], x[0][3], ......。讀x[0][0]的時候可以把x[0][1]也讀進來,讀x[0][2]的時候可以把x[0][3]也讀進來,以此類推。這樣Miss Rate就只有50%啦

 

  • 3. Loop Fusion: Combine 2 independent loops that have same looping and some variables overlap

來看個例子:

 1 /* Before */
 2 for (i = 0; i < N; i = i+1)
 3     for (j = 0; j < N; j = j+1)
 4         a[i][j] = 1/b[i][j] * c[i][j];
 5 for (i = 0; i < N; i = i+1)
 6     for (j = 0; j < N; j = j+1)
 7         d[i][j] = a[i][j] + c[i][j];
 8 
 9 
10 /* After */
11 for (i = 0; i < N; i = i+1)
12     for (j = 0; j < N; j = j+1){
13         a[i][j] = 1/b[i][j] * c[i][j];
14         d[i][j] = a[i][j] + c[i][j];
15     }

在第二種寫法中,line 13已經把a[i][j]和c[i][j]讀進cache了,line14就可以接着用了。加起來比第一種要省很多cache miss。

不過第一種寫法本身時間復雜度也高啊。。。這樣寫代碼會被人打的。。。

 

emmm上面這個例子比較弱智。。。下面再來看一個經典的Matrix Multiplication的例子:

假設我們要計算一個大矩陣的乘法,然后cache block是4個integer的大小。

矩陣乘法是三重循環,O(N^3)的。我們來分析不同的循環順序下,最內層循環的cache miss情況(因為cache很小,只會在最內層循環起作用,外面的肯定都要有miss的):

 

  • 4. Blocking: Improve temporal locality by accessing “blocks” of data repeatedly vs. going down whole columns or rows

從上面的例子中可以看到,當每次access的是同一column中的不同row(a[1][3], a[2][3], a[3][3], a[4][3], ......),而不是同一row的不同colum時,miss rate是很可怕的。那么怎么避免這一現象呢?

一種思路是我們把整個大矩陣分解成若干個小矩陣(以所需的數據能被cache全部裝下為標准),然后每次都把這個小塊內要計算的任務全部完成,這樣就不用access whole column了。

/* Before */
for (i = 0; i < N; i = i+1)
    for (j = 0; j < N; j = j+1){
        r = 0;
        for (k = 0; k < N; k = k+1)
            r = r + y[i][k]*z[k][j];
        x[i][j] = r;
}


/* After */
for (jj = 0; jj < N; jj = jj+B)
    for (kk = 0; kk < N; kk = kk+B)
        for (i = 0; i < N; i = i+1)
            for (j = jj; j < min(jj+B-1,N); j = j+1){
                r = 0;
                for (k = kk; k < min(kk+B-1,N); k = k+1){
                    r = r + y[i][k]*z[k][j];
                }
                x[i][j] = x[i][j] + r;
            }

其中B叫做Blocking Factor。(QR P67)

• Capacity Misses from 2N3 + N2 to 2N3/B +N2
• Conflict Misses Too?(沒講)

Blocking Transformation

 

其實前面提到的這些access pattern現在已經可以被compiler自動優化了,所以也算是上古時代的黑科技了......

 

 

Reducing miss penalty or miss rate via parallelism

10. Hardware prefetching

假設cache block只能裝下一個int,然后我們有如下指令:

int a[];
load a[0];
load a[1];
load a[2];
load a[3];
load a[4];
load a[5];

那么與其每次都cache miss重新載入,不如在第一次cache miss(load a[0])時,讓cache預測到接下來會用到a[1], a[2], a[3], ......,然后提前載入到next level cache里備用。這就是硬件的prefetching。

對於Instruction Prefetching,CPU fetches 2 blocks on a miss: the requested block and the next consecutive block.(Requested block is placed in instruction cache when it returns, and prefetched block is placed into instruction stream buffer)
對於Data Prefetching,Pentium 4 can prefetch data into L2 cache from up to 8 streams from 8 different 4 KB pages. Prefetching invoked if 2 successive L2 cache misses to a page, or if distance between those cache blocks is < 256 bytes.

但hardware prefetching只對比較predictable的access pattern(特別是instruction prefetching)起作用。如果是訪問一個動態鏈表那就不管用了......

 

 11. Compiler prefetching

 ????(QR P69)

 

最后是對這些cache optimization的一個總結(QR  P72):

 

 

 

 

 

 

 

...

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM