最近的工作想基于Intel的AEP非易失内存的物理特性做个读写优化,哪想到这方案还没具体定下来,Intel就宣布要放弃生产所有Optane的产品了,犹如做安倍政治学的成果还没整出来,人就敞开胸怀了。
为了试图让自己做的前期工作不至于白费,必须得想个办法把这个方案从具体硬件上剥离下来,即抽出硬件上的特性作为研究点,而不要将整个方案完全依托在硬件展示出的读写特性上。
因为读写特性大多与底层结构有关,因而有必要了解硬件的底层结构。这里先从数据的底层组织入手,去看了一下之前一直忽略的DRAM的底层结构。
通常来说,DRAM可以分为五个层次:(Row
/Column
)/Array
/Bank
/Rank
/Channel
,分别对应其中物理结构的特性。
Row
和Column
对应的是DRAM最底层的硬件结构,对应的都是一组一维的线性存储结构。首先要知道的是,现有的DRAM都是呈二维方形排布的,因而可以像经典笛卡尔坐标系一样使用横纵坐标对每个存储单元寻址,而Row
与Column
恰好对应这两个坐标量,它们的功能完全不同。从组成DRAM的基本结构上来看,Row
连接的是一组存储单元的源极,而Column
连接的是一组存储单元的栅极,这也就决定了他们各自的作用:Row
线用于存取DRAM电容上的0/1数据,而Column
线则负责从Row
线上选出需要导通用于传输数据的存储单元。
这里简要补充一个题外话,DRAM电容存储的电量有限,那么怎么有效地读取数据呢?
DRAM采用了一种非常骚的方法:反正万物皆可电容,能带电就有电压,那我让Row
线充电到工作电压的一半(介于0/1之间),然后将Row
线悬空,这样由于接通后存储单元的电压非高即低,那么Row
线上的电压在短暂的接通后肯定会发生变化,利用电压差即可判断电容内的数据原本是高电平还是低电平了。那么与哪来的参考电压比较呢?DRAM将Row
线一分为二,将电平读取电路放在中间,反正你一次只读取一位数据,总有一头是保持了原滋原味没动的,可以作为参考电压。
接着,Row
和Column
组成的二维阵列就称为Array
,一次可以从一个Array
中读取的数据量为1bit。由于一个Column
要占用的资源远大于一个Row
(电压比较电路、buffer等),因而在大多数现代DRAM芯片设计中,一个Column
连接的单元数是远大于一个Row
的。(PS. 一个DRAM读写的完整流程可分为6个阶段:解码要访问的Row地址-Column线预充电(precharge,1/2工作电压)-电压比较-数据写入buffer-根据Column地址返回要读取的bit-根据buffer数据写回数据单元)
那么,我要字节寻址(一次读写1Byte)怎么办?直接整8个Array
并行放置不就好了。这就是Bank
的定义:一次可从一个Bank
中读取的数据大小位1Byte。而由于一个Bank
通常较小,我们平常见到内存条上的一个芯片内部都是封装了多个(4/8/16)Bank
的。
然后通常一个内存条上有8个芯片,可以分别同时读取,这个单位叫做Rank
;而内存条级别的并行就是Channel
,也就是平常常说的单通道双通道什么的,读写带宽翻倍带来的是实打实的性能提升。
对于内存条而言,通常有DIMM这个说法,即Dual-Inline Memory Module。那么这个“双”体现在哪里呢,双面都贴芯片吗?答案是针脚。仔细观察,可以发现现有内存条两面都有金手指,并且是不互通的,相当于针脚数是我们一次可以看到的数量的两倍。而早期内存条通常是SIMM,也就是说内存条两面的金手指电气功能是一致的,这便是DIMM中这个“Dual”的含义。(PS. 与体系结构相对应,SIMM单次读取宽度位32bit,DIMM则为64bit)
但是,光这么看的话,似乎还是与现代CPU 64Byte的缓存行相差甚远(即便是4-Channel也只是每周期32Byte数据)。原本内存频率就要低于CPU,结果数据速率还跟不上,听起来似乎还是有点问题。
这个时候就可以将视野重新拉回Array
层次了:我们可以发现,每次为了取1bit数据,都要经过完整的从预充电到写回的流程。然而,实际上在整个流程中,读取数据占用的周期数反而很小,频繁对Row线做激活读数据操作浪费了大量的时钟周期。
那么怎么办呢?考虑到程序执行正好有空间局部性,实际上连续读取内存区域(尤其是在没有分支或跳转的代码段的情况下)的概率是很高的,那么在空间上做预取是可能具有性能提升的可能的。而恰好我们可以发现,实际上在Row
内部,一次读取的数据是物理地址上连续的,恰好满足连续地址预取的要求——那么我在每个内存读写周期内,多传输几个数据,不就能提升数据传输效率了吗?这个想法就是DRAM中的Burst Mode,通过一次预充电一口气读写多个DIMM宽度的方式实现了读写带宽的提升。
与想法完全相同的是,我们可以直接在芯片内添加缓冲区来将我们一次读取的多个数据打包带走。考虑到在Array
层面以bit为单位做缓存对设计复杂度影响大,在实际的实现中,是把这个缓冲区放在了Bank的层面,将8个Array读出来的8个bit缓存为一个byte(写则相反,在写回之前多改几个bit)。根据规范,DDR2一次可连续从一个Array
中读取4bit,DDR3/DDR4是8bit。可以看到,从DDR3开始,我们就实现了一次内存读取可以填满一个CPU缓存行的功能了。
这里顺便提一下Bank Interleaving这个东西,类似于RAID3/4/5,把地址连续的数据拆分放在了不同的物理介质上(如Chip0存地址0/4,Chip1存1/5,Chip2存2/6,Chip4存3/7),两者都含有充分利用多份物理设备的带宽的含义在内。只不过在Bank这里,由于读写完数据后还要加电刷回存储单元,要占据缓冲区并消耗多个时钟周期,导致在两次内存访问之间是有一定的绝对不应期的(雾)。使用了Bank Interleaving的话,相当于就给这些Bank
建立了一个流水线,在读完上一个地址的数据后,下一个地址的预充电操作可能就已经完成,可以直接读取数据了,不需要再像单Bank场景一样,在两次内存访问之间白白等一个内存访问周期的时间。如果Bank数量和Burst长度都是2的幂指数倍的时候,还能再二进制地址上进一步做小动作来优化实现,这个就不展开说了。