深入理解计算机系统:存储器层次结构

存储技术

随机访问存储器(RAM)

RAM分为两类:静态的和动态的。静态RAM(SRAM)比动态RAM(DRAM)更快,但也贵得多。一个系统的SRAM不会超过几兆字节,但是DRAM却有几百或几千兆字节。

SRAM

SRAM将每个位存储在一个双稳态的存储器单元里,每个单元是用一个六晶体管电路来实现的。这个电路有这样一个属性:它可以无限期地保持在两个不同的电压状态之一。其他任何状态都是不稳定的,电路会迅速转移到两个稳定状态中的一个。
由于SRAM的双稳态特性,只要有电,它就会永远地保持它的值。
SRAM对干扰不敏感。

DRAM

DRAM将每个位存储为对一个电容的充电。和SRAM不同,DRAM单元对干扰非常敏感。
存储器系统必须周期性地通过读出,然后重写来刷新存储器的每一位。

非易失性存储器

如果断电,DRAM和SRAM会丢失它们的信息,从这个意义上说,他们是易失的。非易失性存储器即使在断电后仍然保存着他们的信息。
可编程ROM(PROM)只能被编程一次。PROM的每个存储器单元有一种熔丝,他只能用高电流熔断一次。
可擦写可编程ROM(EPROM)和电子可擦除PROM(EEPROM)可被多次编程。
闪存(flash)基于EEPROM,也是一类非易失性存储器。
存储在ROM设备中的程序称为固件

访问主存

数据流通过总线在处理器和DRAM主存之间来回。总线是一组并行的数据线,能携带地址、数据和控制信号。

存储技术趋势

  1. 不同的存储技术有不同的价格和性能折中。
  2. 不同存储技术的价格和性能属性以截然不同的速率变化着。增加密度比降低访问时间更容易。
  3. DRAM和磁盘的性能滞后于CPU的性能。

局部性

局部性是指:一个编写良好的计算机程序倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。
局部性通常有时间局部性和空间局部性。
在一个具有良好时间局部性的程序中,被引用过一次的存储器位置可能在不远的将来再被多次引用。
在一个具有良好空间局部性的程序中,如果一个存储器位置被引用了一次,那么程序很可能在不远的将来引用附近的一个存储器位置。

存储器层级结构

高速缓存

高速缓存(cache)是一个小而快速的存储设备,它作为存储在更大、也更慢的设备中的数据对象的缓冲区域。

第k+1层的存储器被划分成连续的数据块,每个块有唯一的标识。
第k层的存储器被划分成较少的块,每个块的大小与第k层的块的大小一样。在任何时刻,第k层的缓存包含第k+1层块的一个子集的拷贝。
数据总是以块为传送单元,在第k层和第k+1层之间来回拷贝的。

缓存命中

当程序需要第k+1层的某个数据对象d时,它首先在当前第k层的一个块中查找d。如果d刚好缓存在第k层中,那么就是缓存命中。该程序直接从第k层读取d。

缓存不命中

另一方面,如果第k层中没有缓存数据对象d,那么就是我们所说的缓存不命中。此时,第k层的缓存从第k+1层缓存中取出包含d的那个块,如果第k层已满,可能就会覆盖现存的一个块。
覆盖一个现存的块的过程称为替换驱逐这个块。被驱逐的块有时也称为牺牲块,决定该替换哪个块是由缓存的替换策略控制的。

缓存不命中的种类

区分不同种类的缓存不命中有时候是很有帮助的。
如果第k层是空的,那么对任何数据对象的访问都会不命中,空缓存称为冷缓存,此类不命中称为强制性不命中或冷不命中
只要发生了不命中,第k层的缓存就必须执行某个放置策略,确定把它从第k+1层中取出的块放到哪里。对于接近CPU的中高层缓存,它们是用硬件实现的,通常使用更严格的放置策略:将第k+1层的某个块限制放置在第k层块的一个小子集中。例如:第k+1层的块i必须放在第k层的块(i % 4)中。
然而,这种限制性的放置策略会引起冲突不命中,因为有些对象会映射到同一个缓存块。
一个嵌套循环可能会反复访问同一个数组的元素,这个块的集合称为工作集,如果工作集的大小超过了缓存的大小,就会发生容量不命中

缓存管理

存储器层次结构的本质是:每一层存储设备都是较低一层的缓存。缓存管理的逻辑实现可以是硬件、软件,或是两者的结合。缓存管理负责将缓存划分成块,在不同的层之间传送块,并判断是否命中。

高速缓存存储器cache

早期的计算机系统的存储器层次结构只有三层:CPU寄存器、DRAM主存储器和磁盘存储。但由于CPU和主存之间逐渐拉大的差距,在CPU寄存器和主存之间插入了SRAM高速缓存存储器,称为L1高速缓存(一级缓存),后来又加入了L2高速缓存、L3高速缓存。

通用的cache结构

每个存储器地址有m位,形成M=2^m个不同的地址,它被组织成一个有S=2^s个cache组的数组,每个组包含E个cache行。每个行包含一个B=2^b字节的数据块,一个有效位来指明该行是否包含有意义的信息,还有t=m-(b+s)个标记位。它们唯一标识存储在该cache行中的块。

一般而言,高速缓存的结构可以用元祖(S,E,B,m)来描述,高速缓存的容量C指的是所有块的大小之和,标记位和有效位不包括在内,因此,C=S*E*B
深入理解计算机系统:存储器层次结构_第1张图片
当一个加载指令指示CPU从主存地址A中读一个字时,它将地址A发送到cache,如果cache正保存着地址A处那个字的拷贝,它就立即将那个字发回给CPU。那么cache如何知道它是否包含地址A那个字的拷贝的呢?

cache的结构使得它能使用极其简单的hash函数的hash表:
参数S和B将m个地址分为三个字段,A中s个组索引位是一个到S个组的数组的索引,它告诉我们这个字必须存储在哪个组中。然后,A中t个标记位就指示了在这个组中的哪个行包含这个字。当且仅当设置了有效位并且该行的标记位与地址A中的标记位相匹配时,组中的这一行才包含这个字。一旦我们在由组索引标识的组中定位了由标记所指示的行,那么b个块偏移位就给出了在B个字节的数据块中的字偏移。

参数 描述
S=2^s 组数
E 每个组的行数
B=2^b 块大小(字节)
m=log2(M) (主存)物理地址位数
M=2^m 存储器地址的最大数量
s=log2(S) 组索引位数量
b=log2(B) 块偏移位数量
t=m-(s+b) 标记位数量
C=S*E*B 不包括有效位和标记位的高速缓存大小(字节)

直接映射cache

根据每个组的高速缓存行数E,高速缓存被分为不同的类。每个组只有一行的cache称为直接映射cache。
cache确定一个来自CPU的请求是否命中,然后抽取被请求的字的过程,分为三步:1)组选择,2)行匹配,3)字抽取。

组选择

在这一步中,cache从地址中抽取s个组索引位,这些位被解释成一个对应于一个组号的无符号整数。这个组索引位就是以cache组为元素的一维数组的索引。

行匹配

在直接映射cache中,每个组只有一行,当且仅当设置了有效位,且行标记和地址A中的标记相匹配时,这一行中包含了A的一个拷贝。
另一方面,如果有效位没有设置,或者标记不相匹配,那么我们就出现了缓存不命中。

字选择

一旦命中,我们就知道A就在这个块中的某个地方。最后一步确定所需要的字在块中的哪里。块偏移位提供了所需要的字的第一个字节的偏移。
我们把cache块看成一个字节的数组,而字节偏移是到这个数组的一个索引。

不命中时的行替换

如果缓存不命中,那么它需要从存储器层次结构中的下一层取出被请求的块,然后将新的块存储在索引位指示的组中的一个高速缓存行中。如果当前cache组的有效cache行已满,那么必须驱逐一个现存行。由于对直接命中cache来说,每个组只包含有一行,所以直接用新取出的行替换当前的行。

组相连cache

组相连cache的每个组保存有多于一个的高速缓存行,一个1的cache通常称为E路组相连高速缓存。

组相联高速缓存中的组选择

它的组选择和直接映射高速缓存的组选择一样,组索引位标识组

组相联高速缓存中的行匹配和字选择

相比直接映射高速缓存,组相联高速缓存的行匹配稍复杂一些,因为它必须检查多个行的标记位和有效位,以确定所请求的字是否在集合中。我们可以将组相联存储器看做是对的数组,以key为输入,返回与输入的key相匹配的(key,value)对中的value值。在这里key是标记和有效位,而value是块的内容。

组相联高速缓存中行匹配的基本思想就是组中的任何一行都可以包含任何映射到这个组的存储器块。所以高速缓存必须搜索组中的每一行,寻找一个有效的行,其标记和地址中的标记相匹配。如果高速缓存找到了这样一行,那么我们就命中,块偏移从这个块中选择一个字。

组相联高速缓存中不命中时的行替换

如果CPU请求的字不在组的任何一行中,那么就是缓存不命中,高速缓存会从存储器中取出包含这个字的块。那么,一旦高速缓存取出了这个块,该替换哪一行呢?当然,如果有一个空行,那它就是个很好的选择,但如果没有空行,那么就必须选择一个非空行,这样的选择会基于一种高速缓存替换策略,如最不常用(LFU)策略,最近最少使用(LRU)策略。所有这些策略都需要额外的时间和硬件,但是,越往存储器层次结构的下层、远离CPU,一次不命中的开销就会更加昂贵,用更好的替换策略使得不命中最少也变得更加划算。

全相联高速缓存

全相联高速缓存是由一个包含所有高速缓存行的组(即E=C/B)组成的。其结构如下:

全相联高速缓存中的组选择

全相联高速缓存中的组选择非常简单,因为只有一个组,所以对应的地址没有组索引位,地址只被划分成了一个标记和一块偏移。

全相联高速缓存中的行匹配和字选择

全相联高速缓存中的行匹配和子选择与组相联高速缓存中的是一样的,它们之间的区别主要是规模大小的问题。因为高速缓存电路必须并行地搜索许多相匹配的标记,构造一个又大又快的相联高速缓存很困难,而且很昂贵。因此,全相联高速缓存只适合做小的高速缓存,例如虚拟存储系统中的翻译备用缓冲器(LTB),它缓存页表项。

有关写的问题

高速缓存对于读的操作很简单。首先,在高速缓存中查找所需字w的拷贝。如果命中,立即返回字w给CPU。如果不命中,从存储器层次结构中较低层中取出包含字w的块,将这个块存储到某个高速缓存行中(可能会驱逐一个有效的行),然后返回字w。

那么对于写的情况呢?假设我们要写一个已经缓存了的字w**写命中。在高速缓存中更新了它的w拷贝之后,怎么更新w在层次结构中低一层中的拷贝呢?最简单的方法,称为直写**,就是立即将w的高速缓存块写回到紧接着的低一层中。虽然简单,但是直写的缺点每次写都会引起总线流量。另一种方法,称为写回。尽可能地推迟存储器更新,只有当替换算法要驱逐更新过的块时,才把它写到紧接着的低一层中。由于局部性,写回能显著减少总线流量,但是缺点是增加了复杂性。高速缓存必须为每个高速缓存行维护一个修改位,表明这个高速缓存块是否被修改过。

但如果写不命中呢?一种方法称为写分配,加载相应的低一层的块到高速缓存中,然后更新这个高速缓存块。写分配试图利用写的空间局部性,但是缺点是每次不命中都会导致一个块从低一层传送到高速缓存。另一种方法,称为非写分配,避开高速缓存,直接把这个字写到低一层中。直写高速缓存通常是非写分配的。写回高速缓存通常是写分配的。

你可能感兴趣的:(深入理解计算机系统:存储器层次结构)