《深刻理解计算机系统》(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存储器单元对干扰很是敏感。当电容的电压被扰乱以后,它就永远不会恢复了。暴露在光线下会致使电容电压改变。dom

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

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

传统的DRAMide

  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<N;i++)
        sum +=v[i];
    return sum;
}

  首先,注意对于局部变量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那些事”]

相关文章
相关标签/搜索