最近学习操作系统的内存管理子系统,发现软件上有很多硬件的设计思路在里面,例如TLB等,为了更好的实现理论源于实践,又指导实践的原则,又重新学习了《深入理解计算机系统》的存储器系统,本文主要是对第6章存储层次结构的学习笔记。
1. 存储技术
本节主要介绍SRAM,SDRAM,FLASH以及磁盘这集中存储技术,了解其原理和实现方式
1.1 RAM
RAM(随机访问存储器)分为以下两类:
- SRAM(静态随机访问存储器):用来作为高速缓存存储器,既可以在CPU芯片上,也可以在片下
- DRAM(动态随机访问存储器):用来作为主存以及图像系统的帧缓冲
其SRAM和DRAM存储器特性如下,只要供电,SRAM就会保持不变,而DRAM,需要持续的刷新。SRAM的存取比DRAM更快,对光电噪声不敏感,代价就是SRAM需要更多的每个单位Bit就需要更多的晶体管,那么就产生了更高昂的价格。
每位晶体管数 | 相对访问实践 | 持续的刷新 | 敏感度 | 花费 | 应用 | |
---|---|---|---|---|---|---|
SRAM | 6 or 4 | 1X | 否 | 否 | 100X | 高速缓冲存储器 |
DRAM | 1 | 10X | 是 | 是 | 1X | 主存储器,帧缓冲区 |
1.1.1 SRAM
SRAM之所以被称为“"静态"存储器,是因为只要处于通电状态,里面的数据就可以保持存在。而一旦断电,里面的数据就会丢失了。在SRAM里面,一个bit的数据,需要4~6个晶体管。所以SRAM的存储密度不高,同样的物理空间下,能够存储的数据有限,不过SRAM的电路简单,所以访问速度更快。下面是SRAM的构成图:
具体bit的信息是保存在M1、M2、M3、M4这四个场效应管中。M1和M2组成一个反相器,我们称之C1。Q(有上划线的那个Q)是C1的输出。M3和M4组成另外一个反相器C2,Q是C2的输出。C1的输出连接到C2的输入,C2的输出连接到C1的输入,通过这样的方法实现两个反相器的输出状态的锁定、保存,即储存了1个bit的数据。M5和M6是用来控制数据访问的。
在CPU里,这样的SRAM就称为了我们常接触到的L1,L2,L3这样的三层高速缓存。每个CPU核心都有属于自己的L1的高速缓存,通常分为指令缓存和数据缓存,分开存放CPU使用的指令和数据。对于L2 cache同样是每个CPU核心都有,但是它往往部在CPU核心的内部,所以L2访问速度会比L1稍微慢一点。而对于L3 cache,则通常由多个CPU核心公用,尺寸更大一点,访问速度自然更慢一点。
所以我们可以将CPU中的L1 cache理解为我们大脑中的记忆,把L2 cache理解成我们的口袋,那么L3就理解成我们的书包,把内存当成我们的拥有的书架或者书桌。当我们的需要的时候,依次从大脑–>口袋–>书包–>书桌上寻找。
1.1.2 DRAM
DRAM被称为”动态“存储器,是因为DRAM需要靠不断地刷新,才能保持数据被储存起来。DRAM的一个比特,只需要一个晶体管和一个电容就能存储。所以DRAM在同样的物理空间下,能够存储的数据也就更多,也就存储的密度更大。但是,因为数据存储在电容里,电容会不断的漏电,所以需要定时刷新充电,才能保持数据不丢失。DRAM的数据访问电路和刷新电路都比SRAM更复杂,所以访问延时也就更长。具体的原理可以参考[SDRAM internals],而现在DRAM已经称为我们使用的主存储器了,应用于各式各样的场景中。
1.2 ROM
如果断电,DRAM和SRAM会丢失他们的信息,从这个意义上来说,他们是易失的(volatile)。另一方面,非易失性存储器即使是在关电后,仍然保存着他们的信息。由于历史原因的发展,其发展过程如下:
- Read-only memory(ROM):programmed during production。
- Programmable ROM(PROM):can be programmed once
- Eraseable PROM(EPROM):an be bulk erased (UV, X-Ray)
- Electrically eraseable PROM(EEPROM):electronic erase capability
- Flash memory:是一种非易失性的存储器。在嵌入式系统中通常用于存放系统、应用和数据等。在 PC 系统中,则主要用在固态硬盘以及主板 BIOS 中。另外,绝大部分的 U 盘、SDCard 等移动存储设备也都是使用 Flash Memory 作为存储介质。
- 3D XPonit(Intel Optane) & emerging NVMs:3D XPonit技术将主要面向消费级别和使用机器学习和大数据云应用的企业SSD,游戏同时也是这项技术的受益者之一,相比较现有SSD能够提供5-10倍的性能提升
1.3 传统机械硬盘
我们嵌入式软件工程师了解对于存储接触最多的是Flash,而对于机械硬盘相对默认是一点,但是对于在使用电脑过程中却经常接触,今天我们来了解一下硬盘的组成,创痛的而机械硬盘由以下不同的部件组成
- 磁盘:保存数据的硬盘是由一个个"磁盘"(Platter)组成,每个磁盘都有两面,盘面本省通常是用的铝、玻璃或者陶瓷这样的材质做成的光滑盘片,然后涂上一层磁性的涂层,我们的数据就存储在这样的磁性涂层上
- 磁头:们的数据并不能直接从盘面传输到总线上,而是通过磁头,从盘面读取到,然后再通过电路信号传输给控制电路,接口,再到总线上
- 主轴:磁盘被堆叠在一起沿共同的主轴(Spindle)旋转,这页式我们买硬盘经常听到的一个指标,硬盘转速,我们的硬盘一般由5400转,7200转等,指的是盘面中间电机控制的转轴的旋转速度,也就是每分钟旋转的圈数(PRM)
- 悬臂:悬臂悬挂在磁头上,并且在一定的范围内会取把磁头定位到盘面的的某个特定的磁道上,只要完成径向运行
- 串行接口:通过接口与计算机主板进行连接,硬盘的读取和写入速度与接口有很大关系,最常见的有IDE硬盘接口(133MB/s),SATA接口(理论600MB/s),SCIS接口(理论320MB/s)
每个磁盘的表面是由一组称为磁道(track)的同心圆组成,每一个磁道被划分为一组扇区(sector)。每个包含相同数量的数据位(通常是512个字节),这些数据编码在扇区上的磁性材料中,扇区之间由一些间隙(grap)分隔开,这些间隙不存在数据位,间隙存储用来标识删去的格式化位,如上图中一个盘片的视图。最终一个或者多个叠放在一起的盘片组成,被封装在一个密封的包装中,就形成了磁盘。我们刚才说的一个磁道,会被分成一个个的扇区,那么上下平行的一个个盘面的相同的扇区,我们叫做一个柱面(Cylinder)。
通过我们上面描述的硬盘的几何结构之后,我们可以很快的知道一个磁盘的容量
磁盘容量 = (磁盘盘片数) * (盘片表面数) * (表面磁道数) * (磁道平均扇区数) * (扇区大小)
假设我们有一个磁盘,有5个盘片,盘片有2个面,每个面有20000条磁道,每个磁道平均有300个扇区,每个扇区512个字节,那么这个磁盘的容量为:
磁盘容量的= 5 * 2 * 20000 * 300 * 512 = 30.72GB
下面我们来看看机械硬盘的读写原理,读取数据,与其组成也有关系,那么我们要访问一个数据,就需要知道该数据所在的磁道,所在的扇区,在扇区哪个地方,其读写过程如下示意如下:
磁盘以扇区大小的块来读取数据,所以就分为三部分,寻道,旋转和传送,也就对应了其时间寻道时间,旋转时间,传送时间
-
寻道(定位磁道):为了读取某个目标扇区的内容,传动臂首先将读/写头定位到包含目标扇区的磁道上。这个过程移动传动臂所需要的时间称为寻道时间,寻道时间依赖于读/写头之前的位置和传动臂在盘面上移动的速度,现代驱动器中平均的时间为3~9ms
-
旋转(定位扇区):一旦读/写头定位到了期望的磁道,驱动器等待目标扇区的第一个位旋转到读/写头下,这个步骤的性能依赖于当读写头到达目标扇区时,盘面的位置以及磁盘的旋转速度。旋转时间最坏的情况是,读写头刚好错过了目标扇区,必须等待磁盘旋转一整圈。
-
传送(读写数据):当目标扇区的第一个位位于读/写头下时,驱动器就开始读或者写该扇区的内容,一个扇区的传送时间依赖于旋转速度和每条磁道下的扇区数目。
下面我们来看一次完整的船速过程如下图,总的访问时间位=寻道时间+旋转时间+传输时间
1.4 固态硬盘
固态硬盘是一种基于闪存的存储技术,现在已经在多数情况下完成了对于机械硬盘的取代,其行为跟其他硬盘一样,处理来自CPU的读写逻辑磁盘请求。一个SSD封装由一个或多个闪存芯片和闪存翻译层组成,闪存翻译层是一个硬件/固件设备,扮演与磁盘控制器相同的角色,将对逻辑块的请求翻译成对底层物理设备的访问,其框图如下:
固态硬盘中分成很多的块(Block),每个块又有很多页(Page),大约 32-128 个,每个页可以存放一定数据(大概 4-512KB),页是进行数据读写的最小单位。但是有一点需要注意,对一个页进行写入操作的时候,需要先把整个块清空(设计限制),而一个块大概在 100,000 次写入之后就会报废。
与传统的机械硬盘相比,固态硬盘在读写速度上有很大的优势。但是因为设计本身的约束,连续访问会比随机访问快,而且如果需要写入 Page,那么需要移动其他 Page,擦除整个 Block,然后才能写入。现在固态硬盘的读写速度差距已经没有以前那么大了,但是仍然有一些差距。
1.5 存储器的层级结构
下图是一个典型的存储器层次结构图,一般而言,从高层往底层走,存储设备变得越来越慢,越来越便宜,越来越大。
从cache,内存到SSD和HDD硬盘,一台现代计算机中,就用上了所有这些存储设备。其中容量越小的设备速度越快,而且,CPU并不直接和每一种存储器打交道,而是每一种存储设备,只和它相邻的存储设备打交道。例如,CPU cache是从内存加载而来,或者写回到内存,并不会直接写回数据到硬盘,也不回直接从硬盘加载数据,而是先加载到内存,再从内存加载到cache中。
这样,各个存储器只和相邻的一层存储器打交道,并且随着一层层向下,存储器的容量越来越大,访问速度越来越慢,单位存储成本越来越低,这样就构成了我们日常所说的存储器层次结构,其各个器件对比如下
1.6 高速缓存
高速缓存,是一个小而快速的存储设备,它作为存储在更大,也更慢的设备中的数据对象的缓冲区域,使用高速缓冲的过程称为缓存。
存储器层次结构的中心思想是,对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。换句话说,层次结构中的每一层都缓存来自较低一层的数据对象。
下图展示了存储器层次结构中缓存的一般性概念,第k+1层的存储器被划分成连续的数据对象组块,称为block。每个块都有一个唯一的地址和名称,使之于其他块区别。
例如上图,第k+1层被划分为16个大小固定的块,编号为0~15,第k层的存储器被划分为较少的块的集合,每个块的大小与k+1层的块大小一样。在任何时刻k层缓存包含第k+1层块的一个子集的副本。
- 缓存命中(cache hit):当程序需要第k+1层的某个数据对象时,它首先在当前存储在第k层的一个块中查找。如果刚好缓存在第k层中,那么就是我们所说的缓存命中(cache hit),该程序直接从第k层读取该数据
- 缓存不命中(cache miss):如果第k层中没有缓存数据对象d,那么就是我们所说的缓存不命中(cache miss)。当发生缓存不命中的时候,第k层的缓存从第k+1层缓存中取出包含的数据的那个块,如果第k层的缓存已经满了,可能就需要替换现在的一个块。决定该替换哪个块是由缓存的替换策略来控制的。例如一个具有最近最少未被使用(LRU)替换策略的缓存会选择哪个最后被访问的时间距现在最远的那个块
区分不同种类的缓存不命中有时候能帮忙我们分析一些问题,常见的缓存不命中种类有
- cold cache:缓存是空的和首次引用该缓存
- Conflict miss:缓存冲突Miss,这种情况下,缓存足够大,但是由于算法导致不命中,例如如果程序请求块0,然后块8,然后块0,依此反复,那么就将导致每次引用都会不命中,即使容量足够大也无用
- Capacity miss:当工作集的大小超过缓存的大小时候,缓存会经历容量不命中,换句话说,就是缓存太小了,不能处理这个工作集
存储器层次结构的本质是,每一层存储设备都是较低一层的缓存,在每层上,某种形式的逻辑必须管理缓存。在这里将缓存划分成块,在不同的层之间传递,判断是hit还是miss,并处理他们,管理缓存的逻辑可以是硬件,软件或者是两者的结合。
2. 局部性
一个编写良好的计算机程序常常具有良好的局部性,也就是,它们倾向于引用邻近于其他最近引用过的数据项,或者最近引数据项本身,这种倾向性,被称为局部性原理,是一个持久的概念,对硬件和软件系统的设计和性能都有着极大的影响。
- 时间局部性(Temporal Locality): 如果程序有好的时间局部性,如果程序有好的时间局部性,那么在某一时刻访问的memory,在该时刻随后比较短的时间内还多次访问到
- 空间局部性(Spatial Locality)。如果程序有好的空间局部性,那么一旦某个地址的memory被访问,在随后比较短的时间内,该memory附近的memory也会被访问
2.1 对程序数据引用的局部性
int sumvec(int v[N])
{
int i, sum = 0;
for (i = 0; i < N; i++)
sum += v[i];
return sum;
}
其简单的是对一个向量的元素求和,这个程序有良好的局部性吗?要回答这个问题,我们来看看每个变量的引用模式
- sum变量在每次循环迭代中被引用一次,因此,对于sum来说,有好的时间局部性,因为sum是标量,所以没有空间局部性
- 向量v的元素被顺序的读取,一个接一个,按照它们的存储在内存中的顺序。因此,对于变量v,函数有很好的空间局部性,但是时间局部性很差,因为每个向量元素只被访问一次
所以该函数有良好的局部性,按照上面的例子对数组v进行访问的模式叫做stride-1 reference pattern。下面的例子可以说明stride-N reference pattern:
int sumarraycols(int v[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;
}
二维数组v[i][j]是一个i行j列的数组,在内存中,v是按行存储的,首先是第1行的j个列数据,之后是第二行的j个列数据……依次排列。在实际的计算机程序中,我们总会有计算数组中所有元素的和的需求,在这个问题上有两种方式,一种是按照行计算,另外一种方式是按照列计算。按照行计算的程序可以表现出好的局部性,不过sumarraycols函数中是按照列进行计算的。在内循环中,累计一个列的数据需要不断的访问各个行,这就导致了数据访问不是连续的,而是每次相隔N x sizeof(int),这种情况下,N越大,空间局部性越差。
2.2 取指令的局部性
因为程序指令是存放在内存中的,CPU必须取出这些指令,所以我们也能够评价一个程序关于取指的局部性。例如,for循环体里的指令是按照连续的内存顺序执行,因为循环有良好的空间局部性。因为循环体会被执行多次,所以也有很好的时间局部性。
代码区别在于程序数据的一个重要属性是在运行时,他是不能被修改的额,当程序正在执行时,CPU只从内存中读出它的指令。CPU会少重写或者修改这些指令
在这一节,我们了解了局部性的基本思想,得出了量化评价程序重局部性的一些重要原则
- 重复引用相同变量的程序有良好的时间局部性
- 对于具有步长为k的引用模式的程序,步长越小,空间局部性越好。具有步长为1的引用模式的程序有很好的空间局部性,在内存重以大步长跳来跳去的程序空间局部性会很差
- 对于取指令来说,循环有很好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好