存储器层次结构

存储器层次结构

存储技术

计算机技术的成功很大程度来源于存储技术的巨大进步。早期的电脑甚至没有磁盘。现在电脑上的磁盘都已经按T算了。

随机访问存储器(Random-Access Memory, RAM)

随机访问存储器(Random-Access Memory, RAM)分两类:

  1. 静态的:SRAM,高速缓存存储器,既可以在CPU,也可以在片下。
  2. 动态的:DRAM,用于主存或者图形系统帧缓冲区。

通常情况下,SRAM的容量都不会太大,而相比之下DRAM容量可以大得离谱。

静态RAM

SRAM将每个位存储在一个双稳态存储器单元里,每个单元用一个六晶体管电路实现。

这种电路有一个属性,它可以无限期地保持两个不同的状态的其中一个,其他状态都是不稳定的。

SmartSelect_20200430-075019_Xodo Docs

如上图,它能稳定在左态和右态,如果处于不稳定状态,它就像钟摆一样立刻变成两种稳态的其中一种。

也因为它的双稳态特性,即使有干扰,等到干扰消除,电路就能恢复成稳定值。

动态RAM

DRAM的每个存储是一个电容和访问晶体管组成,每次存储相当于对电容充电。

该电容很小,大约只有30毫微微法拉。

因为每个存储单元比较简单,DRAM可以造的非常密集。但它对干扰非常敏感,被干扰后不会恢复。

因此它必须周期性地读出重写来刷新内存的每一位。或者使用纠错码来纠正任何单个错误。

两者总结

SmartSelect_20200430-080726_Xodo Docs

传统的DRAM

DRAM芯片内的每一个单元被叫做超单元。

在芯片内,总共有\(d\) 个超单元,它们被排列成一个\(r \times c\) 大小的矩阵,也就是说\(d = r \times c\),每个超单元都可以用类似\((i, j)\) 之类的地址定位

而每个超单元则是由\(w\) 个DRAM单元组成。因此一个DRAM芯片可以存储\(dw\) 位的信息。

SmartSelect_20200430-081854_Xodo Docs

上图是一个\(16 \times 8\) 的DRAM芯片的组织。

首先由两个addr引脚依次传入行地址i 和列地址j 。每个引脚携带一个信号。由于这是\(4 \times 4\) 的矩阵,因此两个就够了。

然后定位到\((i, j)\) ,将该地址的超单元信息传出去。

信息是由引脚data 传出去的。由于一个超单元里有8个DRAM单元,因此使用了8个引脚

每个DRAM芯片被连接到一个内存控制器。该控制器可以读入或者读出\(w\) 位的数据。

整个读出过程是这样的:

  1. 内存控制器发送行地址\(i\) 到DRAM
  2. 内存控制器发送行地址\(j\) 到DRAM
  3. DRAM发送\((i, j)\) 的内容作为响应

其中行地址被称为\(RAS(Row \space Access \space Strobe \space 行访问选通脉冲)\) ,列地址被称为\(CAS(Column \space Access \space Strobe \space 行访问选通脉冲)\)

两个地址是共用一个addr 引脚的。

举个实际例子:

SmartSelect_20200430-085120_Xodo Docs

首先,内存控制器发送行地址2 ,DRAM做出的响应则是将一行的内容都复制到内部行缓冲区。

SmartSelect_20200430-090005_Xodo Docs

其次,内存控制器发动列地址1,DRAM做出的响应则是赋值行缓冲区的1列中的8位,然后发送到内存控制器。

将DRAM设计成矩阵的一个原因是降低芯片地址上的引脚数量,而缺点是必须分两步分发地址,增加访问时间。

内存模块

DRAM芯片封装在内存模块中,插到主板的拓展槽上。

看下图:

SmartSelect_20200430-091429_Xodo Docs

整个读取过程是这样的:

  1. 首先,内存控制器将一个内存地址翻译成一个超单元地址\((i, j)\)
  2. 内存控制器将地址广播到每一个DRAM芯片上。
  3. 每一块DRAM芯片作出响应,传出一个8位的字作为1个字节.
  4. 电路收集这些信息,将其合并成64位的字,再将信息返回给内存控制器

增强的DRAM

实际上就是为了迎合需求,对普通的DRAM进行特定的优化以满足需求。

  1. 快页模式(FPM DRAM):与传统的DRAM不同的地方在于,它可以一个RAS之后接过多个CAS,即可以连续的读取同行的数据,不需要重复发送RAS
  2. 扩展数据输出DRAM(EDO DRAM):实际上就是FPM DRAM的增强形式,他让CAS信号可以发送地更紧密一些。
  3. 同步DRAM(SDRAM):传统的DRAM是异步传输地址的。而这个利用一些技术达成了同步传输地址,它能比传统的异步存储器更快地输出单元信息。
  4. 双倍数据速率同步DRAM(DDR SDRAM):对SDRAM的一种增强,能使DRAM速度翻倍。
  5. 视频RAM(VRAM):它用于图形系统的帧缓冲区中。

非易失性存储器

如果断电,DRAM和SRAM会丢失它们的信息,他们属于易失性存储器。

因此,非易失性存储器就是关电之后仍然保存它们的信息。他们统称只读存储器\(Read-only \space Memory \space,ROM\)

它们是以能够被重编程的次数和重编程的机制来区分的。

  1. PROM\((\space Programmable \space ROM \space)\) 可编程ROM:只能被编程一次,PROM的每个存储器单元有一种熔丝,只能用高电流熔断一次。
  2. 可擦写可编程ROM\((Erasable \space Programmable \space ROM, \space EPROM\)) :可反复擦写编程1000次以上。
  3. 电子可擦除PROM\((Electrically \space Erasable \space PROM, \space EEPROM)\) :可擦写编程的数量级为\(10^5\) 次。
  4. 闪存\((flash \space memory)\) :非易失性存储器,基于EEPROM。

访问主存

数据流在处理器和DRAM主存之间的来来回回是通过总线(bus)的共享电子电路实现的。

数据传送的步骤被称为总线事务读事务从主存传送数据到CPU,写事务从CPU传送数据到主存。

  • 总线是一行并行的导线,能携带地址,数据和控制信号。

  • 数据和地址信号能否共享同一组导线取决于总线的设计。且两台以上的设备也能共享总线。

  • 总线上的控制信号会同步事务,且能标识出事务类型。

SmartSelect_20200430-124737_Xodo Docs

考虑如下操作时会发生什么:

movq	A, %rax
  1. CPU将地址A放到系统总线上,I/O桥把信号传到内存总线

    SmartSelect_20200430-131800_Xodo Docs

  2. 主存发现了内存总线上的地址,从内存总线中读取地址,再从DRAM中将数据x放到内存总线上

    SmartSelect_20200430-132011_Xodo Docs

  3. I/O桥把内存总线上的信号翻译成系统总线信号,再放上系统总线传递。最后CPU发现了数据的传递,从总线上读取数据,并将数据复制到寄存器%rax

    SmartSelect_20200430-132338_Xodo Docs

磁盘存储

磁盘是被大量使用来存储信息的设备。

磁盘构造

磁盘由盘片构成,每个盘片有两面,都称为表面,表面覆盖着磁性记录材料。

盘片中央有一个可以旋转的主轴,它能使盘片以固定的旋转速率旋转,通常是5400~15000转每分钟。

通常磁盘包含一个或者多个这样的盘片,并封装在一个密封的容器内。

SmartSelect_20200502-101357_Xodo Docs

上图展示了一个典型的磁盘表面的结构。

每个表面是由一组称为磁道的同心圆组成的。每个磁道被划分成为一组扇区。每个扇区会有相等数量的数据位。

扇区之间由一些间隙隔开,间隙间不存储数据位,间隙存储用来标识扇区的格式化位。

SmartSelect_20200502-102024_Xodo Docs

磁盘由一个或者多个叠放在一起的盘片组成的,它们被封装在一个密封的包装里,简称磁盘,或旋转磁盘,使之区别于固态硬盘。

柱面用来描述盘片驱动器的构造,用来表示所有表面到上离主轴距离相同的磁道集合。

磁盘容量

磁盘的容量是磁盘上可以记录的最大位数。

磁盘容量由以下技术因素决定:

  1. 记录密度:磁道一英寸的段可以放入的位数
  2. 磁道密度:从盘片中心出发半径上一英寸内可以有的磁道数
  3. 面密度:记录密度和磁道密度的乘积

磁盘密度容量:

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

磁盘操作

RPM ————–> 硬盘转速

磁盘用读写头来读写在磁性表面的位,而读写头连接到一个传动臂一端。

通过移动传动臂,读写头可以移动到任意一个磁道上,这样的机械运动叫寻道(seek)

一旦移动到合适的位置,读写头可以读出这个位的值,或者修改这个位的值。

SmartSelect_20200502-143751_Xodo Docs

读写头垂直排列,一旦读写头移动,一致行动。所有的读写头都位于同一柱面上。

磁盘总是密封包装的。读写头仅仅在表面约0.1微米处以80km/h的速度飞翔。在这极小的空隙中,一粒灰尘都是一块巨石。因此磁盘总是密封包装的。

对扇区的访问时间有三个主要部分:寻道时间,旋转时间和传送时间

  • 寻道时间:指将读写头移动到对应的磁道上。最坏的寻道时间在\(T_{max \space seek} = 20ms\) ,平均寻道时间为\(T_{avg \space seek} = 3 -- 9ms\)

  • 旋转时间:当读写头移动到对应的磁道上后,驱动器等待目标扇区第一个位旋转到读写头下。最坏情况是读写头刚刚错过第一个位:\(T_{max \space rotation} = \frac{1}{RPM} \times \frac{60s}{1min}\) ,平均情况是最坏情况的一半。

  • 传送时间:驱动器开始读和写。一个扇区的传送时间依赖于旋转速度和每条磁道的扇区数目。平均传送时间:\(T_{avg \space transfer} = \frac{1}{RPM} \times \frac{1}{(平均扇区数/磁道)} \times \frac{60s}{1min}\)

  • 整个读写的过程中,访问字节几乎不需要花费时间,但是寻道与旋转时间花费了大量的时间。

  • 寻道时间与旋转时间大致相等,寻道时间乘2就是估计磁盘访问时间简单而合理的方法。

逻辑磁盘块

为了对操作系统隐藏这样的复杂性,现代磁盘抽象出一个简单的视图,一个B个扇区大小的逻辑块序列,编号0,1,2,3……。

磁盘里封装着一个小的硬件/固件设备,磁盘控制器,维护者逻辑块号和实际磁盘扇区的映射关系。

系统进行一个I/O操作的过程:

  1. 发送一个命令到磁盘控制器,让它读某个逻辑块号。
  2. 控制器将逻辑块号翻译成一个可以唯一标识对应物理扇区的三元组。
  3. 控制器的硬件会解释这个三元组,将读写头移动到对应的位置。
  4. 读写头感知到的位放到控制器上的一个小缓冲区中,然后将它复制到主存中。

连接I/O设备

访问磁盘

固态硬盘(Solid State Disk, SSD)

一种基于闪存的存储技术。

它与其他硬盘的行为一样。

  1. 通常硬盘用USB或者SATA插槽。
  2. 同样是处理来自CPU读写逻辑磁盘的请求。
  3. 一个SSD封装一个或者多个闪存芯片和闪存翻译层。
    1. 闪存芯片与机械驱动器作用相同
    2. 闪存翻译层与磁盘控制器相同

SmartSelect_20200502-155208_Xodo Docs

如上图:

  1. 一个闪存由B个块组成
  2. 一个块由P个页组成

通常来说,一个页大小为512B ~ 4KB,一个块有32 ~ 128页,一个块的大小即为16KB ~ 512KB。

只有在一个块被擦除之后,才能写其中一页。不过一旦被擦除,块中的每一个页都不用再擦除。

大约在100000重复写之后,块就会磨损,不能再使用。

SmartSelect_20200507-092643_Xodo Docs

随机写的速度明显比随机读要慢。原因是:

  1. 擦除块要很久,1ms级。
  2. 如果写入的页已经有有用的数据在块中的其他页中,则需要先将块中的数据复制到别的块,再对进行擦写。

SSD的优点

  1. 结实
  2. 能耗低
  3. 随机访问速度极快

SSD的缺点

  1. 再反复写之后,容易磨损。

    对与这个问题,实际上已经在闪存翻译层进行平均磨损逻辑处理,SSD实际上能用非常长的时间了。

  2. 价格比较昂贵。

    现在价格也降下去了,反正SSD牛逼。

存储技术趋势

  1. 不同的存储技术有不同的价格个性能折中。
  2. 不同的存储技术的价格和性能属性以截然不同的速率变化着。
  3. DRAM和磁盘的性能滞后于CPU的性能

局部性

一种更喜欢引用最近引用过的数据项,或者邻近其他最近引用过的数据项的数据项,的倾向性,被称为局部性原理。

局部性通常由两种不同的形式:

  1. 时间局部性:被引用过一次的数据很可能在不远的将来再次被引用
  2. 空间局部性:被引用过一次的数据很可能在不远的将来引用其附近的数据

有良好局部性的程序比局部性差的程序运行的更快。

对程序数据引用的局部性

先看一段程序:

int sumvec(int v[N]){
    int i, sum = 0;
    for(i = 0; i < N; i++)
        sum += v[i];
    return sum;
}

对于sum 来说,它拥有良好的时间局部性,不过由于是标量,不具备空间局部性。

数组v 是一组向量,在这段程序中,它拥有良好的空间局部性,因为每个数据都是被顺序读取的。但是它的时间局部性很差,因为每个数据只会被读取一次。

在这个函数中,循环体的内的变量不是具有良好的时间局部性,就是具有良好的空间局部性,因此我们认为该函数拥有良好的局部性。

像这样访问一个向量的函数,我们称其是步长为1的引用模式,也叫顺序引用模式。

每隔k个元素进行访问,我们称其是步长为k的引用模式。

一般来说,步长越大,空间局部性越差。

再看一个例子:

int sumarrayrows(int a[M][N]){
    int i, j, sum = 0;
    for(i = 0; i < M; i++)
        for(j = 0; j < N; j++)
            sum += a[i][j];
    return sum;
}

我们知道,二维数组是按行放置的,这种写法实际上就是顺序引用模式,具有良好的空间局部性。

而下面这段程序:

int sumarrayrows(int a[M][N]){
    int i, j, sum = 0;
    for(j = 0; j < N; j++)
        for(i = 0; i < M; i++)
            sum += a[i][j];
    return sum;
}

看上去和上面那一段区别不大,但实际上这里已经变成了步长为N的引用模式,空间局部性很差,它的运行效率会比写法慢得多。

取指令的局部性

由于程序指令是放在内存中的,CPU必须读出这些指令,所以我们也希望能评价取指令的局部性。

而对于这部分来说,有循环体,循环体越小,迭代次数越多,其空间局部性和时间局部性越好。

局部性小结

  1. 重复引用相同变量的程序具有良好的时间局部性。
  2. 对于具有k步的引用模式的程序,步长越小,空间局部性越好。反之越差。
  3. 对于取指令来说,循环体越小,迭代次数越多,其空间局部性和时间局部性越好。

存储器层次结构

软件和硬件的这些基本属性互相补充得很完美。利用这种互补关系,我们想到一种组织存储器系统的方法,称为存储器层次结构。

SmartSelect_20200507-141121_Xodo Docs

从高处往低处走,存储设备变得更慢,更便宜和更大。

存储器层次结构中的缓存

高速缓存(cache,读作“cash”)是一种小而快速的存储设备,作为更大,更慢的设备中的数据作缓冲区域。使用高速缓存的过程称为缓存。

SmartSelect_20200507-212202_Xodo Docs

见上图。第K+1层被划分成数据对象组块,称为块,每个块都有唯一的地址和名字。其大小可以固定,也可变。

到了第K层则是被划分成较少的块,块的大小一样,包含着第K+1层的一个子集副本

缓存命中

如果程序需要在第K+1层寻找数据块d,则现在第K层寻找d,如果刚好在第k层,就叫缓存命中。这样读取速度更快。

缓存不命中

与缓存命中相反,并没有在第K层找到d,那我们需要从第K+1层找到d,放到第K层。如果K层已满,则会选择覆盖其中的一个块。

覆盖一个现存的块叫替换或者驱逐,被驱逐的块有时也称为牺牲块。

决定替换哪个块由缓存的替换策略来控制。例如:随机替换策略等。

缓存不命中的种类

区分不同种类的缓存不命中

  • 冷缓存:第K层的缓存是空的。
  • 强制性不命中/冷不命中:由于冷缓存,出现缓存不命中。该情况非常短暂,缓存暖身后就不会出现这种情况。
  • 放置策略:当出现的不命中,第k层的缓存就必须执行放置策略,确定应该放在哪里。
  • 随机替换策略:可以放在第k层的任何块中。速度最优但是实现代价昂贵,定位代价高。
  • 冲突不命中:由于替换策略,块映射到同一个块,在需要一直调用时,会一直发生不命中。
  • 容量不命中:由于缓存太小,没法把数据全部放下,导致出现不命中。

缓存管理

缓存管理是指,我们某个东西要将缓存划分为块,在不同的层之间传送块,并判定命中还是不命中,最后处理它们。

存储器层次概念小结

实际上就是遵循两个局部性设计的存储器层次结构。将时间局部性,空间局部性好的数据放在小但快且接近处理器的地方。

SmartSelect_20200508-155125_Xodo Docs

高速缓存存储器

早期计算机系统存储器层次结构只有三层:CPU寄存器,DRAM主存储器和磁盘存储。

由于CPU和主存之间的性能差距增大,系统设计者在CPU寄存器文件和主存之间插入一个小的SRAM高速缓存器,称为L1高速缓存。

性能差距依然在增大。系统设计者在L1高速缓存与与主存之间又塞了一个更大的高速缓存,称为L2高速缓存。

到现在,可能还包括一个更大的高速缓存,称为L3高速缓存。

通用的高速缓存存储器组织结构

每个存储器地址有m位,形成\(M = 2^m\) 个不同的地址。如下图

SmartSelect_20200508-160723_Xodo Docs

该高速缓存被组织成一个有\(S = 2^s\) 个高速缓存组的数组。

每个组包含E个高速缓存行。

每行由一个\(B=2^b\) 字节数据块组成。

一个有效位标记该行是否包含有有意义的信息。

t个标记位唯一标识存储在这个高速缓存行中的块。

可以用元组\((S, E,B,m)\) 来描述高速缓存的结构。

高速缓存的大小不包括标记位和有效位,高速缓存的容量大小为\(C = S \times E \times B\)

当一条加载指令要从主存地址A中读一个字出来时,CPU将地址A发送给高速缓存。如果高速缓存保存着地址A的副本,他就立即将器发送给CPU。

SmartSelect_20200509-112038_Xodo Docs

参数S和B将m个地址位分为了3个字段。

符号总结:

SmartSelect_20200509-112402_Xodo Docs

直接映射高速缓存

根据每个组的高速缓存行数E,高速缓存被分为不同的类。每个组只有一行的高速缓存称为直接映射高速缓存。

SmartSelect_20200509-113114_Xodo Docs

高速缓存确定一个请求是否命中,然后抽取出被请求的字的过程,分为3步:

  1. 组选择
  2. 行匹配
  3. 字抽取

直接映射高速缓存中的组选择

高速缓存从w的地址中间抽取出s个组索引位。索引位被解释成一组无符号数。就像是数组的索引一样。

SmartSelect_20200509-113948_Xodo Docs

直接映射高速缓存中的行匹配

由于一个组只有一行,当我们完成了组选择,就只需要判断有效位是否有效,有效则命中,无效则不命中。

SmartSelect_20200509-114109_Xodo Docs

直接映射高速缓存中的字选择

一旦命中,最后一步就是确定需要的字在块中是从哪里开始的。

块位移位提供了所需字第一个字节的偏移,就像是第一个字的索引一样。

SmartSelect_20200509-114109_Xodo Docs

直接映射高速缓存中不命中时的行替换

当缓存不命中,那就需要在下一层找到想要的块,然后将其放在组索引位指示的位置上。

运行中的直接映射高速缓存(实例)

我们先假设有一个直接映射的高速缓存:

\[(S,E,B,m) = (4,1,2,4) \]

解释:高速缓存有4个组,每组一行,每个块两个字节,地址4位。

将16个地址一一划分得出:

SmartSelect_20200510-102234_Xodo Docs

对上面的表做一些小小的总结:

  1. 标记位和索引位合起来标识了内存中的每一个块。
  2. 多个块会有同一个索引,即它们有相同的索引值。
  3. 映射到同一个高速缓存组的块由标记位唯一标识。

简单模拟:

初始时,高速缓存长这样:

SmartSelect_20200510-103931_Xodo Docs

  1. 读地址0的字。

    现在组0的有效位是0,不命中。高速缓存从内存中取出块0,存储在组0,再返回内容。

    SmartSelect_20200510-104226_Xodo Docs

  2. 读地址1的字。

    刚刚将组1存好了,这次缓存命中。高速缓存立即返回数据,状态没有变化。

  3. 读地址13的字。

    组2的标记位不是有效的,缓存不命中,加载块6,返回地址13的字。

    SmartSelect_20200510-105234_Xodo Docs

  4. 读地址8的字。

    组8的有效位是有效的,但是标记位不匹配,因此将原数据牺牲掉。

    SmartSelect_20200510-105434_Xodo Docs

  5. 读地址0的字。

    又发生缓存不命中,因为刚刚才把它牺牲掉,发生了冲突不命中,只能再重新复制一份。

    SmartSelect_20200510-105633_Xodo Docs

直接映射高速缓存中的冲突不命中

冲突不命中经常发生,举一个向量点积的函数例子:

float dotprod(float x[8], float y[8]){
    float sum = 0.0;
    int i;
    for(i = 0; i < 8; i++)
        sum += x[i] * y[i];
    return sum;
}

看起来具有良好的空间局部性,实际上并非如此。

我们假设每个块是16字节,整个高速缓存有两组。x和y数组内存是紧跟着的。

SmartSelect_20200510-151812_Xodo Docs

在运行时,先取出x[0] 缓存不命中,在块0中存储x[0] ~ x[3] 。接着引用y[0] 的时候,又是一次缓存不命中,结果由于地址问题,块0被覆盖成y[0] ~ y[3] 。接下来可想而知,x[1] 也同样的不命中。

这种情况就是典型的冲突不命中,,我们描述这种情况为抖动,即高速缓存反覆地加载和驱逐相同的高速缓存块的组。这种抖动导致速度下降2到3倍都不稀奇。

一旦发现抖动现象,程序员也能很好的解决。一个简单的办法就是在数组后面填充B个字节,消除抖动冲突不命中。

SmartSelect_20200510-152755_Xodo Docs

为什么高速缓存的索引不设置在高位

如果将索引设置在高位,那一段连续内存会一直只使用一个块造成冲突不命中而降低效率。

组相联高速缓存

组相联高速缓存放宽了限制,每个组都保存有超过一个的高速缓存行。一个\(1<E<\frac{C}{B}\) 的高速缓存通常称为E路组相联高速缓存。

SmartSelect_20200510-161929_Xodo Docs

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

与直接映射高速缓存的组选择相同,组索引位标识组。

SmartSelect_20200510-162329_Xodo Docs

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

由于组相联高速缓存一个组存放了多个块,行匹配则必须检查多个行的标记位和有效位,才能确定是否在集合中。

SmartSelect_20200510-162650_Xodo Docs

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

如果CPU请求的字不在组里的任何一行,就叫做缓存不命中。

接下来我们需要取出需要的行放在组中。如果组中有空行,那就是一个好的选择。如果并没有空行,我们选择一个没有背经常使用的非空的行。

全相联高速缓存

一个组包含着所有高速缓存行的组,即\(E= C/B\)

SmartSelect_20200510-163738_Xodo Docs

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

它只有一个组,不存在组选择,因此也没有索引位。

SmartSelect_20200510-164039_Xodo Docs

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

它的匹配方式和组相联高速缓存是相同的,主要就是规模问题。

SmartSelect_20200510-164253_Xodo Docs

有关写的问题

高速缓存有关读的操作非常简单,但是写操作就不是那样的了。

假设我们要写一个已经缓存的字,在高速缓存更新了它的w副本,怎么更新w在层次结构中紧接着低一层中的副本?

最简单的方法就是直写,立即将更新的副本写回紧接的低一层。缺点是会引起总线流量。

另一种方法是写回,尽可能推迟更新,只有在更新算法要驱逐这个更新过的块时才把它写到紧接的低一层。它能显著的降低总线流量,但是它的缺点时增加了复杂性,需要额外维护一个修改位。

另一个问题时怎么处理写不命中。

一种方法时,写分配。在低一层加载块到高速缓存,然后更新这个高速缓存块。写分配利用局部性,但是每次不命中都会导致一个块从低一层传送到高速缓存。

另一种方法,称为非写分配,直接把这个字写到低一层中。

直写时非写分配,写回时写分配。

一个真实的高速缓存层次结构的解剖

高速缓存即保存数据,也保存指令。只保存指令的高速缓存称为i-cache ,只保存数据的高速缓存称为d-cache ,既保存指令又包括数据的高速缓存称为统一高速缓存(unified cache)。

现代大部分处理器都有两个独立的高速缓存,原因是处理器可以同时读一个指令字和一个数据字。

缺点是虽然可以确保指令和数据不会形成冲突不命中,但却容易造成容量不命中。

下图是Intel Core i7 处理器的高速缓存层次结构。

SmartSelect_20200511-093101_Xodo Docs

高速缓存参数的性能影响

有许多指标来衡量高速缓存的性能:

  • 不命中率:一个程序执行的过程中,内存引用不命中的比率。
  • 命中率:1-不命中率
  • 命中时间:从高速缓存传送到一个字到CPU所需的时间。
  • 不命中惩罚:由于不命中所需要的额外的时间。

高速缓存大小的影响

一方面,较大的缓存可能会提高命中率。

另一方面,使大存储器运行得更快总是要难一些。

块大小的影响

一方面,较大的块能利用程序中可能存在的空间局部性,帮助提高命中率。

另一方面,较大的块会导致高速缓存的行数较少,这会损害时间局部性。

还有,块越大,传输越慢,会导致不命中的惩罚增加。

相联度的影响

较高的相联度能够降低由于冲突不命中出现的抖动的可能性。而高相联度会导致速度变慢,且实现较难。

最终就是变成了不命中惩罚和命中时间之间的折中。

写策略的影响

高速缓存越往下层,越可能使用写回而不是直写。

编写高速缓存友好的代码

一个好的程序员应该总是试着去编写高速缓存友好的代码。

让常见情况运行的更快

注意力集中在核心函数里的循环上。

尽量减少每个循环内部的缓存不命中数量

不命中较少的循环运行得更快。

int sumvec(int v[N]){
    int i, sum = 0;
    for (i = 0; i < N; i++)
        sum += v[i];
    return sum;
}

观察该函数是否高速缓存友好。

我们假设一个高速缓存的块为B字节,那么一个步长为k的引用模式平均每次循环迭代会有\(min(1, (wordsize \times k)/B)\) 次缓存不命中。

假设V是块对齐的,字是4个字节的,高速缓存块为4个字,初始为空。

SmartSelect_20200511-101414_Xodo Docs

然后整个程序中,每一次不命中,都会让接下来3次都命中。这是我们能做的最好的情况了。

总之:

  1. 对局部变量的反复引用是好的
  2. 步长为1的引用模式是好的

下面讨论一下二维数组:

int sumarrayrows(int a[M][N]){
    int i, j, sum = 0;
    for(i = 0; i < M; i++)
        for(j = 0; j < N; j++)
            sum += a[i][j];
    return sum;
}

假设和上面的函数一样,由于C语言数组是行优先,所以和上面的几乎是同样的情况。

SmartSelect_20200511-110320_Xodo Docs

但我们如果交换了ij

int sumarrayrows(int a[M][N]){
    int i, j, sum = 0;
    for(j = 0; j < N; j++)
        for(i = 0; i < M; i++)
            sum += a[i][j];
    return sum;
}

如果数组不是特别大,我们是可以得到和上面的情况一样的命中率,可是如果数组比较大,那么整个过程就将全部都不命中,效率极低。

SmartSelect_20200511-110634_Xodo Docs

综合:高速缓存对程序性能的影响

存储器山

重新排列循环以提高空间局部性

在程序中利用局部性

posted @ 2020-05-11 17:06  Herman·H  阅读(1403)  评论(0编辑  收藏  举报