关于c:深入理解计算机系统CSAPP读书笔记-第六章-存储器层次结构

7次阅读

共计 7989 个字符,预计需要花费 20 分钟才能阅读完成。

  在计算机系统模型中,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 位的行和列超单元地址。其余携带管制信息的引脚没有显示进去。

  每个 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 位,并把它们发送到内存控制器。

  电路设计者将 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 桥接器将系统总线的电子信号翻译成内存总线的电子信号。

局部性

  一个编写良好的计算机程序经常具备良好的局部性(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 的主存,能够在几十到几百个时钟周期内拜访它们。接下来是慢速然而容量很大的本地磁盘。最初,有些零碎甚至包含了一层附加的近程服务器上的磁盘,要通过网络来拜访它们。

存储器构造中的缓存

  一般而言,高速缓存(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 那些事”]

正文完
 0