【CSAPP笔记】11. 存储器层次结构

在没有专门研究存储器系统之前,我们依赖的存储器模型是一个很简单的概念,也就是把它看成一个线性数组,CPU 能在一个常数时间内访问任何一个存储器位置。虽然在研究别的问题时,这是一个有效的模型,但是它不能反映存储器系统的实际工作方式。

存储器系统(memory system)是一个具有不同容量、成本、访问时间的存储器层次结构。CPU 寄存器保存着最常用的数据;靠近 CPU 的小的、快速的高速缓存存储器(cache)作为一部分存储在相对慢速的主存储器(简称主存)中的数据;主存用来暂时存放存储在容量大、慢速磁盘上的数据;磁盘又作为通过网络连接的其他机器的磁盘上的数据的缓冲区域。

存储器层次结构是可行的,因为一个编写良好的程序总是倾向于更频繁访问某一层次上的存储设备。所以,下层的存储设备可以更慢速一点,也因此可以更大、更便宜。

计算机程序有一个重要的基本属性,叫做局部性原理(locality)。具有良好局部性原理的程序比局部性差的程序更多地倾向于在存储器层次结构中的较高层次处访问数据,因此运行的更快。

作为一个程序员,你应该理解存储器系统,因为它对应用程序的性能有着巨大的影响。如果了解系统是如何将数据在存储器层次中上上下下移动的,那么你就可以编写你的应用程序,使数据尽量存储在层次较高的地方,CPU 能更快得访问到它们。

计算机技术的成功很大程度上源于存储技术的巨大进步。

随机访问存储器

随机存取存储器(RAM, Random-Access Memory),有两种类型,第一种叫做静态 RAM(static RAM)。SRAM 将每个位存储为一个双稳态(bistable)的存储器单元里,每个单元用一个六晶体管电路来实现。这种电路有一种限制,就是可以无限期保持两种不同的电压状态之一。由于其双稳态特性,只要有电,它就会永远保持它的值。

动态 RAM(Dynamic RAM),将每个位存储为对一个电容的充电。DRAM 存储器可以制造得非常密集,每个单元由一个电容和访问晶体管组成。与 SRAM 不同,DRAM 对干扰非常敏感,且被干扰后永远无法恢复。存储器系统必须周期性地通过读出,重写来刷新存储器的每一位。

SRAM 是只要有供电,那它就会保持不变,不需要刷新。SRAM 速度比 DRAM 快,对干扰不敏感,但也贵得多。SRAM 用来作为高速缓存存储器,DRAM 用来作为主存以及图形系统的帧缓冲区。

非易失性存储器

如果断点,那么 DRAM 和 SRAM 会丢失信息,所以他们是易失(volatile——的。非易失性存储器(nonvolatile memory)即使是在关电后,依然保持它们的信息。如果想让数据持续保持,要考虑使用非易失性存储器。PROM(Progrommable ROM)可编程只读存储器,只能被编程一次。可擦写可编程 ROM(Rrasable Progrommable ROM,ERPOM)有一个透明的石英窗口,允许光透过,照射存储单元。紫外线照射,这个单元就被清除。对 ERPOM 的编程是通过一种特殊设备完成。ERPOM 的擦写和重编程的次数可以达到1000次。电子可擦除 ROM(Electrically Erasable ROM,EEPROM)可以在印制电路卡上编程。闪存(flash memory)是一种基于 EERPOM 的非易失性存储器。它是一种重要的存储技术,为大量电子设备提供快速、持久的非易失性存储,例如数码相机、手机、笔记本、台式机、音乐播放器等。

访问主存——总线

数据流通过称为总线(bus)的共享电子电路在处理器和 DRAM 主存中来来回回。每次 CPU 和主存之间的数据传送都是通过一系列步骤来完成的,这些步骤称为“总线事务”(bus transaction)。读事务从主存传送数据到 CPU,写事务从 CPU传送数据到主存。

总线是一组并行的导线,能携带信号、数据、地址。多个设备可以共享同一根总线。数据和地址信号可以共享一组线,也可以用不同的,取决于总线的设计。信号的作用是同步事务,标识当前正在被执行事务的类型。

总线配置:主要部件是 CPU 芯片、I/O 桥芯片组(包括存储控制器),DRAM 存储器模块。这些部件由总线连接起来。系统总线连接 CPU 和 I/O 桥。存储器总线连接 I/O 桥和主存。I/O 桥的作用是将系统总线的电子信号翻译成存储器总线的电子信号。总线设计是计算机系统中复杂而又变化迅速的方面。使用简单而抽象的模型是很有用的,可以掌握主题思想而不必与私有的设计细节绑得太紧。

假设 CPU 需要从硬盘中读取一些数据,就发起读事务,会给定指令,逻辑块编号和目标地址,并发送给存储器总线。主存感受到信号,就读地址,从 DRAM 中取出数据,写到存储器总线,然后信号沿着总线返回 CPU。假设 CPU 需要把寄存器的内容写到主存上,就发起写事务,CPU 还是先将地址放到系统总线上,存储器从存储器总线读出地址,然后等待数据到达。 CPU 将寄存器内容拷贝到系统总线上,最后主存从存储器总线读取这些数据,然后存储到 DRAM 中。

磁盘

磁盘(disk)是由盘片(platter)构成的,每个盘片有两个面,或者成为表面(surface),表面覆盖着磁性记录材料。盘片中央有一个可以旋转的主轴(spindle),它是的盘片以固定速度旋转,速度通常在5000 ~ 15000 转每分钟。磁盘通常包含一个或多个盘片,封装在一个密封的容器内,整个装置被称为磁盘驱动器(disk drive),简称为磁盘。

每个表面是由一组成为磁道(track)的同心圆组成的。每个磁道划分成一组扇区(sector)。每个扇区包含相等数量的数据位(比如 512 字节),数据就编码在扇区上的磁性材料中。扇区之间有一些间隙(gap)。术语柱面(cylinder)来描述多个盘片的驱动器,柱面是所有盘片表面上到主轴中心的距离相等的磁道的集合。

磁盘容量

容量是指磁盘可以记录的最大位数,由下面几个因素决定:

  • 记录密度(recording density,位/英寸):track 中 1 英寸能保存的字节数
  • 磁道密度(track density,道/英寸):1 英寸直径能保存多少条 track
  • 面密度(areal density,位/平方英寸):上面两个数值的乘积

$ 磁盘容量 = \frac{字节数}{扇区} × \frac{平均扇区数}{磁道} × \frac{磁道数}{表面} × \frac{表面数}{盘片} × \frac{盘片数}{磁盘} $

假设一个磁盘有 5 个盘片,每个扇区 512 个字节,每个面 20000 个磁道,每条磁道平均 300 个扇区。则磁盘容量是:

$ 磁盘容量 = \frac{512 字节}{扇区} × \frac{300 个扇区}{磁道} × \frac{20000 个磁道}{表面} × \frac{2 个表面}{盘片} × \frac{5 个盘片}{磁盘} = 30 720 000 000 字节 = 30.72 GB $

读写操作

磁盘用读/写头来读写存储在磁性表面的位,当磁道上的每个位通过它的下面时,读写头能够感知到这个位的值,也能修改这个位的值。读写头连接在一个传送臂(actuator arm)的一端。通过旋转,传动臂可以将读写头定位在磁盘上的任何磁道上,这个行为叫做寻道(seek)。在任何时刻,所有读写头位于同一柱面。

寻道时间:

$ T_{avgseek} $ 通常在 3 ~ 9 ms,一次最大的寻道时间 $ T_{maxseek} $ 可以达到 20 ms。

一旦读写头定位到了期望的磁道,下一个行动就是等待目标扇区的第一个位旋转到读写头下。最好情况下就是要读取的位刚好在读写头下,最差的情况就是需要旋转一整圈。

旋转时间:

$$ T_{avg rotation} = \frac{1}{2} × \frac{1}{RPM} × \frac{60 secs}{1 min} $$

RPM 是指:rotation per minute 每分钟旋转次数,RPM 的倒数就是 MPR ,即每转一圈需要的分钟数。乘以二分之一后转换单位即可。

当寻道和旋转都完成了之后,驱动器就可以开始读或写内容了。读写传送时间依赖于旋转速度和每条磁道的扇区数目。粗略估计一个扇区以秒为单位的平均传送时间如下:

$$ T_{avgtransfer} = \frac{1}{RPM} × \frac{1}{平均扇区数 / 磁道} × \frac{60 secs}{1 min} $$

传送时间 = 转一圈所需分钟数 × 一个扇区占一个磁道的占比

  • 访问磁盘扇区的主要时间是寻道时间和旋转时间。开始访问第一个字节要耗时很久,但访问剩下的字节几乎不需要时间。
  • 寻道时间和旋转延迟差不多。

例如有一个如下参数的磁盘

参数
旋转速率 7200 RPM
平均寻道时间 9ms
每条磁道的平均扇区数 400

平均旋转时间:

$$ T_{avgrotation} = \frac{1}{2} × \frac{1}{7200} = 4 ms $$

传送时间:

$$ T_{avg transfer} = \frac{1}{7200} × \frac{1}{400} = 0.02 ms $$

总时间为:

$$ T_{access} = T_{avgseek} + T_{avgrotation} + T_{avgtransfer} = 13.02 ms $$

固态硬盘

基于闪存的磁盘驱动器,称为固态硬盘(Solid State Disk,SSD),是替代传统磁盘的极有吸引力的替代产品。闪存翻译层(flash translation layer)是一个硬件设备,扮演与磁盘控制器相同的角色。闪存芯片替代传统磁盘中的机械驱动器。固态硬盘中分成很多的块(Block),每个块又有很多页(Page),大约 32-128 个,每个页可以存放一定数据(大概 4-512KB),页是进行数据读写的最小单位。对一个页进行写入操作的时候,需要先把整个块清空后才能写这一页,而一个块大概在 100,000 次写入之后就会报废。

与传统的机械硬盘相比,固态硬盘由半导体存储器构成,没有移动的部件,因此在在读写速度上有很大的优势,能耗低,更结实。SSD 也容易磨损。

程序的局部性原理

一个编写良好的计算机程序常常具有比较好的局部性(locality)

  • 时间局部性(Temporal Locality): 如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。程序循环、堆栈等是产生时间局部性的原因。
  • 空间局部性(Spatial Locality): 在最近的将来将用到的信息很可能与现在正在使用的信息在空间地址上是临近的。
  • 顺序局部性(Order Locality): 在典型程序中,除转移类指令外,大部分指令是顺序进行的。顺序执行和非顺序执行的比例大致是5:1。此外,对大型数组访问也是顺序的。指令的顺序执行、数组的连续存放等是产生顺序局部性的原因。

一般而言,具有良好的局部性的程序比局部性差的程序运行得更快。计算机系统的各个层次,硬件、操作系统、应用程序,都利用了局部性。

下面来看局部性原理在程序设计时的体现。我们来看看一个利用循环对数组求和的函数对于每个数据引用的模式

1
2
3
4
5
6
7
8
9
int sumv(int *v, int N)
{
int i, sum = 0;
for(i = 0; i < N; i++)
{
sum += v[i];
}
return sum;
}

因为数组中的元素是一个接一个访问的,因此有很好的空间局部性。由于每个元素只访问一次,因此时间局部性很差。对于循环体中的每个变量,函数要么有很好的空间局部性,要么有很好的时间局部性。因此这个函数有良好的局部性。

我们说像 sumv 函数这样访问数组的函数,具有 步长为 1的引用模式(stride-1 reference pattern)每隔 k 个元素进行访问,就叫 步长为 k 的引用模式。一般来说,随着步长的增加,空间局部性下降。对于多维数组来说,步长就是一个很重要的问题。

一些看上去很小的改动对程序的性能会有很大的影响。步长越小,空间局部性越好。如果在存储器中以大步长跳来跳去,空间局部性很差。了解了高速缓存的工作原理后,就能明白为什么具有良好局部性的程序通常比局部性差的程序运行得更快。

存储体系

存储技术和计算机软件的一些基本属性:

  1. 硬件存储技术:不同的存储技术的访问时间差异很大,速度快的技术成本较高,速度慢的技术容量较大。不同的存储技术有不同的价格和性能折中,也以不同的速度在变化。
  2. 软件:一个编写的好的计算机程序倾向于展现出良好的局部性。
  3. 硬件和软件的这些基本属性互相补充得很完美。它们这种补充性质让人们想到一种组织存储器系统的方法,就是存储器层次结构(memory hierarchy)。从高层往底层走,存储设备变得更慢,但也变得更便宜,容量更大。


参考链接