《深入理解计算机系统》(CSAPP)读书笔记 —— 第六章 存储器层次结构

  在计算机系统模型中,CPU执行指令,而存储器系统为CPU存放指令和数据。实际上,存储器系统是一个具有 不同容量、成本和访问时间的存储设备的层次结构
  如果你的程序需要的数据是存储在CPU 寄存器中,那么在指令的执行期间,在0个周期内就能访问到它们。如果存储在 高速缓存中,需要 4~75个周期。如果存储在 主存中,需要 上百个周期。而如果存储在 磁盘上,需要大约 几千万个周期!
  计算机程序的一个基本属性称为 局部性。具有良好局部性的程序倾向于一次又一次地访问 相同的数据项集合,或是倾向于访问 邻近的数据项集合。具有良好局部性的程序比局部性差的程序更多地倾向于从存储器层次结构中较高层次处访问数据项,因此运行得更快。

[TOC]

存储技术

随机访问存储器

  随机访问存储器( Random-Access Memory,RAM)分为两类:静态的和动态的。静态RAM(SRAM)比动态RAM(DRAM)更快,但也贵得多。SRAM用来作为高速缓存存储器。DRAM用来作为主存以及图形系统的帧缓冲区

静态RAM

  SRAM将每个位存储在一个双稳态的( bistable)存储器单元里。每个单元是用一个六晶体管电路来实现的。这个电路有这样一个属性,它可以无限期地保持在两个不同的电压配置( configuration)或状态( state)之一。其他任何状态都是不稳定的,在不稳定状态时,电路会迅速转移到两个稳定状态的一个。

  由于SRAM存储器单元的双稳态特性,只要有电,它就会永远地保持它的值。即使有干扰(例如电子噪音)来扰乱电压,当干扰消除时,电路就会恢复到稳定值。

动态RAM

  DRAM将每个位存储为对一个电容的充电。DRAM存储器可以制造得非常密集。每个单元由一个电容和一个访问晶体管组成。但是,与SRAM不同,DRAM存储器单元对干扰非常敏感。当电容的电压被扰乱之后,它就永远不会恢复了。暴露在光线下会导致电容电压改变。

  下表总结了SRAM和DRAM存储器的特性。只要有供电,SRAM就会保持不变。与DRAM不同,它不需要刷新。SRAM的存取比DRAM快。SRAM对诸如光和电噪声这样的干扰不敏感。代价是SRAM单元比DRAM单元使用更多的晶体管,因而密集度低,而且更贵,功耗更大。

每位晶体管数 相对访问时间 持续的 敏感的 相对花费 应用
SRAM 6 1X 1000X 高速缓存存储器
DRAM 1 10X 1X 主存,帧缓冲区

传统的DRAM

  DRAM芯片中的单元(位)被分成d个超单元( supercell),每个超单元都由w个DRAM单元组成。一个$d \times w$的DRAM总共存储了$dw$位信息。超单元被组织成一个r行c列的长方形阵列,这里rc=d。每个超单元有形如(i,j)的地址,这里i表示行,而j表示列。

  例如,如下图所示是一个16×8的DRAM芯片的组织,有d=16个超单元,每个超单元有w=8位,r=4行,c=4列。带阴影的方框表示地址(2,1)处的超单元。信息通过称为引脚(pin)的外部连接器流入和流出芯片。每个引脚携带一个1位的信号。下图给出了两组引脚:8个data引脚,它们能传送一个字节到芯片或从芯片传出一个字节,以及2个addr引脚,它们携带2位的行和列超单元地址。其他携带控制信息的引脚没有显示出来。

image-20201212092653332

  每个DRAM芯片被连接到某个称为内存控制器( memory controller)的电路,这个电路可以一次传送w位到每个DRAM芯片或一次从每个DRAM芯片传出w位。为了读出超单元(i,j)的内容,内存控制器将行地址i发送到DRAM,然后是列地址j。DRAM把超单元(i,j)的内容发回给控制器作为响应。行地址i称为RAS( Row Access strobe,行访问选通脉冲)请求。列地址j称为CAS( Column Access strobe,列访问选通脉冲)请求。注意,RAS和CAS请求共享相同的DRAM地址引脚。

  例如,要从图6-3中16×8的DRAM中读出超单元(2,1),内存控制器发送行地址2,如下图a所示。DRAM的响应是将行2的整个内容都复制到一个内部行缓冲区。接下来,内存控制器发送列地址1,如下图b所示。DRAM的响应是从行缓冲区复制出超单元(2,1)中的8位,并把它们发送到内存控制器。

image-20201212093003882

  电路设计者将DRAM组织成二维阵列而不是线性数组的一个原因是降低芯片上地址引脚的数量。例如,如果示例的128位DRAM被组织成一个16个超单元的线性数组,地址为0~15,那么芯片会需要4个地址引脚而不是2个。二维阵列组织的缺点是必须分两步发送地址,这增加了访问时间

增强的DRAM

  可以通过以下方式提高访问基本DRAM的速度。

  快页模式DRAM( Fast Page Mode dram, FPM DRAM)。传统的DRAM将超单元的一整行复制到它的内部行缓冲区中,使用一个,然后丢弃剩余的。FPM DRAM允许对同一行连续地访问可以直接从行缓冲区得到服务

假如要读取第4行的3个超单元,传统DRAM需要发出3次RAS,CAS。而FPM DRAM只需要发出一次RAS,CAS,后面跟2个CAS即可。

  扩展数据输出DRAM( Extended Data Out Dram, EDO DRAM)。 FPM DRAM的个增强的形式,它允许各个CAS信号在时间上靠得更紧密一点。

 同步DRAM( Synchronous DRaM, SDRAM)。 SDRAM用与驱动内存控制器相同的外部时钟信号的上升沿来代替许多这样的控制信号。最终效果就是 SDRAM能够比那些异步的存储器更快地输出它的超单元的内容。

  双倍数据速率同步DRAM( Double data- Rate SynchronouS DRAm, DDR SDRAM)。DDR SDRAM是对 SDRAM的一种增强,它通过使用两个时钟沿作为控制信号,从而使DRAM的速度翻倍。不同类型的 DDR SDRAM是用提高有效带宽的很小的预取缓冲区的大小来划分的:DDR(2位)、DDR2(4位)和DDR(8位)。

  视频RAM( Video ram,VRAM)。它用在图形系统的帧缓冲区中。VRAM的思想与 FPM DRAM类似。两个主要区别是:1)VRAM的输出是通过依次对内部缓冲区的整个内容进行移位得到的;2)VRAM允许对内存并行地读和写。因此,系统可以在写下一次更新的新值(写)的同时,用帧缓冲区中的像素刷屏幕(读)。

非易失性存储器

  如果断电,DRAM和SRAM会丢失它们的信息,从这个意义上说,它们是易失的( volatile)。另一方面,非易失性存储器( nonvolatile memory)即使是在关电后,仍然保存着它们的信息。

  对EPROM编程是通过使用一种把1写人 EPROM的特殊设备来完成的。 EPROM能够被擦除和重编程的次数的数量级可以达到1000次。EEPROM能够被编程的次数的数量级可以达到10次。

  闪存( flash memory)是一类非易失性存储器,基于 EEPROM,它已经成为了一种重要的存储技术。

访问主存

  数据流通过称为总线(bus)的共享电子电路在处理器和DRAM主存之间来来回回。每次CPU和主存之间的数据传送都是通过一系列步骤来完成的,这些步骤称为总线事务( bus transaction)。读事务( read transaction)从主存传送数据到CPU。写事务( write trans-action)从CPU传送数据到主存。

  总线是一组并行的导线,能携带地址、数据和控制信号。取决于总线的设计,数据和地址信号可以共享同一组导线,也可以使用不同的。同时,两个以上的设备也能共享同一总线。控制线携带的信号会同步事务,并标识出当前正在被执行的事务的类型。例如,当前关注的这个事务是到主存的吗?还是到诸如磁盘控制器这样的其他I/O设备?这个事务是读还是写?总线上的信息是地址还是数据项?

  展示了一个示例计算机系统的配置。主要部件是CPU芯片、我们将称为IO桥接器(I/ O bridge)的芯片组(其中包括内存控制器),以及组成主存的DRAM内存模块这些部件由一对总线连接起来,其中一条总线是系统总线( system bus),它连接CPU和I/O桥接器,另一条总线是内存总线( memory bus),它连接I/O桥接器和主存。I/O桥接器将系统总线的电子信号翻译成内存总线的电子信号。

image-20201212101344668

局部性

  一个编写良好的计算机程序常常具有良好的局部性( locality)。也就是,它们倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。这种倾向性,被称为局部性原理( principle of locality),是一个持久的概念,对硬件和软件系统的设计和性能都有着极大的影响。局部性通常有两种不同的形式:时间局部性( temporal locality)和空间局部性( spatial locality)。在一个具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不远的将来再被多次引用。在一个具有良好空间局部性的程序中,如果一个内存位置被引用了次,那么程序很可能在不远的将来引用附近的一个内存位置。一般而言,有良好局部性的程序比局部性差的程序运行得更快。

  如下所示的函数sumvec,它对一个向量的元素求和。在这个例子中,变量sum在每次循环迭代中被引用一次,因此,对于sum来说,有好的时间局部性。另一方面,因为sun是标量,对于sum来说,没有空间局部性。

int sumvec(int v[N])
{
    int i,sum = 0;
    for (i = 0; i < N; i++)
        sum += v[i];
    return sum;
}
引用模式:
地址:            0        4        8        12        16
内容:            v0        v1        v2        v3        v4
访问顺序:        1        2        3        4        5

  如上所示,向量v的元素是被顺序读取的,一个接一个,按照它们存储在内存中的顺序(为了方便,我们假设数组是从地址0开始的)。因此,对于变量v,函数有很好的空间局部性,但是时间局部性很差,因为每个向量元素只被访问一次

步长为1的引用模式为顺序引用模式( sequential reference pattern)。一个连续向量中,每隔k个元素进行访问,就称为步长为k的引用模式( stride-k reference pattern)。步长为1的引用模式是程序中空间局部性常见和重要的来源。一般而言,随着步长的增加,空间局部性下降。

  如下的函数 sumarrayrows,它对一个二维数组的元素求和。双重嵌套循环按照行优先顺序(row major order)读数组的元素。也就是,内层循环读第一行的元素,然后读第二行,依此类推。函数 sumarrayrows具有良好的空间局部性,因为它按照数组被存储的行优先顺序来访问这个数组。其结果是得到一个很好的步长为1的引用模式,具有良好的空间局部性。

int sum_array_rows(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;
}
引用模式:
地址:            0        4        8        12        16
内容:            a00        a01        a02        a10        a11
访问顺序:        1        2        3        4        5

存储器层次结构

  存储技术和计算机软件的一些基本的和持久的属性:
  存储技术:不同存储技术的访问时间差异很大。速度较快的技术每字节的成本要比速度较慢的技术高,而且容量较小。CPU和主存之间的速度差距在增大。
  计算机软件:一个编写良好的程序倾向于展示出良好的局部性。

  硬件和软件的这些基本属性互相补充得很完美。它们这种相互补充的性质使人想到一种组织存储器系统的方法,称为存储器层次结构( memory hierarchy),下图展示了一个典型的存储器层次结构。一般而言,从高层往底层走,存储设备变得更慢、更便宜和更大。在最高层(L0),是少量快速的CPU寄存器,CPU可以在一个时钟周期内访问它们。接下来是一个或多个小型到中型的基于SRAM的高速缓存存储器,可以在几个CPU时钟周期内访问它们。然后是一个大的基于DRAM的主存,可以在几十到几百个时钟周期内访问它们。接下来是慢速但是容量很大的本地磁盘。最后,有些系统甚至包括了一层附加的远程服务器上的磁盘,要通过网络来访问它们。

image-20201212112607240

存储器结构中的缓存

  一般而言,高速缓存( cache,读作“cash”)是一个小而快速的存储设备,它作为存储在更大、也更慢的设备中的数据对象的缓冲区域。使用高速缓存的过程称为缓存( caching,读作“ cashing")。

  存储器层次结构的中心思想是,对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。换句话说,层次结构中的每一层都缓存来自较低一层的数据对象。

  数据总是以块大小为传送单元( transfer unit)在第k层和第k+1层之间来回复制的。虽然在层次结构中任何一对相邻的层次之间块大小是固定的,但是其他的层次对之间可以有不同的块大小。如上图所示,L1和L0之间的传送通常使用的是1个字大小的块。L2和L1之间(以及L3和I2之间、I4和I3之间)的传送通常使用的是几十个字节的块。而L5和L4之间的传送用的是大小为几百或几千字节的块。一般而言,层次结构中较低层(离CPU较远)的设备的访问时间较长,因此为了补偿这些较长的访问时间,倾向于使用较大的块。

缓存命中

  当程序需要第k+1层的某个数据对象d时,它首先在当前存储在第k层的一个块中查找d。如果d刚好缓存在第k层中,那么就是我们所说的缓存命中( cache hit)。

缓存不命中

  另一方面,如果第k层中没有缓存数据对象d,那么就是我们所说的缓存不命中( cache miss)。当发生缓存不命中时,第k层的缓存从第k+1层缓存中取出包含d的那个块,如果第k层的缓存已经满了,可能就会覆盖现存的一个块。(缓存的替换策略:随机替换替换策略,最少被使用(LRU)替换策略)。

缓存不命中种类

  区分不同种类的缓存不命中有时候是很有帮助的。如果第k层的缓存是空的,那么对任何数据对象的访问都会不命中。一个空的缓存有时被称为冷缓存( cold cache),此类不命中称为强制性不命中( compulsory miss)或冷不命中( cold miss)。冷不命中很重要,因为它们通常是短暂的事件,不会在反复访问存储器使得缓存暖身( warmed up)之后的稳定状态中出现。

缓存管理

  存储器层次结构的本质是,每一层存储设备都是较低一层的缓存。在每一层上,某种形式的逻辑必须管理缓存。这里,我们的意思是指某个东西要将缓存划分成块,在不同的层之间传送块,判定是命中还是不命中,并处理它们。管理缓存的逻辑可以是硬件、软件,或是两者的结合。

高速缓存存储器

  高速缓存关于读的操作非常简单。首先,在高速缓存中查找所需字$w$的副本。如果命中,立即返回字$w$给CPU。如果不命中,从存储器层次结构中较低层中取出包含字$w$的块,将这个块存储到某个高速缓存行中(可能会驱逐一个有效的行),然后返回字$w$。

  写的情况就要复杂一些了。假设我们要写一个已经缓存了的字$w$(写命中, write hit)。在高速缓存更新了它的$w$的副本之后,怎么更新$w$在层次结构中紧接着低一层中的副本呢?最简单的方法,称为直写( write-through),就是立即将$w$的高速缓存块写回到紧接着的低一层中。虽然简单,但是直写的缺点是每次写都会引起总线流量。另一种方法,称为写回( write-back),尽可能地推迟更新,只有当替换算法要驱逐这个更新过的块时,才把它写到紧接着的低一层中。由于局部性,写回能显著地减少总线流量,但是它的缺点是增加了复杂性。高速缓存必须为每个高速缓存行维护一个额外的修改位( dirty bit),表明这个高速缓存块是否被修改过。

  另一个问题是如何处理写不命中。一种方法,称为写分配( write-allocate),加载相应的低一层中的块到高速缓存中,然后更新这个高速缓存块。写分配试图利用写的空间局部性,但是缺点是每次不命中都会导致一个块从低一层传送到高速缓存。另一种方法,称为非写分配(not- write-allocate),避开高速缓存,直接把这个字写到低一层中。直写高速缓存通常是非写分配的。写回高速缓存通常是写分配的。

  高速缓存既保存数据,也保存指令。只保存指令的高速缓存称为 i-cache。只保存程序数据的高速缓存称为 d-cache。既保存指令又包括数据的高速缓存称为统一的高速缓存( unified cache)。现代处理器包括独立的 i-cache和d-cache。这样做有很多原因。有两个独立的高速缓存,处理器能够同时读一个指令字和一个数据字。 i-cache通常是只读的,因此比较简单。通常会针对不同的访问模式来优化这两个高速缓存,它们可以有不同的块大小,相联度和容量。使用不同的高速缓存也确保了数据访问不会与指令访问形成冲突不命中,反过来也是一样,代价就是可能会引起容量不命中增加。

编写高速缓存友好的代码

  确保代码高速缓存友好的基本方法。
  1)让最常见的情况运行得快。程序通常把大部分时间都花在少量的核心函数上,而这些函数通常把大部分时间都花在了少量循环上。所以要把注意力集中在核心函数里的循环上,而忽略其他部分。
  2)尽量减小每个循环内部的缓存不命中数量。在其他条件(例如加载和存储的总次数)相同的情况下,不命中率较低的循环运行得更快。

  考虑如下的函数

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

  首先,注意对于局部变量i和sum,循环体有良好的时间局部性。现在考虑一下对向量v的步长为1的引用。一般而言,如果一个高速缓存的块大小为B字节,那么一个步长为k的引用模式(这里k是以字为单位的)平均每次循环迭代会有$\min (1,(wordsize \times k)/B)$次缓存不命中。当k=1时,它取最小值,所以对v的步长为1的引用确实是高速缓存友好的。

  例如,假设v是块对齐的,字为4个字节,高速缓存块为4个字,而高速缓存初始为空(冷高速缓存)。在这个例子中,对v[0]的引用会不命中,而相应的包含v[0] ~v[3]的块会被从内存加载到高速缓存中。因此,接下来三个引用都会命中。对v[4]的引用会导致不命中,而个新的块被加载到高速缓存中,接下来的三个引用都命中,依此类推。总的来说,四个引用中,三个会命中,在这种冷缓存的情况下,这是我们所能做到的最好的情况了。

  总之,简单的 sumvec示例说明了两个关于编写高速缓存友好的代码的重要问题:第一,对局部变量的反复引用是好的,因为编译器能够将它们缓存在寄存器文件中(时间局部性)。第二,步长为1的引用模式是好的,因为存储器层次结构中所有层次上的缓存都是将数据存储为连续的块(空间局部性)。

总结

  本章主要介绍了各种各样的存储系统及其原理,一般来说,较小、较快的设备在顶部,较大、较慢的设备在底部。因为编写良好的程序有好的局部性,大多数数据都可以从较高层得到服务,结果就是存储系统能以较高层的速度运行,但却有较低层的成本和容量。我们可以通过编写有良好空间和时间局部性的程序来显著地改进程序的运行时间。例如,可以利用基于SRAM的高速缓存存储器。主要原因是从高速缓存取数据的程序比主要从内存取数据的程序运行得快得多。

  养成习惯,先赞后看!如果觉得写的不错,欢迎关注,点赞,转发,谢谢!
如遇到排版错乱的问题,可以通过以下链接访问我的CSDN。

**CSDN:[CSDN搜索“嵌入式与Linux那些事”]

你可能感兴趣的:(c)