1. 程式人生 > >程式設計原則——區域性性原理

程式設計原則——區域性性原理

儲存器系統是一個具有不同容量、成本和訪問時間的儲存裝置的層次結構: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;

}