• 存储器层次结构


    存储器层次结构

    存储技术

    计算机技术的成功很大程度来源于存储技术的巨大进步。早期的电脑甚至没有磁盘。现在电脑上的磁盘都已经按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

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

    存储器山

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

    在程序中利用局部性

  • 相关阅读:
    服务器监控利器
    退出率与跳出率
    PHP替换中文字符
    编码问题导致样式显示在IE中不正常
    ADO.NET
    生成n*n蛇形矩阵的算法
    数组地址问题
    数组的首地址,数组名取地址,地址的强制转换为int
    教程:VS2010 之TFS入门指南
    10进制与17进制的转化(代码已测试)
  • 原文地址:https://www.cnblogs.com/hyong-bingbing/p/12870263.html
Copyright © 2020-2023  润新知