内存的物理结构
这次要介绍的内存是DRAM,DRAM的全拼是Dynamic Random Access Memory,这是一种上电使用,断电数据就消失的Memory。先看一下内存的外部结构
如上图照片,内存条上一块黑色的芯片称为一个chip,8个chip组成一个rank。按照常理,一个chip的存储容量是1GB,所以这应该是一个8GB的内存条。
再看一个chip的内部结构
如上图,一个chip内部有8个bank,每个bank可以看成存储数据的最小单元,一个bank内部在寻址上分为row和column,一个确定的row和column就唯一确定8bit数据,8个bank就唯一确定64bit数据。每个bank下有一个row buffer用来缓存读出的数据。
内存读取数据
大家都知道CPU和内存之间是总线相连,CPU会将想要的数据的内存寻址地址(row、column)发送给总线,总线再将该地址发送给内存,内存取到数据再将数据放到总线上,CPU再从总线上获得数据。
CPU --------BUS--------> (Address) RAM
CPU (Data) <--------BUS------------ RAM
整个上面这个过程,在计算机的体系结构中都是由系统时钟控制的。
在RAM控制器这一边,过程大概是这样的:
1.CPU和BUS通信,物理地址放到总线;
2.BUS和内存芯片通信,总线把地址传给芯片组;
3.芯片组对内存寻址和读取数据
a.内存模块确定chip
b.内存芯片行地址预充电 ,需要tRP个周期(time of Row Precharge)
c.锁定行到等待列地址,需要tRCD个周期(tiime of RAS to CAS Delay)
d.锁定列到响应数据,需要tCL个周期(time of CAS Latency)
4.数据送到BUS上,CPU读取。
读取数据,如下图所示
从图中可以看到,64bit数据,每8bit存储在一个bank上,64bit并不是连续的。
内存对齐
以前在参见面试的时候经常遇到这样的问题,为什么要在编程的时候要做到内存对齐?
从上面的分析过程中,1次内存取数据能够得到64bit的数据,如果我们取0-63bit的数据,1次内存取数据OK,我们取64-127bit的数据,1次内存取数据也OK,但是如果我们想获得60-77bit的数据,那么就要进行2次内存取数据,那这效率可就下来了!
时钟周期
对于时钟再多说一下,在电路设计中,时钟是一个最为基础也是最为重要的概念,电子电路每个模块都有自己的动作,每个元器件完成动作的时间是未知的,如何让他们步调统一的工作,这时候时钟就派上用场了。时钟就是最最基本的节拍器。
像DRAM Controller这样复杂的模块,有时候一个大的动作的同步可能一个时钟周期还不OK,需要多个时钟周期。
对于上面提到几个时钟,如下表
tCL | tRCD | tRP | tRAS | |
全称 | CAS Latency | RAS to CAS Delay | RAS Precharge | Act-to-Precharge Precharge Delay |
描述 | 发送列地址到数据响应的延时 | 行地址到列地址的延迟 | 行地址控制器预充电的时间 | 前后两次IO,如果行地址变化,读数据至少需要的周期数 |
周期 | 7 | 7 | 7 | 24 |
其中:
CAS:列地址选通脉冲
RAS:行地址选通脉冲
有了上面的知识就可以计算:
假设内存的IO频率是533Mhz
case1 :在连续两次的取数据的过程中,行地址(row)不变,列地址(column)改变,则第2次内存取数据的时间
因为连续内存的行地址不变,则在第2次内存取数据的时候不再需要行地址控制预充电的时间(tRP)和行地址到列地址的延迟(tRCD),只需要经历发送列地址到数据响应的延时(tCL)即可。
所以,t = 7 * 1 / 533Mhz = 13.13ns
case2:在连续两次的取数据的过程中,行地址(row)改变,列地址(column)改变,则第2次内存取数据的时间
因为这次行地址也改变,列地址也改变,所以要经历行地址控制器预充电的时间(tRP)、行地址到列地址的延迟(tRCD)和发送列地址到数据响应的延时(tCL),这一共是21个时钟周期。前后两次IO,如果行地址变化,需要重新预充电,读数据至少需要24个周期(tRAS)。
所以,t = 24 * 1 / 533Mhz = 45ns。
所以能够看到即使对于DRAM这种高速的存储器,随机存取和顺序存取的效率也是差了好多,顺序要快3倍以上!
注意,因为CPU有Cache,在现代计算机体系结构中,为了提高效率,CPU和内存的IO在实际中都是以64字节为单位进行的(程序运行的局部原理)!
再说时钟周期
内存的发展,现在也有几代了,从SDR到DDR,DDR到DDR2,再到DDR3,其频率变化如下表所示
这里面我想说两点:
1.可以看到核心频率在几代中并没有明显的提升,那是因为核心频率,是计算机主板的时钟的震荡频率,这个时钟提升是很难的。
2.但是从I/O频率和等效频率来看还是有提升的,这是因为主要在两处做了优化:1.由之前只在时钟上升沿取数据,到改进后上升沿下降沿皆可以取数据;2.预取更多的数据达到的。
测试
下面通过一组测试来检验内存不同的随机IO方式,对性能的影响。
代码如下:
#include <time.h>
#include <iostream>
#include <random>
using namespace std;
const long long SIZE = 8 * 1000 * 1000;
const long long LOOP = 1000;
const int STEP = 1;
int main(int argc, char* argv[])
{
long long *p = new long long[SIZE];
long long a = 0;
long long start = clock();
for (int i = 0; i < LOOP; i++){
for (int j = 0; j < SIZE; j += STEP){
a += p[j];
}
}
long long end = clock();
long long div = LOOP * (SIZE/STEP);
double t = (double)(end - start) * 1000 / div;
cout << "One get memory time : " << t << " ns" << endl;
}
a.这里面进行1000次循环试验,取均值,防止一些扰动。
b.在内存new出来的空间为8 * 8 * 1000 * 1000 = 64MB,之所以取这么大,是防止CPU Cache的干扰。
STEP分别取1、8、32、64、128、256、512、1024,得到的实验结果如下表所示
STEP | 1 | 8 | 32 | 64 | 128 | 256 | 512 | 1024 |
Time(ns) | 1.41 | 3.10 | 5.66 | 6.38 | 8.20 | 8.71 | 9.65 | 10.15 |
可以看到空间取值随着步长一步步被打乱,取数据的时间也一点点增大。
随机乱序取数据
代码如下:
#include <time.h>
#include <iostream>
#include <random>
using namespace std;
const long long SIZE = 8 * 1000 * 1000;
const long long LOOP = 1000;
int main(int argc, char* argv[])
{
default_random_engine generator;
uniform_int_distribution<int> distribution(0, SIZE - 1);
auto dice = bind ( distribution, generator );
long long *p = new long long[SIZE];
long long a = 0;
long long start = clock();
for (int i = 0; i < LOOP; i++){
for (int j = 0; j < SIZE; j++){
a += p[dice()];
//dice(); /*单独执行,计算随机数的影响*/
}
}
long long end = clock();
long long div = LOOP * SIZE;
double t = (double)(end - start) * 1000 / div;
cout << "One get memory time : " << t << " ns" << endl;
}
随机取内存数据的时间为163.96 ns,随机数的时间为90.00 ns,那么得到内存的取数据时间为73.96ns
这个和上面计算的45ns有一些差距,我想可能由于以下原因:
1.逻辑地址到物理地址转换的时间(这个是在CPU里面执行的,应该很快);
2.CPU发地址给总线,CPU从总线上接收到数据(这也需要几个时钟周期);
3.tRAS=24是最小的周期数,可能在某些时候在内存中取数据要大于24。
备注:
以上是我对内存这块知识的理解和自己的总结,肯定有些不对的地方,希望大家指正,谢谢。