第六章 存储器层次结构
1. 随机访问存储器(RAM)
1. 静态RAM(SRAM)
常用来做cache。
SRAM存储器单元只要有电,就会永远保持它的值。
2. 动态RAM(DRAM)
常用来做内存。
DRAM每个位存储为电容充电。因为有很多原因会导致漏电,所以内存系统必须周期性地通过读出数据,重写来刷新内存的每一位。
3. 传统DRAM
下图是一个16*8的DRAM芯片。
16是超单元的个数,8表示每个超单元可以存储8bit的数据
整个DRAM芯片通过内存引脚和数据引脚与内存控制器相连
当我们需要从DRAM芯片中读出图中所示的超单元(2,1)
-
首先内存控制器发送行地址2到DRAM芯片,DRAM所作出的响应是将整个第二行的内容全部复制到内部的行缓冲区中。
-
然后内存控制器发送列地址1到DRAM芯片,DRAM从这个行缓冲区中复制出对应的数据位,并发送到内存控制器。
-
为什么分两次发送:
DRAM是一个二维的阵列,而不是一个线性数组。
若是一个0-15的数组,那么需要四个地址引脚。但用4*4的阵列分两次发送就只需要两个地址引脚
4. 内存模块
下图是一个用DRAM芯片封装成的内存模块,一共有8个DRAM芯片
每个芯片的大小为8M*8,也就是8MB
因此整个模块的大小为64
每个超单元存储8bit的数据,对于8字节的数据需要8个超单元来存储。
这8个超单元分布在八个芯片上,具有相同的i,j(行、列地址)
DRAM0存储最低8位,DRAM7存储最高八位
5. 访问主存
数据流通过总线在处理器和DRAM主存之间来来回回。
cpu和主存之间的数据传送称为总线事务。
- 读事务:从主存传送数据到cpu
- 写事务:从cpu传送数据到主存
当cpu执行一个如下操作
movq A , %rax //地址A的内容加载到寄存器rax中
- cpu将地址A放到系统总线上,I/O桥将系统总线的电子信号翻译成内存总线的电子信号
- 主存感觉到内存总线上的地址信号,从内存总线读地址,从DRAM取出数据,并将数据写到内存总线上
- I/O桥将内存总线上的电子信号转化为系统总线。cpu感觉到系统总线上的信号,从总线上读数据,并将数据复制到寄存器
2. 磁盘存储
无论是SRAM还是DRAM,都需要有电的情况下才能保存数据。
磁盘是在断电下也能保存数据的存储介质
目前市面上主流的磁盘有两类:机械磁盘和固态硬盘
1. 磁盘构造
盘片表面有磁性的记录材料,主轴带动盘片高速旋转。如图有三个盘片,一共六个表面可以存储数据。
盘片的表面被划分成一圈圈磁道(左图),像同心圆一样。
每个磁道被划分成多个扇区(右图)。扇区之间有一些间隙,是用来存放扇区标识信息,不能用来存储数据
磁盘通过读写头来读写存在盘片表面的数据。
盘片每个表面都对应着一个独立的读写头,所有的读写头连接在一个传送臂上,通过传送臂的移动读取数据。这种机械运动是寻道
2. 磁盘操作
磁盘以扇区大小的块来读写数据。对扇区的访问时间有三个主要部分
-
寻道时间
当目标扇区所处的磁道与读写头当前所处的磁道不同时,传动臂需要将读写头移动到目标扇区所处的磁道。传送臂移动的时间就是寻道时间 -
旋转时间
当读写头位于目标扇区所在的磁道,驱动器等待目标扇区的第一个位旋转到读写头下。主要取决于读写头到达目标扇区时的位置和磁盘的旋转速度
-
传送时间
当目标扇区的第一个位位于读写头下,驱动器就可以开始读或者写该扇区的内容。假如每条磁道的平均扇区有400个,转一圈需要8ms,所以转过一个扇区需要0.02ms,传送时间为0.02ms
3. 逻辑磁盘块
从操作系统角度看,每个磁盘抽象成一个个逻辑块序列,每个逻辑块和一个扇区的容量一致,都是512个字节。
磁盘内部有一个硬件/固件设备:磁盘控制器。维护逻辑块号与实际磁盘扇区的映射关系
当操作系统执行读一个磁盘扇区数据到主存。
- 操作系统发送一条命令到磁盘控制器,让它读某个逻辑块号
- 控制器上的固件执行一个快速表查找,将一个逻辑块号翻译成一个(盘面,磁道,扇区)三元组
- 控制器上的硬件解释这个三元组,并将读到的数据写入控制器上的缓冲区中,然后复制到主存。
3. 固态硬盘
闪存翻译层扮演着和磁盘控制块相同的角色,将对逻辑块的请求翻译成对底层物理设备的访问
4. 局部性
-
有良好的时间局部性的程序中
被引用过一次的内存位置很可能在不远的将来再次被多次引用 -
有良好的空间局部性的程序中
如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置
在下面这个程序中
sum在每次循环迭代中都被引用一次,具有良好的时间局部性
数组是行优先存储的,先循环行再循环列有良好的空间局部性
int sum = 0;
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
sum+=a[i][j]
}
5. 存储器层次结构
存储器层次结构示意图:
存储器层次结构的中心思想是:
对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。
第k层的缓存包含第k+1层块的一个子集的副本。
数据总是以块大小为传送单元在第k层和第k+1层来回复制的
缓存命中
当程序需要第k+1层的某个数据对象d时,它首先在第k层找d。
如果d刚好在第k层,就是缓存命中
缓存不命中
当第k层中没有缓存数据对象d,也就是缓存不命中
当缓存不命中,第k层的缓存从第k+1层缓存中取出包含d的那个块。如果第k层缓存已满,可能会覆盖现存的一个块
6. 高速缓存存储器
1. 通用的高速缓存存储器组织结构
考虑一个计算机系统,每个存储器地址有m位,形成
一个高速缓存被组织成有
每个组包含E个高速缓存行(cache line)
每个行由一个
块偏移为b,所以一个数据可以选择从数据块的第
如下图,m位的地址被划分成t个标记位,s个组索引位,b个块偏移位
cache的大小是指所有数据块的大小,所以cache的大小可通过S * E * B得到
当cpu执行数据加载,从内存地址A处读取指令时。
- 根据存储器层次原理,cpu将地址A发送到cache,如果cache中保存着目标数据的副本,就将目标数据发回给cpu。
那么cache是如何知道自己保存目标数据的副本的呢。
- 首先我们可以根据目标数据地址(地址A)的组索引位来确定目标数据存储在哪个组(set)中。
- 通过长度为t的标记来确定具体的行,此时有效位必须要等于1
- 通过长度为b的块偏移量来确定目标数据在数据块中的确切地址
为什么cache要用中间位作为组索引而不是高位
假设如下图这种情况,cache里只有四组。
那么对于一个有良好空间局部性的程序来说。如果组索引在高位的话,引用临近的内存会导致所在的组一直相同,就会导致一直缓存不命中,cache的使用率很低
2. 直接映射高速缓存
当E=1,也就是每个组都只有一行cache line的高速缓存称为直接映射高速缓存(direct-mapped cache)
-
组选择
根据组索引表示的无符号数确定在哪个组
如下图,这个组索引表示的是标号为1的set
-
行匹配
若该组该行的有效位等于1,且地址中的标记和cache line中的标记位一样,则匹配成功 -
字选择
块偏移位给出了字在块中是从哪开始
如图,块偏移位是100,所以该地址数据的副本是从第四个字节开始的
-
缓存不命中时的行替换
从存储器层次结构的下一层取出被请求的块,然后存储在组索引位指示的组中的一个cache line中。
3. 组相联高速缓存
每个set有多个cache line
其中c是cache的容量,b是每行数据块的大小
- 组选择
和直接映射一样 - 行匹配
找到有效位等于1且与地址中的标记位匹配的cache line - 字选择
和直接映射一样 - 不命中时的行替换
4. 全相联高速缓存
全相联高速缓存由一个包含所有数据块的set组成
因为高速缓存必须并行地搜索许多相匹配的标记,构造一个又大又快的相联高速缓存很困难。所以全相联高速缓存只适合做小的高速缓存。例如虚拟内存系统中的翻译备用缓冲器(TLB)
5. cache的写操作
写命中
定义:想要写入数据的地址在cache中
- 写穿透(write-through)
在写cache的同时写内存(更低一级的cache)
缺点是每次写都会引起总线流量 - 写回(write-back)
只有当替换算法要驱逐这个更新过的块时,才把它写到紧接着的低一层中。
缺点是增加了复杂性。cache必须为每一个cache line维护一个额外的修改为(dirty bit),表明这个高速缓存块是否被修改过。
写不命中
-
写分配(write-allocate)
加载相应的低一层中的块到高速缓存中,然后更新这个高速缓存块 -
非写分配
避开高速缓存,直接把这个字写到低一层
写穿透高速缓存通常是非写分配的,写回高速缓存通常是写分配的
6. 一个真实的高速缓存层次结构的解剖
实际上,高速缓存既保存数据也保存指令。
只保存数据:d-cache
只保存指令:i-cache
既保存数据又保存指令:unified-cache
下图是Intel Core i7处理器的高速缓存层次结构
每个cpu芯片有四个核。
每个核有自己的L1 i-cache,L1 d-cache和L2统一的高速缓存。
所有的核共享片上L3统一的高速缓存。
下图是Core i7高速缓存层次结构的特性
7. 高速缓存参数的性能影响
有许多指标来衡量高速缓存的性能
-
不命中率
不命中数量/引用数量 -
命中率
1-不命中率 -
命中时间
从cache传一个字到cpu所需的时间
包括组选择、行确认和字选择的时间 -
不命中处罚
由于不命中所需要的额外时间。
L1不命中需要从L2得到服务的处罚通常是数10个周期,从L3得到服务的处罚,50个周期
1. 高速缓存大小的影响
较大的cache可能会提高命中率,但同样会增加命中时间
2. 块大小的影响
较大的块能利用程序中可能存在的空间局部性,帮助提高命中率。
但对于给定的cache,块越大意味着cache line越少,时间局部性好的程序命中率就会受到损害。
越大的块传送时间越长,对不命中处罚也有负面影响
3. 相联度的影响
这里是参数E : cache line选择的影响
较高的相联度的优点是降低了高速缓存由于冲突不命中出现抖动的可能性。
缺点是增加命中时间,因为复杂性增加了。增加不命中处罚,因为选择替换的行的复杂性也增加了
相联度的选择最终成为命中时间和不命中处罚之间的折中。
4. 写策略的影响
越往层次结构下走,传送时间增加,减少传送的数量就变得更加重要。
所以一般而言,高速缓存越往下层,越可能是写回而不是写穿透。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)