内存底层原理
内存底层结构
由于高速缓存的工作机制,内存对齐后性能高。且底层实现中内存的IO是以8个字节64bit为单位进行的。
内存物理结构:一个内存是由若干个黑色的内存颗粒构成的。每一个内存颗粒叫做一个chip。每个chip内部,由8个bank组成。每一个bank是一个二维平面上的矩阵。矩阵中每一个元素中都是保存了1个字节,也就是8个bit
内存编址
对于在应用程序中内存中地址连续的8个字节,例如0x0000-0x0007,是从位于bank上的呢?直观感觉,应该是在第一个bank上吗?其实不是的,程序员视角看起来连续的地址0x0000-0x0007,实际上是位于8个bank中的,每一个bank只保存了一个字节。在物理上,他们并不连续。下图很好地阐述了实际情况。
编址原因:是电路工作效率。内存中的8个bank是可以并行工作的。如果想读取地址0x0000-0x0007,每个bank工作一次,拼起来就是需要的数据,IO效率会比较高。但要存在一个bank里,那这个bank只能自己干活。只能串行进行读取,需要读8次,这样速度会慢很多。
内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的。 对于64位数据宽度的内存,假如cpu也是64位的cpu(现在的计算机基本都是这样的),每次内存IO获取数据都是从同行同列的8个bank中各自读取一个字节拼起来的。从内存的0地址开始,0-7字节的数据可以一次IO读取出来,8-15字节的数据也可以一次读取出来。
假如指定要获取的是0x0001-0x0008,也是8字节,但是不是0开头的,内存需要怎么工作呢?没有好办法,内存只好先工作一次把0x0000-0x0007取出来,然后再把0x0008-0x0015取出来,把两次的结果都返回。CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。
扩展
事实上,编译和链接器会自动替开发者对齐内存的,尽量保证一个变量不跨列寻址。
其实在内存硬件层上,还有操作系统层。操作系统还管理了CPU的一级、二级、三级缓存。高速缓存里的Cache Line是64字节,它是内存IO单位的8倍,不会让内存IO浪费。
内存访问延迟
内存延迟一般是通过CL-tRCD-tRP-tRAS四个参数来标识的。详细理解一下这四个参数的含义:
- CL(Column Address Latency):发送一个列地址到内存与数据开始响应之间的周期数
- tRCD(Row Address to Column Address Delay):打开一行内存并访问其中的列所需的最小时钟周期数
tRP(Row Precharge Time):发出预充电命令与打开下一行之间所需的最小时钟周期数。
tRAS(Row Active Time):行活动命令与发出预充电命令之间所需的最小时钟周期数。也就是对下一次预充电时间进行限制。
除了CL是固定周期数以外,其它的三个都是最小周期。另外上面的参数都是以时钟周期为单位的。因为现代的内存都是一个时钟周期上下沿分别各传输一次数据,所以用Speed/2就可以得出,如果机器的Speed是1066MHz,则时钟周期为533MHz。自己的机器可以通过dmidecode命令查看:
1 | dmidecode | grep -P -A16 "Memory Device" |
四个工作场景
- 场景1:
进程需要内存地址0x0000为的一个字节的数据,CPU这时候向内存控制器发出请求,内存控制器进行行地址的预充电,需要等待tRP个时钟周期。再发出打开一行内存的命令,又需要等待tRCD个时钟周期。接着发送列地址,再等待CL个周期。最终将0x0000-0x0007的数据全部返回给了CPU。CPU把这些数据放入到了自己的cache里,并帮你开始对0x0000的数据进行运算。
- 场景2:
进程需要内存地址0x0003的一个字节数据,CPU发现发现它在自己的cache里存在,直接使用就好了。这个场景里其实根本就没有内存IO发生。
- 场景3:
进程需要内存地址0x0008的一个字节数据,CPU的cache并没有命中,于是向内存控制器请求。内存控制器发现行地址和上一次工作的行地址一致,这次只需要发送列地址后等待CL个周期,就可以拿到0x0008-0x0015的数据并返回给CPU了。
- 场景4:
进程需要内存地址0xf000的一个字节数据,同样CPU的cache并不命中,向内存控制器请求。内存控制器一看(内心有些许的郁闷),这次行w地址又变了,得,和场景1一样。继续等待tRP+tRCD+CL个周期后,才能够取到数据并返回。
实际的计算机的内存IO过程中还需要进行逻辑地址和物理地址的转换,这里忽略。
实际计算
内存也存在和磁盘一样,随机IO比顺序IO要慢的问题。如果行地址同上一次访问的不一致,则需要重新拷贝row buffer,延迟周期需要tRP+tRCD+CL。而如果是顺序IO的话(行地址不变),只需要CL个周期既可完成。
估算内存的延时,若测试机器上的内存参数Speed为1066MHz(通过dmidecode查得),该值除以2就是时钟周期的频率=1066/2=533Mhz。其延迟周期为7-7-7-24。
- 随机IO:这种状况下需要tRP+tRCD+CL个时钟周期,7+7+7=21个周期。但是还有个tRAS的限制,两次行地址预充电不得小于24。所以得按24来计算,24*(1s/533Mhz) = 45ns
- 顺序IO:这种状况下只需要CL个时钟周期 7*(1s/533Mhz)=13ns
扩展:CPU的cache line虚拟内存概念
因为对于内存来说,随机IO一次开销比顺序IO高好几倍。所以操作系统在工作的时候,会尽量让内存通过顺序IO的方式来进行。做法关键就是Cache Line。当CPU发现缓存不命中的时候,实际上从来不会向内存去请求1个字节,8个字节这种。而是一次性就要64字节,然后放到自己的Cache中存起来。
用上面的例子来看,
- 如果随机请求8字节:耗时是45ns
- 如果随机请求64字节:耗时是45+7*13 = 136ns
开销也没贵多少,因为只有第一个字节是随机IO,后面的7个字节都是顺序IO。数据是8倍,但是IO耗时只有3倍,而且取出来的数据后面大概率要用,所以计算机内部就这么搞了,通过这种方式避免一些随机IO!
另外,内存也支持burst(突发传输)模式,在这种模式下可以只传入一次行列地址,就命令内存返回该内存开头的连续字节数据,比如64字节。这种模式下,只有第一次的8字节需要真正的行列访问延迟,后面的7个字节可以直接按内存的数据频率给吐出来。
内存核心频率
内存真正的工作频率是核心频率,时钟频率和数据频率都是在核心频率的基础上,通过技术手段放大出来的。内存越新,放大的倍数越多。 核心频率已经多年没有实质性进步了,这是受物理材料的极限限制,内存的核心频率一直在133MHz~200MHz之间徘徊。
实际的内存提速使用的是电路时钟周期预取以及Bank Group等技术。
扩展:内存延迟
内存还有个概念叫IO频率、也叫时钟频率。简单理解为将DDR内存的Speed频率除以2,就是内存的IO频率。这个必须和CPU的外频相匹配才能工作。例如对于DDR3来说,假如核心频率133Mhz的内存工作频率下,匹配533MHz的CPU外频,其IO频率就是533Mhz。数据传输因为上下沿都可以传,所以是核心频率的8倍,也就是1066MHz左右。
所有的内存条都有CL-tRCD-tRP-tRAS四个参数。其中最重要的是CL-tRCD-tRP这三个参数,只要费点劲,所有的在售内存你都能找到这3个值。例如经典的DDR3-1066、DDR3-1333及DDR3-1600的CL值分别为7-7-7、8-8-8及9-9-9。现在京东上一条比较流行的台式机内存金士顿(Kingston)DDR4 2400 8G,其时序是17-17-17。
第四个参数有时候会被省略。原因有二,第一:现在的开发者不需要直接和内存打交道,而操作系统呢又做的比较内存友好,很少会有这个开销真正发生。第二,这个开销的值要比其它的值大很多,实在不太好看。商家为了内存能多卖一些,干脆就避而不谈了。
好了,问题来了。为什么内存越进步,延迟周期反而会变大了呢?
这就是因为延迟周期使用延迟时间除以内存Speed算出来的。这其实根本就不科学,最科学的办法应该是用延迟时间来评估。延迟时间很大程度上是受内存的核心频率的制约的。而这些年核心频率又基本上没有进步,所以延迟时间也不会有实质的降低。内存的制造商们又为了频率数据好看,能多卖些内存,非得采用Speed作为主周期来用。导致在用这个周期一衡量,貌似延迟周期就越来越大了。
测试参考
在各种情况下进行内存访问延迟实验。最快的情况下其实不是内存在IO,而是CPU的高速缓存在工作。穿透到内存的话,顺序IO延时大约在10ns左右,随机访问确实比顺序访问慢的多,大概是4倍的关系。
现代的服务器里,CPU和内存条都有多个,它们之间目前主要采用的是复杂的NUMA架构进行互联,NUMA把服务器里的CPU和内存分组划分成了不同的node。属于同一个node里的CPU和内存之间访问速度会比较快。而如果跨node的话,则需要经过QPI总线,总体来说,速度会略慢一些。
参考
https://mp.weixin.qq.com/s/F0NTfz-3x3UxQeF-GSavRg
https://mp.weixin.qq.com/s/ps8VfGpbL4-xKggM2ywjHw