程序设计原则——局部性原理

存储器系统是一个具有不同容量、成本和访问时间的存储设备的层次结构: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;

}

 

没有更多推荐了,返回首页