一、局部性
一个编写良好的电脑程序倾向于利用好的局部性。这指代程序会引用最近引用过的数据项附近的数据,或者这个程序会经常引用最近引用过的数据项。这种倾向,被称为局部性原则,是一个对软件及硬件系统的设计有着巨大影响的持久性的概念。
局部性通常被描述为两种不同的形式:时间局部性和空间局部性。在一个有着良好空间局部性的程序中,一个被引用过一次的内存地址倾向于在很近的未来多次被重新引用。在一个有着良好空间局部性的程序中,一个被引用过一次的内存地址附近的程序倾向于在很近的未来被引用。
大体上来说,一个有着良好局部新的程序要比差局部性程序运行的更快。所有层次的现代计算机系统,从硬件到操作系统到应用程序,都被设计为探索局部性。在硬件层次,局部性原则允许计算机设计者加速内存访问速度,这通过引入一个称为高速缓存(cache memories)的内存块实现。这个内存块中存储了最近引用过的指令和数据项。在操作系统层次,局部性原则使得使得操作系统将主存作为最近引用过的虚拟地址空间的缓存。类似的,操作系统使用主存来缓存最近与使用过的磁盘系统中的磁盘块。局部性原则在设计应用程序中也扮演了一个非常重要的角色。例如,网络浏览器通过缓存最近引用的本地磁盘文件来利用时间局部性。大容量网络服务器都会在前端磁盘持有最近引用的文件,再次请求这些文件时不需要服务器的任何干预。
1.1 程序数据的局部性
观察图6.19(a)中的示例程序,这个程序有好的局部性吗?
为了回答这个问题,我们需要观察每个变量的引用规律。在这个例子中,sum变量在每次循环终都被引用一次,因此对于sum变量而言有好的时间局部性。另一方面,由于sum是一个标量,对于sum而言没有空间局部性。
如图6.19(b)所示,向量v中的元素都是按序进行读取,这个顺序就是它们在内存中存储的顺序(为了方便假设数组从地址0处开始)。因此,对于变量v,函数有好的空间局部性,但是时间局部性并不好,因为每个向量元素都仅被访问了一次。由于函数在循环体内的每个变量都有时间局部性或空间局部性,因此这个sumvec函数良好的利用了局部性。
一个诸如sumvec的按顺序访问向量中的每一个元素的函数被称为有着1步引用模式(stride-1 reference pattern,1指代元素大小)。有时候1步引用模式也被称为顺序引用模式。访问一个连续向量中的每k个元素称为k步引用模式(stride-k reference pattern)。1步引用模式是计算机中常见也是中很重要的空间局部性的原因。通常来讲,随着步的提升,空间局部性减弱。
对于引用多维度的数组来说步也是一个重要因素。例如,观察图6.20(a)所示的的sumarrayrows函数,这个函数的功能是将二维数组进行加和:
函数内的双重循环以行优先的顺序读取数组元素。sumarrayrows函数拥有良好的空间局部性,因为它读取数组元素的顺序和数组元素按照行优先的存储顺序相同。
看起来很平常的对程序的修改可能导致对空间局部性的巨大影响。例如图6.21(a)所示的sumarraycols函数和图6.20(a)所示的sumarrayrows函数计算同样的结果,唯一的区别是先遍历列再遍历行:
这个程序的空间局部性就很差,结果变成了N步引用模式。
1.2 指令获取的局部性
由于程序指令存储在内存中,并且会被CPU读取,我们也可以考虑获取程序指令时的局部性。例如,图6.19中for循环中的指令被按照再内存中顺序存储的顺序执行,因此这个循环有良好的局部性。由于这个循环体执行了很多次,它同样有着很好的空间局部性。
一个区分程序数据和程序指令的一个很重要的性质就是程序指令很少在执行过程中发生改变。CPU很少覆盖或修改这些指令。
1.3 局部性总结
这一节介绍了局部性的基本概念并且指明了一些简单的辨别程序局部性的一些规则:
- 重复引用相同变量的程序具有良好的时间局部性
- 对于k步引用模式的程序,步越小空间局部性越好。1步引用模式的程序有着最好的空间局部性。
- 循环在获取指令时有较好的时间局部性和空间局部性。循环体的大小越小,循环的次数越大,局部性越好
练习 6.9
图6.22中的三个函数执行相同的功能,但是它们的空间局部性不同,对它们的空间局部性排序:
答案:
二、内存层次
内存层次被用在所有现代计算机内存系统中。图6.23展示了一个典型的内存层次:
大体上来讲,存储设备在从高层次转变向低层次时会变得更慢、更便宜且更大。在最高等级(L0)是一小部分快速的CPU寄存器,CPU可以在一个时钟周期内访问它们。接下来是一个或多个大小较小的基于SRAM的高度缓存,可以在几个CPU时钟周期内访问。然后是较大的基于DRAM的主存,这部分可以在几十到几百个时钟周期内访问到。接下来是速度慢并且相对较大的局部磁盘。最终,一些系统还包含可通过网络访问的位于一些远程处理器上的磁盘。例如,分布式文件系统比如Andrew文件系统(Andrew File System,AFS)或网络文件系统(Network File System,NFS)允许一个程序访问连接到网络中的远程服务器。类似地,广域网(World Wide Web)允许程序访问存储在世界任意角落的网络服务器上的文件。
2.1 内存层次中的缓存
通常来讲,缓存(cache)是一个较小并且访问速度速度快的存储设备,这个存储设备的作用是为存储在一个较大且较慢设备上的数据作为一个暂时的存储平台。使用缓存的过程被称为缓存过程(caching)。
内存层次的核心观点是,对于每层k,更快且更小的位于层次k的存储设备都作为位于k+1层的更大且更慢的存储设备的缓存。换句话说,在内存层次中的每一层数据都会缓存来自下一层更慢的层次的数据项,直到最小缓存层次——CPU的寄存器。
图6.24展示了内存层次中的缓存的通用概念:
内存k+1处的存储被划分为连续的目标数据集合,称为块(blocks)。每一个块都有独一无二的地址或名字,将其和其他块进行区分。块可以是固定大小的(通常情况下)也可以是可变大小的(例如网络服务器上存储的HTML文件)。例如,图中位于k+1层的存储被划分为16个固定大小的块,编号从0-15。
类似地,k层的存储也被划分为小集合块,块大小和k+1层相同。在任意时间点,k层的缓存都包含k+1层的数据块集合的一个子集。例如,在图6.24中,k层的缓存大小为4个块,现在包含着k+1层块4、9、14和3的拷贝。
数据总是在k层和k+1层间进行复制传输,通过块大小的传输单元(transfer units)。我们需要意识到的很重要的一点是,虽然临近层次的块大小事固定不变的,其他的层次间有着不同的块大小。如图6.23所示,L1和L0间通常使用一个字大小的块。L1和L2间(L2和L3,L3和L4)通常使用8到16个字大小的块。从L4到L5间进行的数据传输通常块大小为成百上千个字节。通常来讲,在内存层次中越低的设备有着越久的访问时间,因此趋向于使用更大的传输块来摊销较长的访问时间。
2.1.1 缓存命中(cache hits)
当一个程序需要层次k+1的一个特定的目标数据d时,它首先会在层次k中寻找包含d的块。如果d碰巧缓存在参差k时,我们称这是一次缓存命中。这时程序直接从层次k中读取d。
2.1.2 缓存缺失(cache misses)
如果目标数据d没有在层次k中进行缓存,那么此时就出现了一次缓存缺失。当出现缺失时,在层次k的缓存获取在层次k+1中的缓存中包含d的数据块,并对当前层次中的一个已经存在的数据块进行覆盖。
这个覆盖已经存在数据块的过程称为数据块的替代(replacing)或者驱逐(evicting)。被替代的数据块有时被称为待替换块(victim block)。替换哪个块由缓存的替换政策(replacement policy)决定。例如,一个使用随机替换策略(random replacement policy)的缓存会选择一个随机待替换块。最近未使用(least-recently used,LRU)替换策略会替换那些最长时间未被访问的块。
在层次k从层次k+1处获取数据块后,程序可以从层次k中读取数据d。如图6.24所示,从层次k的块12中读取数据项会导致一次缓存缺失。一旦从层次k+1中将块12拷贝到层次k后,块12将会在下次被覆盖前一直保留在层次k中。
2.1.3 缓存缺失的种类
2.1.4 缓存管理
2.2 对于内存层次概念的总结