CSAPP 06 - 储存器
储存器系统是一个具有不同容量,成本和访问时间的储存设备的层次结构。
储存技术
如果程序需要的数据存储在 CPU 寄存器中,在指令执行期间,在 0 个周期内就能访问到他们。如果存储在高速缓存中,需要 4~75 个周期。如果存储在主存中,需要上百个周期。而如果存储在磁盘上,大约需要几千万个周期。
RAM
随机访问存储器分为两类:静态和动态。静态 RAM 通常比动态 RAM 更快,但也贵的多。
- SRAM
SRAM 用来作为高速缓存储存器,既可以在 CPU 芯片上,也可以在片外。SRAM 将每个位存储在一个双稳态
的储存单元里,每个单元由 6 个晶体管组成,并且具有双稳态性质:它可以无限期的保持在两个不同的电压配置或状态之一,其他任何状态都是不稳定的 - 从不稳定状态开始,电路会迅速转移到两个稳定态之一。
由于 SRAM 储存器的双稳态特性,只要有电,它就永远的保持它的值。即使有干扰来扰乱电压,当干扰消除时,电路就会恢复到稳定值。
- DRAM
DRAM 用来作为主存以及图形系统的帧缓冲区。DRAM 将每个位存储为对一个电容的充电,每个单元由一个电容和一个访问晶体管组成。并且 DRAM 存储单元对于干扰十分敏感,当电容的电压被扰乱后,它就永远不会恢复了。
实际上有许多原因会导致漏电,使得 DRAM 单元在 10~100 毫秒内失去电荷,不过计算机运行的时钟周期是以纳秒来衡量的,所以内存系统可以通过周期性的读出,然后重写来刷新内存的每一位。
DRAM
为了理解方便,经常将内存抽象为一个字节数组,而实际上 DRAM 被组织成二维阵列,它是一个 r 行 c 列的长方形阵列。并且每 w 个 (一般是 8) DRAM 基本位单元组成一个超单元
,一共 d 个超单元 (d = r * c)。
每个超单元都有唯一的地址 (i, j),表示这个超单元位于 i 行,j 列。信息通过引脚
流入和流出芯片,每个引脚携带一个位的信号:
每个 DRAM 芯片都会链接到一个称为内存控制器
的电路,内存控制器可以一次传送 w 位数据到每个 DRAM 芯片,或者一次从每个 DRAM 芯片传出 w 位的数据。为了读出位于 (i, j) 位置超单元的数据,内存控制器先将行地址 RAS 传到地址引脚,然后将列地址 CAS 传到地址引脚,DRAM 则通过数据引脚将内存返回。
使用二维阵列的形式来组织 DRAM 而不是线性数组的一个原因是:降低了地址引脚的数量,但是通过两步传送超单元地址也会增加访问时间。
内存模块
DRAM 芯片封装在内存模块
中,内存模块插在主板的扩展槽上:
示例模块是一个 8 * 8M 组成的内存模块,这 8 个芯片被编号为 0~7,每个超单元存储主存的一个字节。使用 (i, j) 位置的 8 个超单元来表示主存中一个 64 位字。
要取出内存地址 A 处的一个字,内存控制器将 A 转换为一个超单元地址 (i, j),并将它发送到内存模块,内存模块将 i 和 j 广播到每个 DRAM。作为响应,每个 DRAM 输出它的 (i, j) 超单元的 8 位内容。内存模块收集这些输出,并将他们合并为一个 64 位字返回给内存控制器。
通过将多个内存模块连接到内存控制器,能够聚合为主存。在这种情况中,当控制器收到地址 A 时,控制器选择包含 A 的模块 k,将 A 转换成它的 (i, j) 形式,并将 (i, j) 发送到模块 k。
DRAM2
生产厂商试图跟上迅速增长的处理器速度,不断推出新产品,这些产品都基于传统的 DRAM 做了一些优化,提高访问基本 DRAM 单元的速度。
- FPM DRAM (Fast Page Mode DRAM)
传统 DRAM 将整行数据拷贝到缓冲区中,使用其中一个并丢弃剩余的超单元。FPM DRAM 可以连续访问同一行的超级单元,因此速度更快。例如读取一行中的四个超级单元,传统 DRAM 需要发送四次 RAS 请求和四次 CAS 请求,而 FPM DRAM 则只需要一次 RAS 请求和四次 CAS 请求。
- EDO DRAM (Extend Data Out DRAM)
FPM DRAM 的一个增强形式,它允许各个 CAS 信号在时间上靠得更紧密一点。
- SDRAM (Synchronous DRAM)
前三种 DRAM 都是异步的。SDRAM 使用与驱动内存控制器相同的外部时钟信号的上升沿来代替许多控制信号,同步的 SDRAM 能比异步的 DRAM 更快的输出超单元的内容。
- DDR SDRAM (Double Data-Rate Synchronous DRAM)
DDR SDRAM 是对 SDRAM 的一种增强,它使用两个时钟沿作为控制信号,使得 SDRAM 的速度翻倍。不同类型的 DDR SDRAM 是通过预取缓冲区
的大小来划分的:DDR (2 位),DDR2 (4 位),DDR3 (8 位)。
- VRAM (Video RAM)
常用于图形系统的帧缓存,其本质与 FPM DRAM 类似。区别在于 VRAM 通过依次对内部缓冲区的内容移位来输出数据,并且可以并行地读写内存。
ROM
如果断电,RAM 将丢失所有存储的信息,所以 RAM 具有易失性
。而非易失性存储器
能够在断电后仍保存自己的信息。现在有很多中非易失性存储器如 ROM,由于历史原因,虽然 ROM 中有的类型可读可写,但是他们被统一称作只读存储器
。
- PROM (Programmable ROM)
可编程 ROM 只能被编程一次,其内部的每个存储器单元都有一种熔丝,只能用高电流熔断一次。
- EPROM (Erasable Programmable ROM)
可擦写可编程 ROM 的擦除和重编程次数可以达到 1000 次。电子可擦除 EEPROM 基于 EPROM,其可重编程次数的数量级可以达到 105。
- 闪存 (Flash Memory)
闪存是一类非易失性存储器,基于 EEPROM,他已经称为一种重要的存储技术。SSD (Solid State Disk) 就是基于闪存的一种新型磁盘驱动器。
存储在 ROM 中的程序通常被称为固件
,当一个计算机通电以后,它会首先运行存储在 ROM 中的固件。
访问主存
数据通过共享的电子电路在 CPU 和 DRAM 主存储器之间流动,这些管道被称为总线
。数据传输的过程由一系列被称为总线事务
的步骤组成,其中读事务将数据从主存加载到 CPU 中,写事务则将数据从 CPU 传输到主存中。
总线是一组并行的导线,可以传输地址、数据和控制信号,其中控制总线负责同步事务并识别当前正在执行事务的类型,数据总线和地址总线可以是不同的,也可以共享一组导线:
其中,I/O 桥接器是一个芯片组,它就包含内存控制器芯片。主存是由一个或多个内存模块组成的。所有的这些部件由一对总线连接,系统总线
连接 CPU 和 I/O 桥接器,内存总线
连接 I/O 桥接器和内存。
考虑一条加载操作movq A, %rax
的执行过程:
- CPU 上的总线接口在总线上发起读事务,读事务包含三个步骤。
- CPU 先将地址 A 送上系统总线,I/O 桥内部的内存控制器负责将地址 A 转换成超单元的地址信息,然后将转换后的地址信号送上内存总线。
- 主存感知到地址信号,内部的内存模块读取并收集数据,然后将数据送上内存总线,I/O 桥将内存总线上的信号翻译为系统总线信号,并将其送回系统总线。
- CPU 感知到系统总线上的数据,从总线上读取数据,并将数据复制到寄存器。
存储操作和上述类似movq %rax, A
:
这里的写事务同样有三个步骤:CPU 先将地址送上总线,内存从总线读取地址并等待数据到达,接下来 CPU 将数据字复制到系统总线上,最后主存从内存总线上读取数据并存储到 DRAM 中。
磁盘存储
磁盘是广为应用的保存大量数据的存储设备,存储的数据量可以达到几百兆甚至上千万兆字节,而基于 RAM 的存储设备只能有几百或几千兆字节。不过,从磁盘上读取信息的时间为毫秒级,比从 DRAM 上读满了 10 万倍,比从 SRAM 上读慢了 100 万倍。
磁盘构造
磁盘由盘片
组成,每个盘片有两个表面称为盘面
,盘面覆盖着磁性记录材料。盘片中央有一个可以旋转的主轴
,它使得盘片以固定速率旋转,磁盘通常包含多个这样的盘片,并封装在一个密封的容器内。
每个盘面是由一组称为磁道
的同心圆组成的,每个磁道又被划分为多个扇区
,每个扇区包含同等大小的数据位 (通常是 512 字节),扇区之间有一些间隙
,间隙不存储数据位,而是用来表示扇区的格式化位。
将一个或多个盘片叠放在一起,并用密封包装封装,整个装置就被称为磁盘驱动器
,简称磁盘
。当有多个盘片时,所有盘面上到主轴距离相等的一组磁道组成一个柱面
。
磁盘操作
磁盘使用读写头
来读写存储在磁盘表面的数据位,每个读写头都连接到传动臂
的一端。传动臂可以沿着盘面半径机械运动,因而读写头可以定位到任意磁道上,这个过程称为寻道
。读写头找到了目的磁道,磁道上的每个位通过它下面时,读写头可以读取或修改该值,每个盘面都有一个对应的读写头。
磁盘以扇区大小的块来读写数据。对扇区的访问时间有三个组成部分:寻道时间,旋转时间和传送时间。
- 寻道时间:将传动臂移动到目的扇区所在磁道所花费的时间,寻道时间不是固定的。
- 旋转时间:读写头到达磁道后,等待目的扇区第一个位旋转到读写头下的时间,这个时间也不是固定的。
- 传送时间:目的扇区第一个数据位到达读写头下后,驱动器就可以开始读写该扇区的内容,一个扇区的传送时间依赖于旋转速度和每条磁道下的扇区数目。
相比其他两个因素,传送时间小到可以忽略。而寻道时间和旋转延时大致相等,因此可以用两倍的寻道时间来估算磁盘的访问时间。
逻辑磁盘块
现代磁盘构造复杂,有多个盘面,每个盘面上有不同的记录区分。为了对操作系统隐藏这样的复杂性,现代磁盘将他的构造呈现为:一个 B 个扇区大小的逻辑块
的序列,编号位 0 1 2 …。磁盘中封装有一个小的硬件,称为磁盘控制器
,它维护着逻辑块号和磁盘扇区之间的映射关系。
当操作系统想要执行一个 I/O 操作,如读取一个磁盘扇区的数据到主存:操作系统会发送一个命令到磁盘控制器,让它读取某个逻辑块号。控制器上的某个固件执行一个快速表查找,将这个逻辑块号翻译成 (盘面,磁道,扇区) 的三元组,这个三元组唯一对应了一个物理扇区,控制区可以解释这个三元组,并将读写头移动到对应柱面,然后将对应扇区的数据位复制到小缓冲区中,最后复制到主存中。
连接 I/O 设备
例如图形卡,监视器,鼠标,键盘和磁盘这样的 I/O 设备,都是通过 I/O 总线连接到 CPU 和主存的。系统总线和内存总线总是与 CPU 相关,但是 I/O 总线总是设计成与底层 CPU 无关,例如 Intel 的 PCI 总线:
虽然 I/O 总线比系统总线和内存总线慢,但是它可以容纳种类繁多的第三方 I/O 设备,例如:
- USB 控制器:USB 控制器是一个连接到 USB 总线的中转设备,USB 总线是一个广泛使用的标准,连接各种外围 I/O 设备如鼠标,键盘,打印机,外部磁盘和固态等。
- 图形适配器:负责代表 CPU 在显示器上画像素。
- 主机总线适配器:将一个或多个磁盘连接到 I/O 总线。
访问磁盘
CPU 使用一种称为内存映射 I/O
的技术来向 I/O 设备发射命令。在使用内存映射 I/O 技术的系统中,地址空间中有一块地址是为与 I/O 设备通信而保留的,每个这样的地址被称为一个 I/O 端口。当一个设备连接到总线时,它就被映射到一个或多个端口。一个从磁盘读取数据的例子:
假设磁盘的映射端口为 0xa0,随后 CPU 可能通过执行三条对 0xa0 的存储指令,发起磁盘读:第一条指令是发送一个命令字,告诉磁盘发起一个读,同时还有一些其他参数如读完成时是否发起中断。第二条指令指明应读的逻辑块号。第三条指令应指明存储读出数据的主存地址。
当 CPU 发起请求后,在磁盘进行读操作时,CPU 可以去做其他事情。当磁盘控制器接收这些读指令后,它将逻辑块号对应的数据内容传送到主存,不需要 CPU 干涉,这种设备可以自己执行读或者写总线事务而不需要 CPU 干涉的过程称为直接内存访问
,同时这种数据传送也称为DMA 传送
。
当 DMA 传送完成后,磁盘控制器通过给 CPU 发送一个中断信号来通知 CPU,基本思想是:中断会发送信号到 CPU 的一个外部引脚上,这会导致 CPU 暂停它当前正在做的工作,跳转到一个操作系统例程,这个程序会记录下 I/O 已经完成,然后将控制返回 CPU 被中断的地方。
固态硬盘
固态硬盘 SSD 是一种基于闪存的存储技术。通常 SSD 被封装插到 I/O 总线上的标准硬盘插上,行为和其他硬盘一样,处理来自 CPU 的读写逻辑块的请求。一个 SSD 由一个或多个闪存芯片
和闪存翻译层
组成,闪存翻译层的功能磁盘控制器:将对逻辑块的请求翻译成对底层物理设备的访问。
一个闪存由 B 个块
的序列组成,每个块由 P 个页
组成。通常,页的大小是 512B ~ 4KB,块是由 32 ~ 128 个页组成的。数据以页为单位读写,并且读 SSD 要比写快得多。
只有在一页所属的块被擦除
之后才能写这一页:通常是指将该块中所有的页的位设置为 1。不过一旦一个页被擦除了,块中的每个一页都不需要再擦除就能写一次。
随机写很慢有两个原因:首先是擦除块需要相对较长的时间,比访问页所需要的时间高一个数量级。其次,如果写操作试图修改一个包含已有数据的页 (非全 1),那么 FTL 会将该页所属块的数据读取到内存中进行修改,并将该页所在的块标记为脏块
以等待垃圾回收,最后将数据从内存中写入一个新块,并且更新映射表。
比起旋转磁盘,固态有很多优点:它是由半导体存储器构成的,随机访问时间比磁盘更快,能耗更低,也更结实。但是也有一些缺点:反复重写块会导致磨损而无法使用,闪存翻译层中平均磨损
逻辑试图将擦除平均分布到所有块上来最大化每个块的寿命。其次 SSD 每字节比磁盘大约贵 30 倍。
局部性
一个编写良好的计算机程序常常具有良好的局部性
。也就是,它们倾向于引用邻近于其他最近引用过的数据项,或者最近引用过的数据项本身。这种倾向性称为局部性原理
,是一个持久的概念,对硬件和软件系统的设计和性能有极大的影响。
局部性通常有两种不同的形式:时间局部性
和空间局部性
。在一个具有良好的时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来再被多次引用。在一个具有良好的空间局部性的程序中,如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。
一个对向量元素求和的函数:
int sumvec(int v[N]) {
int i, sum = 0;
for (i = 0; i < N; ++i)
sum += v[i];
return sum;
}
临时变量 sum 在每次循环迭代中都被引用,因此对于变量 sum 来说有很好的时间局部性,但是没有空间局部性。变量 v 的元素是被顺序读取的,一个接一个。因此,对于变量 v 来说有很好的空间局部性,但是时间局部性很差,因为每个向量元素只被访问一次。对于循环体中的每个变量,这个函数要么有好的空间局部性,要么有好的时间局部性。所以,可以说 sumvec 函数具有良好的局部性。
像 sumvec 这样顺序访问一个向量的每个元素的函数,具有步长为 1 的引用模式
。一个连续的向量中,每隔 k 个元素进行访问,就称为步长为 k 的引用模式,步长为 1 的引用模式也叫顺序引用模式
,一般而言,随着步长的增加,程序的空间局部性下降。
层次结构
现代计算机系统中都使用一种存储器层次结构
的方法,将硬件和软件的性质相结合:
缓存
高速缓存是一种小而快的存储设备 (使用 SRAM),通常它作为存储在更大,更慢的设备中的数据对象的缓冲区域。使用高速缓存的过程就叫缓存
。
存储器层次结构的中心思想是,对于每个位于 k 层的更快更小的存储设备作为位于 k + 1 层的更大更慢的存储设备的缓存。即层次结构中每一层都缓存来自叫低一层的数据对像。
第 k + 1 层的存储器被划分成连续的数据对象组块,称为块
。数据总是以块大小为传送单位在第 k 层和第 k + 1 层之间来回复制,每个块都有唯一的地址和名字。块可以是固定大小,也可以是可变大小的。不同层级使用的块大小各不相同,如 L0 和 L1 之间的块通常为一个字,而 L1 和 L2 之间的块则为四到八个字。一般来说,层级越低的设备访问速度越慢,因此为了补偿这些较长的时间,倾向于使用较大的块。
- 缓存命中
当程序需要访问存储在第 k + 1 级设备中的数据对象 d 时,会首先从第 k 级设备中查找。如果 d 恰好缓存在第 k 级设备中,那么便称缓存命中。
- 缓存不命中
如果第 k 层没有缓存数据对象 d,那么就是缓存不命中。当发生缓存不命中时,第 k 层的缓存会从第 k + 1 层缓存取出包含 d 的块,如果 k 层缓存已经满了,可能就会覆盖现存的一个块。
覆盖一个现存的块的过程称为替换
或驱逐
,被驱逐的块也叫牺牲块
。决定替换哪个块是由缓存的替换策略
控制的。
- 不命中的种类
三种常见的缓存不命中的情况:
- 冷不命中:一个空的缓存称为
冷缓存
,如果第 k 层缓存是空的,那么缓存一定会不命中,这种情况就叫冷不命中
。 - 冲突不命中:只要发生了不命中,那么缓存必然执行某种
放置策略
,例如:可以将 k + 1 层中的块 i 放置到第 k 层中的块 (i mod 4) 中,因此 k + 1 层的多个块会映射到同一个 k 层的块上。这种策略就会引起一种冲突不命中
:如果程序交替请求两个替换策略相同的块,那么缓存会一直不命中。 - 容量不命中:如果访问块的
工作集
大小超出了缓存大小,那么就会发生容量不命中
。
- 缓存管理
管理缓存的逻辑可以是硬件,软件或是二者的结合。编译器管理寄存器文件,L1,L2,L3 层缓存完全是由内置在缓存中的硬件逻辑来管理的。并且,在大多数时候,缓存都是自动运行的,不需要程序采取特殊的或是显示的行动。
高速缓存
早期的计算机系统存储器结构层次只有三层:CPU 寄存器,DRAM 主存储器和磁盘存储。由于 CPU 和主存之间的差距逐渐增大,设计者在寄存器和主存之间加入了一个小的 SRAM 高速缓存存储器,称为 L1 高速缓存。
随着性能差距不断增大,设计者又在 L1 高速缓存和主存之间插入了一个更大的高速缓存,称为 L2 高速缓存,有的现代系统还有 L3 高速缓存,虽然在安排上有很多变化,但是通用原则是一致的。
通用结构
考虑一个计算机系统,其中每个存储器的地址有 m 位,形成 M = 2m 个地址。机器的高速缓存被组织成一个有 S = 2s 个高速缓存组
的数组。每个组包含 E 个高速缓存行
。每个行是由一个 B = 2b 字节的数据块组成的,一个有效位
指明这个行是否包含有意义的信息,还有 t = m - (b + s) 个标记位
来表示存储在这个高速缓存行中的块。
一般而言,高速缓存的结构可以使用元组 (S,E,B,m) 来描述。高速缓存的大小 C 指的是所有块的大小的和,标记位和有效位不包括在内,因此 C = S * E * B。
参数 S 和 B 将 m 位的地址分为了三个字段,一个内存地址 A 中的 s 个组索引位
是一个得到 S 个数组的索引,组索引位被解释为一个无符号数,它告诉硬件字存储在哪个组中。A 中的 t 个标记位就告诉硬件组中的哪一行包含这个字 (如果有的话)。当且仅当设置了有效位并且该行的标记位和高速缓存行匹配时,这个字才被缓存,并且很容易根据块偏移
来得到该字在数据块中的偏移。
直接映射缓存
根据每个组的高速缓存行数 E,高速缓存被分为不同的类。E = 1 的高速缓存被称为直接映射高速缓存
。
假设系统只有 L1 一层高速缓存,并尝试从内存中读取一个字 w,高速缓存可能会执行下列操作:
- 组选择
高速缓存从 w 的地址中抽取出 s 个组索引位,将这些位解释为一个无符号整数,这个整数关联了一个高速缓存组。
- 行匹配
通过组选择确定了该地址所在的组 i,接下来是确定是否有字 w 的一个副本存储在组 i 的一个高速缓存行中。因为直接映射高速缓存只有一个缓存行,所以当且仅当设置了有效位,并且高速缓存行中的标记位和地址中的标记匹配时,这一行才包含字 w 的一个副本。
- 字选择
如果字 w 缓存命中,那么最后一步就是确定该字位于缓存块中的哪个位置。只需要根据地址中的块偏移就能确定。
- 行替换
如果缓存不命中,那么它需要从存储器层次结构中的下一层取出被请求的块,然后将新的块存储在组索引位指示的一个高速缓存行中。对于直接映射高速缓存来说,替换策略十分简单,就是直接驱逐高速缓存行。
组相联型缓存
直接映射高速缓存中冲突不命中问题的根源是每个组只有一行。组相联型高速缓存放松了这条限制,所以每个高速缓存组都可以有多个高速缓存行。但是这里的 E < C / B,换句话说 S 必须大于 1。
- 组选择
组相联型高速缓存的组选择和直接映射高速缓存一样。
- 行匹配和字选择
组相联型高速缓存的行选择比直接映射更复杂,因为此时缓存组中不止一行,所以高速缓存必须检查多个行的标记位和有效位,以确定其是否在集合中。相联存储器
是一个 (key,value) 形式的 pair 数组,以 key 为输入,返回与 key 相匹配的 pair 的 value 值,因此可以将标记位和有效位看作 key,value 就是块的内容。
这里一个重要思想是:组中的任何一行都可以包含任何映射到这个组的内存块。所以高速缓存必须搜索组中的每一个行,寻找一个有效的行,其标记和地址中的标记相匹配。如果找到了这一行,那么缓存就命中了,块偏移从这个块中选中一个字。
- 行替换
组相联型高速缓存不命中的替换策略也更加复杂,如果该组中有剩余的空行,那么可以直接替换。如果没有找到空行,此时高速缓存就必须挑选一个缓存行来替换。
全相联型缓存
全相联型高速缓存是一个包含所有高速缓存行的组即 E = C / B。
全相联型高速缓存只有一个组,因此它没有组选择,并且地址 w 也只有两部分:标记和块偏移。
全相联型高速缓存的组匹配和字选择都和组相联型高速缓存一致,不过因为其行选择规模更大,所以高速缓存电路必须并行的搜索许多相匹配的标记,构建一个有大又快的相联高速缓存很困难,而且昂贵。因此,全相联高速缓存只适合做小的高速缓存,例如虚拟内存的快表。
有关写的问题
高速缓存的读操作十分简单,写的情况就复杂很多。如果要写一个已经被缓存的字 w (称为写命中
),在高速缓存中更新了它的 w 副本后,继而更新 w 在较低层次中的副本中有多个策略:
- 直写
这是最简单的策略,即立即将 w 的高速缓存块写回到紧邻的低一层高速缓存中。虽然简单,但是直写的缺点是每次写操作都会引起总线流量
。
- 写回
写回策略是尽可能的推迟更新,只有当替换算法要驱逐这个修改过的块时,才把它写到紧邻的低一层中。写回策略能显著的减少总线流量,但是它增加了复杂性:高速缓存必须为每个缓存行额外维护一个修改位
,表明这个缓存行是否被修改过。
当写不命中时,也有两个策略:一种方法是写分配
,即将字所在的块从低层加载到高速缓存中来修改,写分配试图利用空间的局部性,但是缺点是每次不命中都会导致一个块从低层传到高层缓存。另一种方法是非写分配
,其避开高速缓存,直接把这个字写到低一层中。
直写高速缓存通常是非写分配
的,写回高速缓存通常是写分配
的。并且在虚拟内存系统中,只使用写回。
真实结构解剖
在实际系统中,高速缓存既保存数据,也保存指令。只保存指令的高速缓存称为i-cache
,只保存程序数据的高速缓存称为d-cache
,既保存指令又保存数据的高速缓存称为统一的高速缓存
。现在处理器包括独立的 i-cache 和 d-cache,这样做的好处是处理器能够同时读一个指令字和一个数据字。
性能影响因素
衡量高速缓存的性能指标主要有:
- 不命中率:不命中量 / 引用数量。
- 命中率:1 - 不命中率。
- 命中时间:从高速缓存中读取一个字到 CPU 所需的时间,包括组选择,行匹配和字选择。
- 不命中惩罚:因为缓存不命中而增加的读取时间
缓存的不同参数对其性能的影响为:
- 缓存容量:更大的缓存可以增加命中率,但也会增加命中时间。
- 块的大小:利用空间局部性,通过增大块可以提升命中率。但在缓存容量一定的情况下,块越大,缓存行的数量就越少。此外,更大的块还会增加数据传输的时间;
- 关联性 (E 的大小):更多的缓存行可以避免因冲突不命中而导致的
抖动
,但维护更多的缓存行也需要大量成本。 - 写策略:直写比较容易实现,并且可以使用独立于高速缓存的
写缓冲区
。写回引起的传送比较少,并且越往层次下面走传送时间越长,越可能使用写回而不是直写。
缓存友好代码
编写缓存友好型代码的两个基本准则如下:
- 关注程序中核心函数中的内部循环,尽可能忽略其他部分。
- 尽量减少每个循环内部的缓存不命中数量。
其中第二个准则又可以根据局部性分为两方面:
- 最大化空间局部性:步长位 1 的顺序引用模式是最好的。
- 最大化时间局部性:对局部变量的反复引用是好的。
TODO 储存器山
待更新。