【深入理解计算机系统】局部性

《深入理解计算机系统》学习笔记

局部性原理:一个编写良好的计算机常常具有良好的局部性,它们倾向于引用临近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。

局部性通常有两种形式:时间局部性空间局部性。在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来再被多次引用。在一个具有良好空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。

提高效率,节省空间。(输入法中的偏好记录?)

硬件层:利用高速缓存储存来保存最近被引用过的指令和数据项,从而提高对主存的访问速度。

操作系统:允许系统使用主存作为虚拟地址空间最近被引用块的高速缓存。用主存来缓存磁盘文件中最近被使用的磁盘块。

Web浏览器:将最近被引用的文档放在本地磁盘上。

对程序数据引用的局部性

求一个向量的元素之和的函数

//求一个向量的元素之和
int sumvec(int v[N])
{
	int i,sum=0;
    
    for(i=0;i<N;i++)
        sum+=v[i];
    
    return sum;
}

向量v的引用模式(N=8):

地址0481216202428
内容v0v1v2v3v4v5v6v7
访问顺序12345678

步长为1的顺序引用模式

此函数具有很好的空间局部性,这个函数具有很好的空间局部性,因为它连续地访问了数组v中的元素,这些元素在内存中是相邻存储的,因此当CPU加载第一个元素时,它会预加载相邻的元素到CPU缓存中,以便在循环中快速访问它们,从而提高程序的执行效率。这种连续的内存访问方式可以充分利用CPU缓存,减少了访问内存的次数,从而提高了程序的性能。

此函数的时间局部性较差,因为它每次循环都要重新访问数组v中的元素,而不是利用之前已经加载到CPU缓存中的元素。这会导致CPU缓存中的数据被频繁地替换,从而降低了程序的执行效率。如果数组v的大小很大,而CPU缓存的大小有限,那么程序的性能将会受到更大的影响。为了提高程序的时间局部性,可以考虑使用循环展开、向量化等技术,以减少循环次数和提高CPU的指令级并行度。

求一个二维数组的元素之和

//双重嵌套循环按照行优先顺序读数组的元素
//空间局部性好
int sumarrayrows(int a[m][n])
{
    int i,j,sum=0;
    
    for(i=0;i<m;i++)
    {
		for(j=0;j<n;j++){
            sum+=a[i][j];
        }
    }
    return sum;
}
//假设有一个二维数组A,它有m行n列,代码按行优先的方式访问A中的元素,那么访问A[i][j]时,程序会首先访问A[i][0]、A[i][1]、A[i][2]、...、A[i][n-1],然后再访问A[i+1][0]、A[i+1][1]、A[i+1][2]、...、A[i+1][n-1],以此类推,直到访问完所有元素。

//修改成按照列优先顺序读数组元素
//空间局部性差
int sumarraycols(int a[m][n])
{
    int i,j,sum=0;
    
    for(j=0;j<n;j++)//循环调换索引i和j的内外位置
    {
		for(i=0;i<m;i++){
            sum+=a[i][j];
        }
    }
    return sum;
}

数组a的引用模式(m=2,n=3)

地址048121620
内容a00a01a02a10a11a12
访问顺序123456

按行优先读取二维数组可以改进程序的空间局部性,因为这样可以利用CPU缓存的行缓存机制。当程序按行访问数组时,CPU缓存会缓存一整行的数据,这样在访问下一个元素时,该元素就已经在CPU缓存中了,可以直接访问,不需要再次从内存中读取,从而加快了程序的执行速度。而按列优先读取二维数组时,CPU缓存只能缓存一列的数据,这样在访问下一个元素时,需要从内存中读取一整列的数据,这样会导致CPU缓存中的数据被频繁地替换,从而降低了程序的执行效率。

补充:行缓存机制

CPU的行缓存机制是一种常见的缓存优化技术,它可以提高程序的执行效率。

行缓存是指CPU缓存中缓存的是内存中的一整行数据,而不是单个字节或单个字。这样做的好处是,当CPU需要访问内存中的一个字节或一个字时,它可以将整行数据加载到缓存中,以便将来访问相邻的字节或字时可以直接从缓存中读取,而不需要再次访问内存。

行缓存机制的实现方式是,当CPU需要访问内存中的一个字节或一个字时,它会将整个缓存行加载到缓存中。如果CPU需要访问相邻的字节或字,它可以直接从缓存中读取,而不需要再次访问内存。这样可以大大减少内存访问的次数,从而提高程序的执行效率。

另外,行缓存机制还可以通过预取技术来进一步提高程序的执行效率。预取是指CPU在访问一个缓存行时,预先将相邻的缓存行加载到缓存中,以便将来访问相邻的缓存行时可以直接从缓存中读取,而不需要再次访问内存。这样可以进一步减少内存访问的次数,从而提高程序的执行效率。

需要注意的是,行缓存机制并不是完美的,它有一些缺点。首先,如果程序访问的数据不是按行排列的,那么行缓存机制就无法充分发挥作用。其次,如果程序访问的数据是随机的,那么行缓存机制也无法发挥作用。因此,在实际应用中,需要根据具体情况选择合适的缓存优化技术。

实际上,现代CPU中的缓存系统并不是只有行缓存机制,还有一些其他的缓存机制,例如组相联缓存、多级缓存、预取缓存等。但是,列缓存机制并不是常见的缓存优化技术。

这是因为,在计算机科学中,数组通常是按行存储的,而不是按列存储的。这是因为按行存储可以充分利用CPU缓存的行缓存机制,从而提高程序的执行效率。相反,如果数组按列存储,那么CPU缓存中的数据就会被频繁地替换,从而降低程序的执行效率。

另外,列缓存机制的实现也比较复杂。因为在列缓存中,缓存行必须包含整个列,而不是整个行。这意味着每个缓存行的大小可能会非常大,导致缓存容量的浪费。此外,由于缓存行的大小不是固定的,这也会导致缓存管理的复杂性增加。

因此,虽然列缓存机制在某些特定的应用场景中可能会有一定的优势,但是在大多数情况下,行缓存机制仍然是更为常见和有效的缓存优化技术。

练习

步长为1的顺序引用模式

//改变下面函数中循环的顺序,使得它以步长为1的引用模式扫描三维数组a
int sumarray3d(int a[n][n][n])
{
    int i,j,k,sum=0;
    
    for(i=0;i<m;i++){
    	for(j=0;j<n;j++){
    		for(k=0;k<n;k++){
           		sum+=a[k][i][j];}
        }
    }
    return sum;
}

int sumarrayrows1(int a[n][n][n])
{
    int i,j,k,sum=0;
    
    for(k=0;k<n;k++){
        for(i=0;i<m;i++){
            for(j=0;j<n;j++){
                sum+=a[k][i][j];
            }
        }
    }
    return sum;
}

结构体


#define N 1000

typedef struct{
    int vel[3];
    int acc[3];
}

point p[N];

void clear1(point *p,int n){
    int i,j;
    for(i=0;i<3;i++){
        p[i].vel[j]=0;
    }
    for(j=0;j<3;j++){
        p[i].acc[j]=0;
    }
}

void clear2(point *p,int n){
	int i,j;
    for(i=0;i<3;i++){
        for(j=0;j<3;j++){
        	p[i].vel[j]=0;
        	p[i].acc[j]=0;
        }
    }
}

void clear3(point *p,int n){
	int i,j;
    for(j=0;j<3;j++){
        for(i=0;i<3;i++){
        	p[i].vel[j]=0;
        }
        for(i=0;i<3;i++){
            p[i].acc[j]=0;
        }
    }
}

解决这个间题的关键在于想象出数组是如何在内存中排列的,然后分析引用模式。

函数clear1以步长为1的引用模式访问数组,因此明显地具有最好的空间局部性。函数clear2依次扫描N个结构中的每一个,这是好的,但是在每个结构中,它以步长不为1的模式跳到下列相对于结构起始位置的偏移处:0、12、4、16、8、20。所以clear2的空间局部性比clear1的要差。函数clear3不仅在每个结构中跳来跳去,而且还从结构跳到结构,所以clear3的空间局部性比clear2clear1都要差。

取指令的局部性

因为程序指令是存放在内存中的,CPU必须取出(读出)这些指令,所以我们也能够评价一个程序关于取指令的局部性。例如,图6-17中for循环体里的指令是按照连续的内存顺序执行的,因此循环有良好的空间局部性。因为循环体会被执行多次,所以它也有很好的时间局部性。

代码区别于程序数据的一个重要属性是在运行时它是不能被修改的。当程序正在执行
时,CPU只从内存中读出它的指令。CPU很少会重写或修改这些指令。

局部性小结

  • 重复引用相同变量的程序有良好的时间局部性。
  • 对于具有步长为k的引用模式的程序,步长越小,空间局部性越好。具有步长为1的引用模式的程序有很好的空间局部性。在内存中以大步长跳来跳去的程序空间局部性会很差。
  • 对于取指令来说,循环有好的时间和空间局部性。循环体越小,循环选代次数越多,局部性越好。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值