大话局部性原理

一.局部性原理内容概述

程序的局部性原理是计算机系统设计的四个定量原理之一,程序执行时所访问的存储器分布存在 一个规律:程序执行时间的90%是在执行程序中的10%的代码

局部性原理一般表现在两个方面:

  • 时间局部性

程序中的某条指令一旦执行,不久后该指令可能再次执行;某数据被访问过,不久后该数据可能再次被访问。就是即将访问信息很可能就是目前正在使用的信息。

  • 空间局部性

一旦程序访问了某个存储单元,不久后,其附近的存储单元也将被访问,就是即将访问信息和现在正在使用的信息在空间上相邻或者临近。

总而言之,局部性原理描述了计算机程序具有引用其最近引用过的数据项或其邻近数据项的倾向性,可以利用该原理影响计算机软件和硬件系统的设计和性能。

二.局部性原理在存储系统方面的应用

核心思想——高速缓存技术

1.存储系统层次结构

 ​​

存储系统层次结构的中心思想是:

对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。

换句话说,层次结构中的每一层都缓存来自较低一层的数据对象

注:使用高速缓存的过程称为缓存

缓存思想的具体阐述:

 第k+1层存储器被划分为连续的块,它们大小相等,且每个块都有唯一的地址或名字,用来区分于其它块。

第k层存储器被划分为较少的块的集合,且大小与k+1层一样,在任何时刻,k层中的块集合都是k+1层块的一个子集的副本,例如4,9,14,3就是k+1层的一个子集。

数据以块为单元在两层之间传送,如果程序需要k+1层中的一个数据块,它会先在k层中查找,如果找到了所需要的数据块,则省去了一些不必要的遍历,节省了查找时间,提高了程序效率,这就是缓存技术的作用。

如果在k层中找到了所需的k+1层中的数据块,则称为缓存命中,如果没有找到,则称为缓存不命中,那么就需要在k+1层中取出所需的数据块复制到k层中,因为根据局部性原理,下次程序很可能还需要这个数据块,但是如果k层数据块已满,就需要淘汰掉一个数据块,如何淘汰,淘汰哪个块,则需要另外一种技术——替换策略,在后文会阐述。

缓存不命中有以下几种:

(1)冷不命中

也叫强制性不命中,因为k层是空的,所以缓存为空,这时一定不命中

(2)冲突不命中

因为硬件缓存通常使用更严格的放置策略,强制k+1层的某个块必须放在k层固定位置,会引发一种一直不命中的现象

(3)容量不命中

例如一个循环会反复访问一个数组,该数组可能大小超过缓存大小,缓存就会经历容量不命中。

 2.高速缓冲存储器——Cache

这是硬件层中,局部性原理的一个具体应用

Cache的作用:解决CPU与主存之间速度不匹配而采用的一项重要技术

Cache的基本工作原理 

CPUCache间的数据交换是以为单位;

Cache与主存间的数据交换是以为单位;

      当CPU读取主存的一个字时,便发送字的内存地址给Cache和主存,此时Cache的控制逻辑依据地址判断此字当前是否在Cache中:若是,则此字立即传送给CPU,若非,则用主存读周期把此字从主存中读出送到CPU,同时将含有这个字的整个数据块从主存读出送到Cache中。

 Cache的命中率

设在一段程序执行期间,Cache完成存取次数为Nc主存完成存取次数为Nm,h定义为命中率则有:

h=\frac{N_{_{c}}}{N_{_{c}}+N_{_{m}}}

     根据程序局部性原理,由高速缓冲存储器和主存构成 Cache存储系统,实现主存速度的提高。CPU 访问 Cache存储系统时,当发生块失效时,将信息所在的数据块从主存中调入Cache,根据程序局部性原理,在接下来程序访问的数据很大可能就是刚才所访问的数据(时间局部性)或者 是与刚才访问信息相邻近的数据(空间局部性),刚刚访问的数据和其临近的数据很可能在同一块中,已经调入 Cache中。因 此,CPU执行程序 时,在Cache存储系统 中,Cache的命中率非常高,Cache存储系统的速度接近于 Cache的访问速度

缓存管理:由内置在缓存中的硬件逻辑来管理的 

综上,局部性原理在高速缓冲存储器Cache中的应用是宏观上弥补了CPU与主存之间的速度不匹配

3.快表(TLB)

链接好的程序要装入内存,可以采用非连续分配方式,把程序分成页或段为单位的片段,在装入内存的过程中,要用地址转换机构把逻辑地址转换为物理地址,分页存储管理的地址转换机构如下:

地址变换过程为:

 ①求页号,页内偏移量

页号=逻辑地址/页面长度 (取除完整数)

页内偏移量=逻辑地址%页面长度

 ②判断页号是否越界

 比较页号P和页表长度M,若P≧M,则产生越界中断,否则继续执行

③求内存块号

 要找页表项地址,页表项地址=页表起始地址F+页号P*页表项长度,要访问内存查页表

 ④得到物理地址

物理地址=内存块号*页面大小+页内偏移量

 ⑤访问目标内存单元

又访问一次内存

 上面的地址转换过程可知,页表常驻内存,则存取一个数据或一条指令至少要两次访问内存:第一次访问页表,确定所存取的数据或指令的物理地址;第二次是根据该地址存取数据或指令。显然这种方式速度较慢,效率较低。

但是根据局部性原理,可以将最近访问过的页表项放入高速缓存,称为快表,是存储在相联存储器中的页表(TLB),改进后为:

在具有快表的分页机制中,地址变换过程如下:

 ①求页号,页内偏移量

页号=逻辑地址/页面长度 (取除完整数)

页内偏移量=逻辑地址%页面长度

 ②判断页号是否越界

 比较页号P和页表长度M,若P≧M,则产生越界中断,否则继续执行

③查快表

如果在快表中找到匹配的页号,说明所要访问的页表项在快表中,直接从中取出该页对应的内存块号,与页面偏移量拼接成物理地址,存取数据仅一次访存便可实现。(不用访问内存了)

 ④得到物理地址

物理地址=内存块号*页面大小+页内偏移量

 ⑤访问目标内存单元

访问内存一次

查找快表时,若该页已登记在快表中(命中),则由块号和块内偏移形成绝对地址;若快表中查不到页号(未命中),则只能再查内存中的页表来形成绝对地址,同时将该页登记到快表中。当快表填满后,又要在快表中登记新页时,则需在快表中按一定策略淘汰一个旧的登记项,又需要替换策略。

综上,局部性原理在地址转换策略中的应用是减少访问内存的次数,提高了地址转换效率。

4.虚拟存储器

虚拟内存技术是局部性原理的又一个重大应用,这是在操作系统层中,局部性原理的一个体现

回到最初,程序执行时间的90%是在执行程序中的10%的代码这种程序执行的部分高频率性恰恰提供了一个对立的思路,就是有很多的程序并不被经常使用于是局部性原理表明了一种程序的独立性,程序中可能存在彼此互斥或相互独立的部分,每次运行时总有部分程序不被使用,没有必要将总不被使用的部分放入内存。

聪明的科学家立刻想到了另外一个矛盾:通常一个程序或者一个应用软件要占据很大的内存空间,但是内存的容量是固定的,如何解决内存容量不够的问题。

上面的思路告诉我们,有很多程序是很少执行的,那么它们有必要长期驻留内存吗,如果在必要时把它们换出去,让经常被执行的程序进到内存,那么内存空间是不是就可以支撑起整个程序了,内存大小实际没变,但是在运作上却好像容量变大了一样,虚拟内存技术问世了!

经过虚拟内存思想加工过的内存就像是大了很多,成为了一个虚拟存储器,但实际上这种存储器并不存在。

 综上,局部性原理在虚拟内存技术中的应用是在逻辑上扩充了内存容量。

还有很多存储技术利用了局部性原理,例如Mysql的memory存储引擎,redis技术,多体交叉存储器等,在此不一一列举

三.局部性原理在计算机网络方面的应用

1.Web代理服务器

高速缓存思想在计算机网络方面也尽显神威

 如上图

机构网络是一个高速局域网,这些初始服务器与因特网相连但位于全世界各地。因为所有通信量都经过一条链路,就会导致链路上的时延很大。一个可能的解决办法就是增加接入链路的速率,但是这种方法成本太高。

     现在来考虑另一种解决方案,即不升级链路带宽而是在机构网络中安装一个Web缓存器。这样一部分的请求会从Web缓存器中得到响应,大大降低了接入链路上的流量强度。

下面阐述一下Web缓存器运作原理:

假设浏览器正在请求对象中国矿业大学-主页 (cumt.edu.cn)http://www.cumt.edu.cn/将会发生如下情况:

1)浏览器创建一个到Web缓存器的TCP连接,并向Web缓存器中的对象发送一个HTTP请求。

2) Web缓存器进行检查,看看本地是否存储了该对象副本。如果有,Web缓存器就 向客户浏览器用HTTP响应报文返回该对象。

3) 如果Web缓存器中没有该对象,它就打开一个与该对象的初始服务器(即 www.cumt.edu)的TCP连接。Web缓存器则在这个缓存器到服务器的TCP连接上发送一个对该对象的HTTP请求。在收到该请求后,初始服务器向该Web缓存器发送具有该对象的HTTP响应。

4) 当Web缓存器接收到该对象时,它在本地存储空间存储一份副本,并向客户的浏览器用HTTP响应报文发送该副本(通过现有的客户浏览器和Web缓存器之间的TCP连接)。

        除了向代理服务器而不是真正服务器发送Web请求外,客户还要用它的浏览器高速缓存来执行它们自己的缓存。只有在浏览器尝试以自己的高速缓存页面无法满足请求之后,才会咨询代理服务器,也就是说,代理服务器提供了一个第二级缓存。

 Web缓存相当于一个网络Cache,也起到类似的作用,也具有命中率,替换策略,预取技术等相关技术,也是局部性原理的一个体现。

 综上,局部性原理在Web代理服务器技术中的应用是降低了因特网服务器的时延,提高了信息传输效率。

2.内容分发网CDN

        由于大量的请求会导致源服务器超载,所以引入CDN来缓解源服务器压力,就是在世界各地设置CDN结点,如果用户请求数据,直接从就近的CDN结点来获取信息,从而减少源服务器的带宽占用并且缩短了用户的等待时间。

        CDN为客户提供了这样的信息:在一组分布在不同地点的节点中哪个节点拥有所请求页面的副本,并且指示客户端使用附近的节点作为服务器。

        如下图,这是一棵树,CDN的源服务器将一份内容副本分发到CDN中的其他节点。在这个例子中,这些节点分别位于鞍山、昆明和徐州,图中用虚线表示。然而,客户端从就近的CDN节点获取页面,图中用实线表示,这样,在鞍山的客户获取的都是存储在鞍山的页面副本,他们不会从源服务器处获取页面,源服务器可能在西南。

      内容分发网CDN旨在减小源服务器压力,使用户快速就近获取信息,当一个CDN集群存储器变满时,它就会删除不经常请求的副本,类似于网络上的Cache,也利用了高速缓存思想,也是局部性原理的一个重要应用

综上,局部性原理在内容分发网技术中的应用是通过设置一些源服务器的代理节点,减小了源服务器的带宽占用,缩短用户请求信息的时间。

此外局部性原理在计算机网络方面还有其他应用,例如DNS缓存等,大多都是利用了高速缓存思想。

四.利用局部性原理优化程序代码

1.局部性原理在程序中的体现

给出一段C语言程序:

//程序1
int sumvec(int v[N])
{
    int i,sum=0;
    for(i=0;i<N;i++)
    sum+=v[i];
    return sum;
}

在这个例子中,变量 sum 在每次循环迭代中被引用一次,因此,对于 sum 来说,有好的时间局部性。另一方面,因为是标量,对于 sum 来说,没有空间局部性。

数组v的引用模式,N=8 

        数组 v 的元素是被顺序读取的,一个接一个,按照它们存储在内存中的顺序(为了方便,我们假设数组是从地址 0 开始的)。 因此,对于变量 v, 函数有很好的空间局部性,但是时间局部性很差,因为每个数组元素只被访问一次。因为对于循环体中的每个变量,这个函数要么有好的空间局部性,要么有好的时间局部性,所以我们可以断定 sumvec 函数有良好的局部性。

           我们说像sumvec这样顺序访问一个数组每个元素的函数,具有步长为 1 的引用模式 。 有时我们称步长为 1 的引用模式为顺序引用模式。一个连续向量中,每隔k个元素进行访问,就称为步长为k的引用模式。步长为 1 的引用模式是程序中空间局部性常见和重要的来源。一般而言,随着步长的增加,空间局部性下降。

再看一段程序:

//程序2
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;
}

         对于引用多维数组的程序来说,步长也是一个很重要的问题。例如,考虑以上程序中的它对一个二维数组的元素求和。双重嵌套循环按照行优先顺序读数组的元素。也就是,内层循环读第一行的元素,然后读第二行,依此类推。 函数 sumarrayrows 具有良好的空间局部性,因为它按照数组被存储的行优先顺序来访问这个数组,其结果是得到一个很好的步长为 1 的引用模式,具有良好的空间局部性。 

 但是,稍微改动一点,就会发生很大变化:

//程序3
int sumarraycols(int a[M][N])
{
    int i,j,sum = 0;
    
    for(j = 0; j < N; j++)
        for(i = 0; i < M; i++)
            sum += a[i][j];
    return sum;
}

 例如,程序3中的函数 sumarraycols 计算的结果和程序2中函数 sumarrayrows 的一样。唯一的区别是我们交换了i和j的循环。这样交换循环对它的局部性有何影响?函数 sumarraycols 的空间局部性很差,因为它按照列顺序来扫描数组,而不是按照行顺序。因为 C 数组在内存中是按照行顺序来存放的,结果就得到步长为 N 的引用模式,如下图所示:

 2.高速缓存对程序性能的影响

 一个程序从存储系统中读数据的速率称为读呑吐量,或者有时称为读带宽。如果一个程序在 s 秒的时间段内读 n个字节,那么这段时间内的读吞吐量就等于n/s ,通常以兆字节每秒(MB/s)为单位。

所以我们可以通过编写代码来对一个计算机CPU进行读吞吐量的测试,计算规模为size的数据的处理数elem,然后计算出每一个元素处理的周期数,作比即可得总吞吐量。

这部分代码不是本文重点,代码可参考《深入理解计算机系统》第444页以及CMU提供的源代码

Index of /afs/cs/academic/class/15213-f15/www/code/12-cache-memories/mountain (cmu.edu)http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/code/12-cache-memories/mountain/?C=D;O=D

得出的数据经过可视化之后,就可得到该计算机的存储器山

存储器山:

存储器山是一个读带宽的时间和空间局部性的二维函数,反映了存储器层次结构中不同层次的带宽。也反映了具有不同时间、空间局部性的程序的性能。每个计算机都有表明它存储器系统的能力特色的唯一的存储器山。

Slopes of spqtial locality:空间局部性的斜坡

Ridges of temporal locality:时间局部性的山脊

X轴:Stride,步长      Y轴:Size 读带宽     Z轴:读吞吐率

          这座 Core i7 山的地形地势展现了一个很丰富的结构。垂直于大小轴的是四条山脊, 分别对应于工作集(一个进程当前正在使用的页面的集合)完全在 L1 高速缓存、L2 高速缓存、L3 高速缓存和主存内的时间局部性区域。注意,L1 山脊的最高点(那里 CPU 读速率为 14GB/S)与主存山脊的最低点(那里 CPU 读速率为 900MB/S)之间的差别有一个数量级。

      在 L2、L3 和主存山脊上,随着步长的增加,有一个空间局部性的斜坡,空间局部性下降。注意,即使当工作集太大,不能全都装进任何一个高速缓存时,主存山脊的最高点也比它的最低点高 8 倍。因此,即使是当程序的时间局部性很差时,空间局部性仍然能补救,并且是非常重要的。

         从存储器山可以明显也看到小步长读吞吐率更高—— 这也是代码中要使用步长为 1 的顺序访问的另一个理由。

        如果我们从这座山中取出一个片段,保持步长为常数,如图所示,我们就能很清楚地看到高速缓存的大小和时间局部性对性能的影响了。大小最大为 32KB 的工作集完全能放进 L1 d-Cache 中,因此,读都是由 L1 来服务的,吞吐量保持在峰值 12GB/S 处。 大小最大为 256KB 的工作集完全能放进统一的 L2 高速缓存中,对于大小最大为 8 M,工作集完全能放进统一的 L3 高速缓存中。更大的工作集大小主要由主存来服务。

        以相反的方向横切这座山,保持工作集大小不变,我们从中能看到空间局部性对读吞吐量的影响。下图展示了工作集大小固定为 4MB 时的片段。这个片段是沿着L3 山脊切的,这里,工作集完全能够放到 L3 高速缓存中,但是对 L2 高速缓存来说太大了。

        明智的程序员会试图构造他们的程序,使得程序运行在山峰而不是低谷。目标就是利用时间局部性,使得频繁使用的字从 L1 中取出,还要利用空间局部性,使得尽可能多的字从一个 L1 高速缓存行中访问到。

3.编写高速缓存友好的代码

先介绍几个高速缓存概念:

不命中率:在一个程序执行或程序的一部分执行期间,内存引用不命中的比率。它是这样计算的:不命中数量/引用数量。

命中率:命中的内存引用比率。它等于1- 不命中率。

局部性比较好的程序更容易有较低的不命中率,而不命中率较低的程序往往比不命中率较高的程序运行得更快。

!!!确保代码高速缓存友好的基本方法:

(1)把注意力集中在程序最常执行的地方

      程序通常把大部分时间都花在少量的核心函数上,而 这些函数通常把大部分时间都花在了少量循环上。所以要把注意力集中在核心函数里的循环上,而忽略其他部分。

(2)尽量减小每个循环内部的缓存不命中数量

在其他条件(例如加载和存储的总次数)相同的情况下,不命中率较低的循环运行得更快。 为了看看实际上这是怎么工作的,考虑程序1中的函数 sumvec

//程序1
int sumvec(int v[N])
{
    int i,sum=0;
    for(i=0;i<N;i++)
    sum+=v[i];
    return sum;
}

        一 般而言,如果一个高速缓存的块大小为 B 字节,那么一个步长为 k 的引用模式(这里k是 以字为单位的)平均每次循环迭代会有 min(1, (wordsize*k)/B次缓存不命中。当 k=1 时,它取最小值,所以对 v 的步长为 1 的引用确实是高速缓存友好的。

例如:假设 v 是块对齐的,字为 4 个字节,高速缓存块为 4 个字,而高速缓存初始为空(冷高速缓存)。 1X4/4x4=1/4 然后,无论是什么样的高速缓存结构,对v的引用都会得到下面的命中和不命中模式:

        在这个例子中,对 v[0]的引用会不命中,而相应的包含 v[0]~v[3]的块会被从内存加载到高速缓存中。因此,接下来三个引用都会命中。对 v[4]的引用会导致不命中,而一个新的块被加载到高速缓存中,接下来的三个引用都命中,依此类推。总的来说,四个引用中,三个会命中,在这种冷缓存的情况下,这是我们所能做到的最好的情况了。 

总之:

  •  对局部变量的反复引用是好的,因为编译器能够将它们缓存在寄存器文件中(时间局部性)。
  • 步长为 1 的引用模式是好的,因为存储器层次结构中所有层次上的缓存都是将数据 存储为连续的块(空间局部性)。

        在对多维数组进行操作的程序中,空间局部性尤其重要。例如,程序2中的 sumarrayrows 函数,它按照行优先顺序对一个二维数组的元素求和:

//程序2
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;
}

        由于 c 语言以行优先顺序存储数组,所以这个函数中的内循环有与 步长为 1 的访问模式。例如,假设我们对这个高速缓存做与对sumvec一样的假设,那么对数组 a 的引用会得到下面的命中和不命中模式:

 如果我们做一个看似无伤大雅的改变——交换循环的次序,如程序3

在这种情况中,我们是一列一列而不是一行一行地扫描数组的。如果我们够幸运,整个数组都在高速缓存中,那么我们也会有相同的不命中率 1/4。不过,如果数组比高速缓存要大(更可能出现这种情况), 那么每次对 a[i]a[j]的访问都会不命中!

另有两种提高局部性的方法:

 (1)重新排列循环以提高空间局部性

问题引入:

考虑一对矩阵相乘的问题:C=AB。例如,如果 n=2 那么

\begin{bmatrix} C_1_1&C_1_2 \\ C_2_1&C_2_2 \end{bmatrix}=\begin{bmatrix} a_1_1&a_1_2 \\ a_2_1&a_2_2 \end{bmatrix}+\begin{bmatrix} b_1_1 &b_1_2 \\ b_2_1 &b_2_2 \end{bmatrix}

其中:

C_1_1=a_1_1b_1_1+a_1_2b_2_1

C_1_2=a_1_1b_1_2+a_1_2b_2_2

C_2_1=a_2_1b_1_1+a_2_2b_1_2

C_2_2=a_2_1b_1_2+a_2_2b_2_2

矩阵乘法函数通常是用 3 个嵌套的循环来实现的,分别用索引 i、 j 和k来标识。

  • 矩阵乘法 ijk
for (i=0; i<n; i++) 
{ 
	for (j=0; j<n; j++) 
	{ 
		sum = 0.0; 
		for (k=0; k<n; k++) 
			sum += A[i][k] * B[k][j]; 
		C[i][j] = sum;
	}
}

内循环迭代模式:

          以步长1扫描数组A的一行。 因为每个高速缓存块保存四个8字节的字,A 的不命中率是每次迭代不命中0.25次。另一方面,内循环以步长n扫描数组B的一列。:因为n很大,每次对数组 B 的访问都会不命中,所以每次迭代总共会有1.25次不命中。

每次迭代不命中率: A:0.25    B:1    C:0

  • 矩阵乘法 jik
for (j=0; j<n; j++) 
{ 
	for (i=0; i<n; i++) 
	{ 
		sum = 0.0; 
		for (k=0; k<n; k++) 
			sum += A[i][k] * B[k][j]; 
		C[i][j] = sum; 
	}
}

内循环迭代模式:

 每次迭代不命中率: A:0.25    B:1    C:0

  • 矩阵乘法 ikj
for (i=0; i<n; i++) 
{ 
	for (k=0; k<n; k++) 
	{ 
		r = A[i][k]; 
		for (j=0; j<n; j++) 
			C[i][j] += r * B[k][j];
	} 
}

内循环迭代模式:

  每次迭代不命中率: A:0    B:0.25    C:0.25

  • 矩阵乘法 kij
for (k=0; k<n; k++) 
{ 
	for (i=0; i<n; i++) 
	{ 
		r = A[i][k]; 
		for (j=0; j<n; j++) 
			C[i][j] += r * B[k][j];
	}
}

内循环迭代模式:

  每次迭代不命中率: A:0    B:0.25    C:0.25

  • 矩阵乘法 jki
for (j=0; j<n; j++)
{ 
	for (k=0; k<n; k++) 
	{ 
		r = B[k][j]; 
		for (i=0; i<n; i++) 
			C[i][j] += A[i][k] * r;
	}
}

内循环迭代模式:

 每次迭代不命中率: A:1    B:0    C: 1

  • 矩阵乘法 kji 
for (k=0; k<n; k++) 
{ 
	for (j=0; j<n; j++) 
	{ 
		r = B[k][j]; 
		for (i=0; i<n; i++) 
			C[i][j] += A[i][k] * r;
	}
}

内循环迭代模式:

每次迭代不命中率: A:1    B:0    C: 1 

       下图小结了一个Corei7系统上的矩阵乘法各个版本的性能,这个图画出了测量出的每次内循环迭代所需的CPU周期数作为数组大小(n)的函数。

 可以得出结论,调换循环顺序可以改变程序性能

(2)使用分块来提高时间局部性 

用上面的ijk矩阵举例:

每个高速缓存块保存四个8字节的字,且高速缓存块大小远远小于n

那么总的不命中率应该是(n/4+n)*n^2=(5*n^3)/4,显然太高,根本没有局部性

如果我们利用分块思想,把大矩阵切分成更多的小矩阵,再看一下效果

  

C = (double *) calloc(sizeof(double), n*n);    //建立n*n大矩阵

    int i, j, k;
    for (i = 0; i < n; i+=w)
	for (j = 0; j < n; j+=w)
             for (k = 0; k < n; k+=w)
		 /* 分成w*w的迷你小矩阵乘法 */
                  for (i1 = i; i1 < i+w; i++)
                      for (j1 = j; j1 < j+w; j++)
                          for (k1 = k; k1 < k+w; k++)
             /*注意下面这行代码,是把二维数组用一维数组储存了*/
	                      C[i1*n + j1] += A[i1*n + k1]*B[k1*n + j1];  
for (i=0; i<n; i++) 
{ 
	for (j=0; j<n; j++) 
	{ 
		sum = 0.0; 
		for (k=0; k<n; k++) 
			sum += A[i][k] * B[k][j]; 
		C[i][j] = sum;
	}
}

对比一下,体会分块

利用线性代数分块矩阵乘法:

 计算不命中率:

2n/w(两个矩阵,每一个有n/w个块) * w^2/8(每个块有w*w个元素,每个不命中概率为1/8) = n*w/4

 n*w/4 * (n/w)^2 = n^3/(4w)

n^3/(4w)必然小于(5*n^3)/4

分块有以下要求:

建议最大可能的块尺寸w, 但需受限 3w^2 < C

关于利用局部性原理优化程序代码就讨论到这里,综上所述有以下忠告:

  • 将你的注意力集中在内循环上,大部分计算和内存访问都发生在这里。
  • 通过按照数据对象存储在内存中的顺序、以步长为 1 的来读数据,从而使得你程序中的空间局部性最大。
  • 一旦从存睹器中读入了一个数据对象,就尽可能多地使用它,从而使得程序中的时间局部性最大。

 五.总结

    万字长文归纳了局部性原理在计算机科学领域多方面的应用,并分析了如何利用局部性原理优化程序,程序的局部性原理作为计算机系统设计的四个定量原理之一,应该引起重视,多理解运用有助于计算机系统能力的提升。读者还可以去阅读Dijkstra著名的关于“goto语句有害”的论文,也是考虑了程序的局部性原理,还可以阅读参考文献[5],去了解一个局部性原理在生产开发中具体应用的例子。

参考文献:

[1]深入理解计算机系统(原书第 3 版)/(美)兰德尔 E.布菜恩特(Randal E.Bryant) 等著; 龚奕利,贺莲译 .一北京:机械工业出版社,2016.7

[2]计算机网络:自顶向下方法(原书第7版)/(美)詹姆斯・F.库罗斯(James F. Kurose), (美)基思・W.罗斯(Keith W. Ross)著;陈鸣译•一北京:机械工业出版社,2018.5

[3]计算机网络(第5版).(美)特南鲍姆.(美)韦瑟罗尔 著,严伟潘爱民译出版社:清华大学出版社 

[4]郑丽萍,赵玉娟.《计算机系统结构》课程中程序局部性原理的应用[J].软件导刊(教育技术),2018,17(06):42-43.DOI:10.16735/j.cnki.jet.2018.06.018.

[5]谭海.基于局部性原理的小额实时电子支付结算系统设计[J].计算机应用与软件,2013,30(03):13-16.

[6]b站课程:【精校中英字幕】2015 CMU 15-213 CSAPP 深入理解计算机系统 课程视频_哔哩哔哩_bilibili

[7]CMU的课件,地址:15-213: Introduction to Computer Systems / Schedule Fall 2015 (cmu.edu)http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/schedule.html[8]博主飞翔的哈士奇的文章:储存器山和矩阵乘法_飞翔的哈士奇的博客-CSDN博客_fcyc2博客简介关于cache的性能,影响因素由很多。固有的cache大小即cache组规模会影响cache的性能,这由计算机内部硬件属性决定。而对于编码者来说,可以考虑编写对Cache友好的代码,提高空间局部性进一步来优化cache性能。本博客也将对这两方面展开讨论,用两个具体的例子来测算cache性能的影响因素;其中储存器山的例子展示固有的cache大小,空间局部性对吞吐量的影响。矩阵乘法的例子则考...https://blog.csdn.net/weixin_44307065/article/details/105899831 

猜你喜欢

转载自blog.csdn.net/qq_53162179/article/details/125004093
今日推荐