CSAPP
第6章 存储器层次结构
6.1 存储技术
6.1.1 随机访问存储器
**随机访问存储器(RAM)**分为两类: 静态的和动态的
静态RAM(SRAM)比动态RAM(DRAM)速度更快, 价格也更贵
- 静态RAM
SRAM将每个位存储在一个双稳态的存储器单元里, 每个单元是由一个六晶体管电路组成的. 这种电路有一种属性: 它可以无限期地保持在两个不同的电压配置或状态之一, 其他任何状态都是不稳定的
由于SRAM存储器单元的双稳态特性, 只要有电, 它就会永远地保持它的值
- 动态RAM
DRAM将每个位存储为对一个电容的充电. 与SRAM不同, DRAM存储单元对干扰非常敏感, 当电容的电压被扰乱之后, 它就永远不会恢复了
有很多原因导致DRAM漏电, 使得DRAM单元在10~100毫秒时间内失去电荷。计算机系统必须周期性地通过读出, 然后重写来刷新内存每一位
- 传统DRAM
DRAM芯片中的单元位被分为 d d d 个超单元,每个超单元都由 w w w 个DRAM单元组成. 一个 d × w d \times w d×w 的DRAM总共存储了 d w dw dw 位信息. 每个超单元有形如 ( i , j ) (i,j) (i,j) 的地址, 这里 i i i 表示行, j j j 表示列
信息通过称为 引脚 的外部连接器流入和流出芯片
每个DRAM芯片被连接到某个称为 内存控制器 的电路, 这个电路一次可以传送 w w w 位到某个DRAM芯片或者从某个DRAM芯片传出 w w w 位
为了读出超单元 ( i , j ) (i, j) (i,j) 的内容, 内存控制器将行地址 i i i 发送到DRAM, 然后是列地址 j j j . DRAM将超单元 ( i , j ) (i, j) (i,j) 的内容发回给控制器作为响应
将DRAM组织成二维阵列而不是线性数组的一个原因是降低芯片上地址引脚的数量. 二维阵列组织的缺点是必须分两步发送地址, 这增加了访问时间
- 内存模块
DRAM芯片封装在 内存模块 中, 它插在主板的扩展槽中
要取出内存地址 A A A 的一个字, 内存控制器将地址 A A A 转换为一个超单元地址 ( i , j ) (i, j) (i,j) , 并将它发送到内存模块, 内存模块再将 i i i 和 j j j 广播到每个DRAM. 模块中的电路收集这些输出, 并把它们合并成一个64位字, 再返回给内存控制器
- 增强的DRAM
- 快页模式DRAM
- 扩展数据输出DRAM
- 同步DRAM
- 双倍速率同步DRAM
- 视频RAM
- 访问主存
数据流通过 总线 的共享电子电路在处理器和DRAM主存之间来来回回
读事务从主存传送数据到CPU
写事务从CPU传送数据到主存
总线是一组并行的导线, 能携带地址, 数据, 控制信号. 取决于总线的设计, 数据和地址信号可以共享同一组总线, 也可以使用不同的
上图展示了计算机系统的配置, 主要的部件有CPU芯片, I/O桥芯片组, 主存
系统总线 连接CPU和I/O桥芯片组
内存总线 连接I/O桥芯片组和主存
- CPU从主存中读数据: 首先, CPU将地址 A A A 放到系统总线上, I/O桥将信号传递到内存总线; 接下来, 主存感知到内存总线上的地址信号, 从内存总线取地址, 从DRAM取出数据字, 再将数据写入内存总线; I/O桥将内存总线信号翻译成系统总线信号, 沿着系统总线传递信号; 最后, CPU感知到系统总线上的数据, 从总线上读数据, 并将数据写入到寄存器
- CPU向主存写数据: 首先, CPU将地址放到系统总线上; 内存从内存总线上读出地址, 并等待数据到达; 接下来, CPU将寄存器中的数据字复制到系统总线; 最后, 主存从内存总线中读出数据字, 并且将这些位存储在DRAM中
6.1.2 磁盘存储
磁盘 是广为应用的保存大量数据的存储设备, 磁盘读数据比DRAM慢了10万倍, 比SRAM慢了100万倍
注: 现在在家用机中磁盘使用较少, 大部分都改用了SSD
- 磁盘构造
磁盘是由 盘片 构成的, 每个盘片有两面称为 表面 , 表面覆盖着磁性记录材料. 中央有一个可以旋转的 主轴 , 使得盘片以固定的 旋转速率 旋转, 每个表面是由一组称为 磁道 的同心圆组成的, 每个磁道被划分为一组 扇区 , 扇区之间有一些间隙分隔开, 这些间隙不存储数据位, 只存储标识扇区的格式化位
磁盘是由一个或多个叠放在一起的盘片组成的, 它们被封装在一个封闭的包装里, 整个装置称为磁盘驱动器或磁盘
- 磁盘容量
一个磁盘可以记录的最大位数称为它的 最大容量 , 简称 容量
- 记录密度 : 磁道一英寸的段中可以放入的位数
- 磁道密度 : 从盘片中心出发半径一英寸的段内可以有的磁道数
- 面密度 : 记录密度和磁道密度的乘积
现代大容量磁盘使用 多区记录 的技术, 在这种技术中, 柱面的集合被分割成不相交的子集合, 被称为 记录区 , 每个区包含一组连续的柱面, 一个区中的每个柱面中的每条磁道都有相同数量的扇区, 这个扇区的数量是由该区中最里面的磁道所能包含的扇区数决定的
磁盘容量 = 字节数 扇区 × 平均扇区数 磁道 × 磁道数 表面 × 表面数 盘片 × 盘片数 磁盘 磁盘容量 = \frac{字节数}{扇区}\times\frac{平均扇区数}{磁道}\times\frac{磁道数}{表面}\times\frac{表面数}{盘片}\times\frac{盘片数}{磁盘} 磁盘容量=扇区字节数×磁道平均扇区数×表面磁道数×盘片表面数×磁盘盘片数
制造商是以千兆字节(GB)或兆兆字节(TB)为单位表示磁盘容量的
对于DRAM或SRAM容量相关的计量单位, 通常 K = 2 10 2^{10} 210 , M = 2 20 2^{20} 220 , G = 2 30 2^{30} 230 , T = 2 40 2^{40} 240
对于磁盘或网络一类的I/O设备, 通常 K = 1 0 3 10^3 103 , M = 1 0 6 10^6 106 , G = 1 0 9 10^9 109 , T = 1 0 12 10^{12} 1012
- 磁盘操作
磁盘用 读写头 读写存储在磁性表面的位, 而读写头连接到一个传动臂一端
通过沿着半径轴前后移动传动臂, 驱动器可以将读写头定位到盘面的任何一个磁道上, 这样的机械运动称为 寻道
读写头可以感知到这个位的值, 也可以修改这个位的值
读写头垂直排列, 一致行动, 在任何时候, 所有的读写头都在一个柱面上
盘片上任何一粒灰尘对于读写头来讲都像是一块巨石, 如果读写头碰到灰尘, 就会停下来撞上盘片, 导致盘片损伤, 这就是 读写头冲撞
磁盘以扇区为大小的块来读写数据, 访问时间有三个主要的部分: 寻道时间, 旋转时间, 传送时间
- 寻道时间 : 为了读取某个目标扇区的内容, 传动臂首先将读写头定位到包含目标扇区的磁道上, 移动传动臂所需要的时间称为寻道时间, 寻道时间 T s e e k T_{seek} Tseek 取决于读写头以前的位置和传动臂在盘面上移动的速度
- 旋转时间 : 一旦读写头定位到期望的磁道, 驱动器等待目标扇区的第一个位置转到读写头下, 取决于读写头到达目标扇区时盘面的位置和盘面的旋转速度. 最大旋转延迟: T m a x r o t a i o n = 1 R P M × 60 s 1 m i n T_{max\ rotaion} = \frac{1}{RPM} \times \frac{60s}{1min} Tmax rotaion=RPM1×1min60s , 平均旋转时间 T a v g r o t a t i o n T_{avg\ rotation} Tavg rotation 是 T m a x r o t a t i o n T_{max\ rotation} Tmax rotation 的一半
- 传送时间 : 当目标扇区的第一个位位于读写头下时, 驱动器就可以开始读或者写该扇区的内容了, 取决于旋转速度和每条磁道的扇区数目, 一个扇区以秒为单位的平均传送时间为: T a v g t r a n s f e r = 1 R P M × 1 ( 平均扇区数 / 磁道 ) × 60 s 1 m i n T_{avg\ transfer} = \frac{1}{RPM} \times \frac{1}{(平均扇区数/磁道)} \times \frac{60s}{1min} Tavg transfer=RPM1×(平均扇区数/磁道)1×1min60s
访问一个磁盘扇区的时间主要是寻道时间和旋转延迟
- 逻辑磁盘块
现代磁盘将盘面的构造呈现为一个简单的视图, 一个 B B B 个扇区大小的逻辑块的序列, 编号为 0 , 1 , ⋯ , B − 1 0, 1, \cdots , B- 1 0,1,⋯,B−1.
磁盘封装中有一个小的硬盘/固件设备, 称为 磁盘控制器 , 维护着逻辑块号和实际磁盘扇区之间的映射关系
当操作系统想要执行一个I/O操作时, 例如读一个磁盘扇区的数据到主存, 操作系统会发一个命令到磁盘控制器, 让它读某个逻辑块号, 控制器上的固件执行快速表查找, 将一个逻辑块号翻译成一个(盘面, 磁道, 扇区)的三元组. 控制器上的硬件会解释这个三元组, 将读写头移动到适当的柱面, 等待扇区移动到读写头下, 将读写头感知到的位放在控制器的小缓冲区, 然后将它们复制到主存中
- 访问磁盘
假设磁盘控制器映射到端口0xa0
- 硬盘读: CPU执行三个对地址0xa0的存储指令, 发起硬盘读: 第一条指令是发送一个命令字, 告诉磁盘发起一个读, 同时还发送了其他的参数, 例如读完成时是否应该中断CPU, 第二条指令指明了应该读的逻辑块号, 第三条指令指明了应该存储在磁盘扇区内容的主存地址; 当磁盘控制器收到来自CPU的读指令之后, 它将逻辑块号翻译成一个扇区地址, 读该扇区的内容, 然后这些内容被直接传送到主存, 不需要CPU的干涉, 这个过程称为 直接内存访问 , 数据传送称为 DMA数据传送
6.1.3 固态硬盘
一个SSD封装由一个或多个闪存芯片和 闪存翻译层 组成, 闪存芯片替代磁盘中的机械驱动器, 闪存翻译层替代磁盘控制器
一个闪存由
B
B
B 个块的序列组成, 每个块由
P
P
P 页组成
数据是以页为单位读写的, 只有在一页所属的块整个被 擦除 之后, 才能写这一页
一旦一个块被擦除之后, 块中的每一个页都可以不需要再擦除就写一次
一旦一个块被磨损坏之后, 就不能再使用了
固态硬盘的随机访问时间比机械硬盘更短, 能耗更低, 同时更结实
因为闪存块会磨损, 所以闪存翻译层中的 平均磨损 逻辑试图通过将擦除平均分布到所有的块上来最大化每个块的寿命
6.1.4 存储技术趋势
- 不同的存储技术有不同的价格和性能折中
- 不同存储技术的价格和性能属性以截然不同的速率变化着
- DRAM和磁盘的性能滞后于CPU的性能
6.2 局部性
局部性一般分为时间局部性和空间局部性
在一个具有良好时间局部性的程序中, 被引用过一次的内存位置很可能在将来不久远的时间内再次被引用
在一个具有良好空间局部性的程序中, 如果一个内存位置被引用了一次, 那么程序很可能在不远的将来引用附近的一个内存位置
有良好局部性的程序比局部性差的程序运行得更快
6.3 存储器层次结构
上图为存储器的层次结构
越往上的存储器, 速度越快, 容量越小, 价格越贵
越往下的存储器, 速度越慢, 容量越大, 价格越便宜
6.3.1 存储器层次结构中的缓存
高速缓存 是一个小而快的存储设备, 它作为存储在更大更慢的设备中数据对象的缓冲区域
使用高速缓存的过程称为 缓存
存储器层次结构的核心思想是, 对于每个 k k k , 位于 k k k 层的更快更小的存储设备作为位于 k + 1 k + 1 k+1 层的更大更慢的存储设备的缓存
第 k + 1 k + 1 k+1 层的存储器被划分为连续的数据对象组块, 称为 块 , 每个块都有唯一的地址或名字, 使之区别其他的块
第 k k k 层的存储器被划分为较少的块的集合, 每个块的大小与 k + 1 k + 1 k+1 层的块大小一样.
数据总是以块大小为 传送单元 在第 k k k 层和第 k + 1 k+1 k+1 层之间来回复制的
- 缓存命中
当程序需要第 k + 1 k+1 k+1 层中的某个数据对象 d d d 时, 它首先在当前存储在第 k k k 层的一个块中查找 d d d . 如果 d d d 刚好缓存在第 k k k 层中, 那么就是我们所说的 缓存命中
- 缓存不命中
如果第 k k k 层中没有缓存数据对象 d d d , 那么就是 缓存不命中 . 当发生缓存不命中时, 第 k k k 层的缓存从第 k + 1 k+1 k+1 层的缓存中取出包含 d d d 的那个块, 如果第 k k k 层的缓存已经满了, 可能就会覆盖现有的一个块
覆盖一个现有块的过程称为 替换 或 驱逐 这个块, 被驱逐的这个块有时也称为 牺牲块 . 决定替换哪个块是由缓存的 替换策略 决定的
- 缓存不命中的种类
空的缓存被称为 冷缓存 , 此类不命中称为 强制性不命中 或 冷不命中
将 第 k + 1 k + 1 k+1 层的某个块限制放置在第 k k k 层块的一个小的子集中, 这类称为 冲突不命中 , 这种情况下, 缓存足够大, 能够保持被引用的数据对象, 但是因为这些对象会映射到同一个缓存块, 缓存会一直不命中
当工作集的大小超过缓存的大小时, 缓存会经历 容量不命中
6.4 高速缓存存储器
为了减少CPU和主存之间的差距, 设计者在CPU和主存之间插入了一个小的SRAM高速缓存存储器, 称为 L1高速缓存, L1高速缓存的访问速度和寄存器相当
在L1高速缓存和主存之间又插入了L2高速缓存和L3高速缓存, L2高速缓存大概在10个时钟周期可以访问到它, L3则需要50个时钟周期
6.4.1 通用的高速缓存存储器结构
一个及其的高速缓存被组织为一个有 S = 2 s S = 2^s S=2s 个 高速缓存组 的数组. 每个组包括 E E E 个高速缓存行 . 每个行是由 B = 2 b B = 2^b B=2b 字节的数据块组成的
有效位 指明了这个行是否包含了有意义的信息
高速缓存的大小 C C C 指的是所有块的大小的总和. 标记位和有效位不包括在内. 因此, C = S × E × B C = S \times E \times B C=S×E×B
当一条加载指令指示CPU从主存地址 A A A 读一个字时, 它将地址 A A A 发送到高速缓存. 如果高速缓存正保存着地址 A A A 处那个字的副本, 就立即将这个字发回给CPU
组索引位被解释为一个无符号整数, 它告诉我们这个字必须存储在哪个组中. 一旦我们知道了这个字必须放在哪个组中, A A A 中的 t t t 个标记位就告诉我们这个组的哪一行包含这个字
一旦我们在由组索引标识的组中定位了标号所标识的行, 那么 b个块偏移位 给出了在 B B B 个字节的数据块中的字偏移
6.4.2 直接映射高速缓存
每个组只有一行( E = 1 E= 1 E=1) 的高速缓存被称为 直接映射高速缓存
当CPU执行一条读内存字 w w w 的指令, 它向L1高速缓存请求这个字, 如果L1有 w w w 的一个副本, 那么就得到L1的高速缓存命中, 高速缓存抽取 w w w 这个字并且返回给CPU
高速缓存确定一个字是否命中, 然后抽取被请求的字的过程分为三步: (1) 组选择 (2) 行匹配 (3) 字抽取
- 组选择
高速缓存从 w w w 的地址中抽取出 s s s 个组索引位, 这些位被解释为一个对应于一个组号的无符号整数
- 行匹配
选中的组中只有一个高速缓存行, 并且这个行的有效位已经设定了. 因为高速缓存行中的标记位与地址中的标记位相匹配, 所以我们知道我们想要的那个字确实存在在这个行中, 也就是缓存命中
如果有效位没有设置或者标记位不匹配, 那就是缓存不命中
- 字选择
把块看成一个字节的数组, 而字节偏移是指到这个数组的索引.
- 不命中时的行替换
如果缓存不命中, 那么它需要从存储器层次结构中的下一层取出被请求的块, 替换策略是: 用新取出的行替换当前的行
6.4.3 组相联高速缓存
组相联高速缓存 内每个组都保存有多于一个的高速缓存行
一个 1 < E < C / B 1 < E < C/B 1<E<C/B 的高速缓存通常被称为 E E E 路组相联高速缓存
- 组选择
和直接映射高速缓存一样, 组索引位标识组
- 行匹配和字选择
相联存储器 是一个(key, value)对的数组, 以key为输入, 返回于key相匹配的一个value值
- 不命中时的行替换
如果CPU请求的字不在组中的任意一行, 那么就是缓存不命中, 高速缓存必须从内存中取出包含这个字的块, 如果该组内没有空行, 那么必须选择一个非空行进行替换, 常用的决策算法包括LRU(Least-Recently-Used)和LFU(Least-Frequently-Used)算法
6.4.4 全相联高速缓存
全相联高速缓存 是由一个包含所有高速缓存行的组 (即 E = C / B E = C/B E=C/B) 组成的
- 组选择
全相联高速缓存只有一个组. 地址中没有组索引位, 地址只被划分为一个标记和一个块偏移
- 行匹配和字选择
全相联高速缓存中的行匹配和字选择与组相联高速缓存中的是一样的, 区别在于规模大小的问题
6.4.5 关于写的问题
写一个已经缓存了的字 w w w (写命中), 最简单的方法是 直写 , 就是立即将 w w w 的高速缓存块协会到紧接着的下一层中, 它的缺点就是每次写都会引起总线流量.
另一种方法称为 写回 , 尽可能地延迟更新, 只有当替换算法要驱逐这个更新过的块时, 才把它写到紧接着的下一层中, 它的缺点是增加了复杂性, 高速缓存必须维护一个额外的修改位, 表明这个高速缓存块是否被修改过
另一个问题是如何处理 写不命中 , 一个方法称为 写分配 , 加载相应的低一层中的块到高速缓存中, 然后更新这个高速缓存块. 缺点是每次不命中都会导致一个块从低一层传递到高速缓存
另一个方法是 非写分配 , 避开高速缓存, 直接把这个字写到低一层
6.4.7 高速缓存的性能影响
- 不命中率 : 在一个程序执行或者程序的一部分执行过程中, 内存引用不命中的比率
- 命中率 : 命中的内存引用比率
- 命中时间 : 从高速缓存传送一个字到CPU所需要的时间, 包括组选择, 行匹配, 字选择的时间
- 不命中处罚 : 由于不命中所需要的额外时间
- 较大的高速缓存可能会增加命中时间
- 较大的块能够利用程序中可能存在的空间局部性, 帮助提高命中率, 但是块越大就意味着高速缓存行数越少, 这会损害时间局部性比空间局部性更好的程序中的命中率
- 较高的相联度(也就是 E E E 的值较大)的优点是降低了高速缓存由于冲突不命中出现抖动的可能性, 但是较高的相连度实现起来很昂贵, 而且很难使之速度变快
6.5 编写高速缓存友好的代码
确保代码高速缓存友好的代码:
- 让常见的情况运行得块
- 尽量减少每个循环内部的缓存不命中数量
6.6 高速缓存对程序性能的影响
一个程序从存储系统中读数据的速率称为 读吞吐量 , 或者有时称为 读带宽 . 如果一个程序在 s s s 秒的时间段内读 n n n 个字节, 那么这段时间内的读吞吐量就为 s / n s/n s/n
存储器山见CSAPP第三版P445页或第三版封面
- 将注意力放到内循环, 大部分的计算和内存访问都出自这里
- 通过按照数据对象存储在内存中的顺序, 以步长为1的来读数据, 从而使得程序的空间局部性最大
- 一旦从存储器中读取了一个数据对象, 就尽可能地使用它, 从而使得程序的时间局部性最大