程式設計原則——區域性性原理
儲存器系統是一個具有不同容量、成本和訪問時間的儲存裝置的層次結構:CPU暫存器-》高速緩衝儲存器-》主儲存器-》磁碟-》通過網路連線的其他儲存裝置。
SRAM靜態,一般作為高速緩衝儲存器。
DRAM動態,一般作為大容量的主儲存器
每次CPU和主存之間的資料傳送都是通過一些列的步驟完成的,這些步驟稱為匯流排事務。讀事務從主存傳送資料到CPU,寫事務從CPU傳送資料到主存。
區域性性:一般較好的程式都有較好的區域性性,也就是說,它們傾向於引用的資料項鄰近於其他最近引用過的資料項,或者鄰近於最近自我引用過的資料項。對應的就是空間區域性性和時間區域性性。
區域性性小結:
1、重複引用同一變數的程式有較好的時間區域性性。
2、對於具有步長為k的引用模式的程式,步長越小,空間區域性性就越好。具有步長為1的引用模式的程式有很好的空間區域性性。在儲存器中以大步跳來跳去的程式的空間區域性性就很差。
3、對於取指令來說,迴圈有很好的空間和時間區域性性。迴圈體越小,迴圈迭代次數越多,區域性性越好。
編寫快取記憶體友好的程式碼
編寫高速緩衝友好的程式碼的基本方法:
1、讓最常見的情況執行得快。程式通常把大部分時間都花在少量的核心函式上,而這些函式通常把大部分時間都花在了少量的迴圈上。所以要把注意力集中在核心函式的迴圈上,而忽略其他部分。
2、在每個迴圈內部使快取不命中數量最小。在其他條件,例如載入和儲存的總次數相同的情況下,不命中率低的程式執行得更快。
注意:編譯器將區域性變數儲存到暫存器中,因此迴圈內對區域性變數的引用不需要任何載入或儲存指令。
快取記憶體對程式效能的影響:
1、通過重新排列迴圈以提高空間區域性性:降低高速緩衝的不命中率。例子(求兩個矩陣的乘積)
2、使用分塊來提高時間區域性性。
分塊的大致思想是將一個程式中的資料結構組織成稱為塊(block)的組塊(chunk)。這裡的“塊”指的是一個應用級的塊,不是高速緩衝塊。這樣構造程式,使得能夠將一個塊載入到L1快取記憶體中,並在這個塊中進行所需的所有的讀和寫,然後丟掉這個塊,載入下一個塊,以此類推。
但是分塊可能帶來的負面影響就是會降低程式的可讀性。
在程式中利用區域性性:
為了編寫更有效的程式,不論具體的儲存結構是怎樣的。推薦以下技術:
1、將注意力集中在內部迴圈上,大部分計算和儲存器訪問都發生在這裡。
2、通過按照資料物件儲存在儲存器中的順序來讀取資料,從而使程式的空間區域性性最大。
3、一旦從儲存器中讀入了一個數據物件,就儘可能多的使用它,從而使得程式的時間區域性性最大。
4、記住,不命中率只是確定程式碼效能的一個因素(雖然是重要的)。儲存器訪問數量也扮演中重要的角色,有時需要在兩者之間做一個折中。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~·
這裡在補充一些有關高速緩衝儲存器的內容:
對於一個計算機系統,如果每個儲存器的地址有m位,可形成M=2m個不同的地址。而快取記憶體一般被組織成一個S=2s個快取記憶體組(cache set)的陣列。每個組包含E個快取記憶體行(cache line)。每一行由B=2b位元組的資料塊(block)組成。一個有效位(valid bit)指明這個行包含的資料是否有效,還有t=m-(b+s)個標記位(tag bit)(是當前塊的儲存器地址的位子集),它們唯一的標示儲存在這個快取記憶體行中的塊。
快取記憶體的大小指的是所有塊的大小的和(不包括有效位和標記位在內)。因此C=S*E*B。
快取記憶體主要分為:直接對映快取記憶體(每一組只有一行),組相聯快取記憶體(每一組有兩行以上),全相聯快取記憶體(只有一個組,這個組包含了所有的行)。
注意:快取記憶體載入資料時是以每一行的塊大小來載入資料的,所以正是基於這一點,我們才能利用快取記憶體來提高程式效能。
小技巧:當陣列的大小是2的冪時,直接對映快取記憶體中通常很容易發生衝突不命中,一般可以通過在陣列尾部進行填充來完成“抖動”現象(即,資料反覆在快取記憶體中發生不命中,而頻繁的換進換出)。
但是我看了一下,現在的計算機一般在d-cache、i-cache、二級快取使用的都是組相聯或全相聯形式。
注意:快取記憶體利用的是地址位的中間位作為快取的索引,所以這樣做相對於使用地址高位作為索引的好處是,主存相鄰的塊總是對映到不同的快取記憶體行。這一點又是我們可以利用在程式效能提高上的原理所在。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
* 高速緩衝實驗
* 發現:
* 1、如果在內層迴圈中,儲存器載入和儲存次數相同的情況下,高速緩衝命中率的高低
* 反映在程式執行時間上要相差大約200~300ms
* 2、高速緩衝命中率只是一個因素,迴圈中儲存器的載入次數和儲存次數也是會起到很大的
* 影響,第一個測試程式就比第二個測試程式執行效率更高,雖然它命中率沒有另一個高
* 3、這種高速緩衝的效果一般在較大資料集上,才有明顯的差異,所以如果在資料集比較小的情況下,不用在這個高速緩衝上花心思
*/
#include<iostream>
#include<ctime>
using namespace std;
#define MAXSIZE 400
#define bsize 25
int main(){
int a[MAXSIZE][MAXSIZE],b[MAXSIZE][MAXSIZE],c[MAXSIZE][MAXSIZE];
int i,j,k,r,kk,jj;
int sum;
//初始化陣列
for(i=0;i<MAXSIZE ; i++)
for(j=0;j<MAXSIZE ; j++){ //這裡初始化的過程對高速緩衝是不友好的
a[i][j]=1;
b[i][j]=1;
c[i][j]=0;
}
clock_t time=clock();
//第一個測試程式
for(i=0 ; i<MAXSIZE ; i++){
for(j=0 ;j<MAXSIZE ;j++){
sum=0;
for(k=0;k<MAXSIZE; k++){
sum+= a[i][k]*b[k][j]; //每次迭代載入2次,儲存0次,a[][]按列讀取,b[][]按行讀取
}
c[i][j]+=sum;
}
}
//第二個測試程式
for(i=0;i<MAXSIZE ; i++){
for(k=0 ;k<MAXSIZE ;k++){
r = a[i][k];
for(j=0 ;j<MAXSIZE ; j++){ //每次迭代載入2次,儲存1次,c[][]按列讀取,b[][]按列讀取
c[i][j]+=r*b[k][j];
}
}
}
//第三個測試程式
for(j =0 ; j<MAXSIZE ; j++){
for( k=0 ;k<MAXSIZE ; k++){
r=b[k][j];
for(i=0; i<MAXSIZE ; i++) //每次迭代載入2次,儲存1次,a[][]按行讀取,c[][]按行讀取
c[i][j]+=a[i][k]*r;
}
}
//第五個測試程式:程式分塊
int en=bsize*(MAXSIZE/bsize);
for(kk=0 ;kk<en ; kk+=bsize){
for(jj=0; jj < en ; jj+=bsize){
for(i= 0 ; i<MAXSIZE ;i++){
for( j=jj ; j< jj+bsize ; j++){
sum = c[i][j];
for( k =kk ; k< kk+bsize ; k++){
sum+=a[i][k]*b[k][j];
}
c[i][j]=sum;
}
}
}
}
cout<<"計算用時:"<<clock()-time<<"MS"<<endl;
return 0;
}