随机访问存储器(Random-Access Memory,RAM)分两类:静态(SRAM)和动态(DRAM)。
磁盘读取速度比DRAM慢了10万倍,比SRAM慢100万倍。
1)磁盘构造
一个磁盘包含多个盘片,每个盘片有两个表面,每个表面由一组称为磁道的同心圆组成,每个磁道被划分为若干个扇区,每个扇区包含相等数量的数据位,扇区之间由一些间隙分隔开,这些间隙中不存数据位。柱面k指的是所有盘片表面上到主轴中心距离k的磁道集合,如下图b中柱面k是6个磁道k的集合。
2)磁盘容量
3)磁盘操作
磁盘操作主要有寻道、旋转和传送三个过程构成,操作磁盘的时间主要寻道时间和旋转时间的延迟,传送时间相比于两者基本可忽略。
4)磁盘逻辑块
逻辑块是为了对OS隐藏磁盘的复杂结构而抽象一个概念,磁盘控制器维护了一个逻辑块号和物理磁盘扇区之间的映射关系,当OS想执行一个I/O时,比如读一个磁盘数据到主存,OS会发送一个命令到磁盘控制器,让它读某个逻辑块号。控制器上的固件执行一个快速表查找,将一个逻辑块号翻译成一个三元组(盘面,磁道,扇区),这个三元组唯一的标识了对应的物理扇区。
5)访问磁盘
当CPU从磁盘读取数据时,CPU先把命令(读/写)、逻辑块号和目的内存地址发送到磁盘对应的内存映射地址,然后磁盘控制器按照指示读取内容,并且直接将这些内容传送给主存,不需要CPU干涉(这种设备可以自己执行读写而不需要CPU干涉的过程,称为直接内存访问(DMA)),DMA完成后,磁盘控制器给CPU发一个中断,告诉CPU IO操作已经完成。(搭配处理器和IO设备间的通信一起食用效果更佳)
SSD是基于闪存技术的存储技术,读比写快很多。SSD相对于旋转磁盘来说,好处是访问时间块,能耗低。反复写之后,闪存块也会磨损,一般要很多年才能磨损。
良好的程序倾向于引用最近使用过的数据项,或者引用邻近最近使用过的数据项的数据项,这种倾向称为局部性原理。
局部性通常有两种不同形式:时间局部性和空间局部性。时间局部性指被引用过一次的内存位置很快就被引用;空间局部性指一个内存被引用了,很快就会引用其附近的一个内存位置。
如果不明白为什么要利用局部性,仔细阅读下文《3.1 存储器层次结构中的缓存》。
下图中,变量sum
每次循环都被引用一次,所以具有好的时间局部性,由于sum
是标量,所以不具有空间局部性;数组a
的元素是按照内存中的顺序一个接一个读取的(步长为1),所以数组a
具有好的空间局部性。
下图中,数组a
的空间局部性很差,因为访问步长为3或2。
程序指令一般存放在内存的代码区,CPU的PC寄存器和内存之间还有一个指令Cache,所以取指令的操作也有局部性。编译器的优化器会针对取指令的局部性原理进行优化,比如全局代码置放和过程置放。
sum
。k
引用模式的程序,k
越小,空间局部性越好。在内存中以大步长跳来跳去的程序空间局部性很差,比如上面的第二个例子。不同存储技术的访问时间差异很大,速度快的成本高,慢的成本低。如果数据在寄存器中,指令执行期间,访问需要1个时钟周期;在高速缓存SRAM中,需要4~75个周期;在主存DRAM中,需要上百个周期;在磁盘中,大约要几千万个周期。
存储器层次结构的中心思想是,对于每个k
,位于k
层的更快更小的存储设备作为位于k+1
层的更大更慢的存储设备的缓存,换句话说,层次结构中的每一层都缓存来自较低一层的数据对象。
第k + 1
层的存储器被划分成连续的数据对象组块,称为块(block)。每个块都有唯一地址或名字,可以固定大小(通常是这样),也可以变大变小(存储在Web服务器的远程HTML文件)。第k
层的存储器被划分为比较少的块集合,每个块的大小与k + 1
层的块的大小一样。在任何时刻,第k
层的缓存包含第k + 1
层块的一个子集的副本。比如上图有第4、9、14、3
的副本。
数据总是以块大小为传送单元在第k
层和第k + 1
层之间来回复制。在L1到L0之间通常使用的是1个字大小的块(由具体的指令决定),L2和L1之间(L3和L2之间、L4和L3之间)通常是几十个字大小的块,L5和L4之间是大小为几百或几千字节的块。层次结构距CPU越远的设备访问时间越长,因此为了补偿这些较长的访问时间,倾向于使用较大的块。
1)缓存命中
当程序需要第k + 1
层的某个数据对象d时,它首先在当前存储在第k
层的一个块中查找d,如果d刚好缓存在第k
层中,就称为缓存命中。那程序就直接从缓存中读取d,比去第k + 1
层读d更快。
2)缓存不命中
如果第k层没有缓存数据对象d,就称为缓存不命中。不命中时,第k
层的缓存从第k + 1
层缓存中取出包含d的那个块,如果第k
层的缓存已经满了,可能就会覆盖现存的一个块,称为替换或驱逐这个块。决定该替换哪个块是由缓存的替换策略来控制的。有的策略会替换掉最近最少使用的块。
3)缓存不命中的种类
如果第k
层缓存是空的,那任何数据都不会命中。空的缓存被称为冷缓存,这种不命中称为强制性不命中或冷不命中。
在出现了冷不命中后,第k
层缓存需要按照放置策略来把下一层缓存中的数据放置在本层的缓存中,最灵活的放置策略是随机放置,即允许k+1
层任何块放置到k
层的任何块,但是该策略定位块位置的代价很高。所以硬件会使用约束更多的一种策略,强制将第k+1
层的某些块限制放置在第k
层的一个小的子集中(有时只有一个块),比如上面的图示中,第k + 1
层的块0、4、8、12会映射到第k
层的块0;块1、5、9、13会映射到块1。这种放置策略就引起了冲突不命中,比如程序请求块0、然后块8、然后块0、然后块8……在第k
层的缓存中,请求了块0,接下来请求块8就必然不命中,去k + 1
层读了块8再作为缓存后,接下来请求块0也必然不命中。
程序通常是按照一系列阶段来运行,每个阶段访问缓存块的某个相对稳定不变的集合。例如,嵌套循环可能反复的访问同一个数组的元素。这个块的集合称为这个阶段的工作集。当工作集大小超过了缓存大小,缓存就出现了容量不命中。也就是缓存太小,不能处理这个工作集。
4.缓存管理
每一层都需要某种逻辑来管理缓存,比如将缓存划分成块,在不同的层之间传送块,判定是命中还是不命中,并处理它们。管理缓存的逻辑可以是硬件、软件,或者两者结合。
例如,编译器管理寄存器文件,缓存层次结构的最高层。它决定当发生不命中时如何发射加载,以及确定哪个寄存器来存放数据。L1、L2和L3层的缓存完全是由内置在缓存中的硬件逻辑来管理,在一个有虚拟内存的系统中,DRAM主存作为存储在磁盘上的数据块的缓存,是由OS软件和CPU上的地址翻译硬件共同管理的。对于一个具有像AFS这样的分布式文件系统的机器来说,本地磁盘作为缓存,它是由运行在本地机器上的AFS客户端进行管理的。在大多数时候,缓存都是自动运行的,不需要程序采用特殊行动。
1.利用时间局部性:由于时间局部性,同一数据对象可能会被多次使用。一旦一个数据对象在第一次不命中时被复制到缓存中,我们就会期望后面对该目标有一系列的访问命中。因为缓存比低一层的存储设备更快,如果命中了比不命中要快很多。
2.利用空间局部性:块通常包含有多个数据对象。由于空间局部性,我们期望后面对该块中其他对象的访问能够补偿不命中复制该快的花费。
早期计算机系统的存储器层次结构只有三层:CPU寄存器、DRAM主存和磁盘存储。后来在CPU寄存器和DRAM主存间插入了L1高速缓存、L2高速缓存、L3高速缓存,以下会假设一个简单的层次结构,CPU和主存之间只有一个L1高速缓存。
高速缓存的组织结构如下图a,分为S组,每个组有E行,每行上有1个有效位,有t个标记位,有B个字节。所以高速缓存的大小C(指的是所有块的大小的和,不包括有效位和标记位,因为他们不用来存储数据)为:C = S * E * B
。
一条加载指令指示CPU从主存地址A中读一个字时,它将地址A发送到高速缓存。如果高速缓存中正保存着地址A处那个字的副本,它就立刻将那个字发回给CPU。那么高速缓存如何知道它是否包含地址A处那个字的副本呢?
高速缓存的结构使得它能通过简单地检查地址位,找到所请求的字,类似于使用一种简单的函数对应关系。将一个m位(如32位或64位)的地址分为了t、s和b三个段(如上图b),其中s段存放的值作为高速缓存组(每组可能有一行或多行)的索引;t段存放的值与s段对应组中的行的“标记”做匹配,匹配成功则说明地址中存放的值在Cache中;b段存放的值则是地址对应的值在与t段匹配成功的行中的偏移值,找到起始位置,根据加载指令读取的长度读取相应数据即可。
【总结】高速缓存Cache,没有独立的编址空间,处理器访问Cache和访问存储器使用的是相同的地址(给一个m位的地址,存储管理会将其分成三个段,通过这三个段可以找到在Cache中是否有缓存当前地址中的值)。由于Cache没有独立的编址空间,且只能存放一部分内存的内容,所以一个Cache单元可能在不同时刻存储不同的内存单元的内容,这就需要一种机制来标明某个Cache单元当前存储的是哪个内存单元的内容。因此Cache的每一个单元不仅要存储数据,还要存储该数据对应的内存地址(称为Cache标签,Tag)以及在Cache中的状态(如是否有效,是否被改写等)。
Cache的容量远小于内存,会涉及多个内存单元映射到同一个Cache单元的情况,具体怎么映射需要考虑。通常分为3种索引方式:直接相连、全相连和组相连。
在直接相联方式中,每个组只有一行,每个内存块只能放到Cache的一个位置上,假设要把内存的第12号块放到Cache中,且Cache只有8行,所以只能放在第(12 mod 8=4)行上,其他地方都不能放;由此可知第4、12、20、28号内存块都对应到Cache的第4行上,如果冲突了就只能替换。这就是直接相联,硬件简单但效率低。
在全相联方式中,每个内存块都可以放到Cache的任一位置上,这样第4、12、20、28号内存块可以同时放入Cache中。这就是全相联,硬件复杂但效率高。
组相联是直接相联和全相联的折中。以两路组相联为例,Cache中第0、2、4、6号位置为一路(这里称为第0路),第1、3、5、7为另一路(这里称为第1路),每路4个Cache块。对于内存的第12号块,因为12除以4余数为0,所以既可以把第12号块放到Cache第0路的第0号位置(即Cache的第0号位置),也可以放到第1路的第0号位置(即Cache的第1号位置)
高速缓存关于读的操作非常简单,但是写就要复杂一点。假设我们写一个已经缓存了的字w,在高速缓存更新了它的w的副本之后,怎么更新w在接下来底层的副本呢?一种最简单的方法是直写,就是立即将w的高速缓存块写回到紧接着的低一层中。这样简单,但是每次写都要引起总线流量。另一种是写回,这种方法尽可能的推迟更新,只有当替换算法要驱逐这个更新过的块时,才把他写回到低一层缓存中,但是这样存在Cache一致性问题。
在处理写不命中时,一种方法是写分配,加载相应的低一层中的块到高速缓存中,再更新这个高速缓存块,这个方法缺点是每次不命中都会导致一个块从低一层传送到高速缓存。另一个方法是非写分配,避开高速缓存,直接把这个字写到低一层中。
高速缓存既能存数据,也能存指令。只保存指令的称为 i-cache,只保存程序的称为d-cache。两者都保存的称为统一高速缓存。下图给出了 Intel Core i7处理器的高速缓存层次结构。每个CPU芯片有四个核。每个核有自己私有的L1 i-cache,L1 d-cache和L2统一的高速缓存。所有的核共享片上L3统一高速缓存。
基本存储技术包括RAM、ROM和磁盘。RAM分为SRAM和DRAM,SRAM更快也更贵,可以做高速缓存;DRAM更慢也更便宜,可以做主存和图像缓冲区。掉电后ROM保持数据,可以存储固件。旋转磁盘可以保存大量数据,但是访问时间比DRAM长很多;SSD正在逐步替代旋转磁盘。
系统为了弥补不同存储设备的访问速度,设计了存储器层次结构,较小、较快的设备在顶部,较大、较慢的设备在底部。可以通过编写具有良好空间和时间局部性的程序来显著提高程序性能。利用cache特别重要,从cache存取数据的程序(局部性好)比直接从内存取数据的程序(局部性差)运行得快很多。
在程序中利用局部性