《深入理解Linux进程与内存》学习笔记——内存硬件原理

一、CPU对内存的硬件支持

在这里插入图片描述

该图是CPU的总架构图,CPU有两个内存控制器(IMC),每个内存控制器上都有一个DDR PHY,DDR PHY是连接DDR内存条和内存控制器的桥梁。

每个DDR PHY有三个DDR4通道,每个通道有两个内存插槽,可以连接2条DIMM。DIMM是现代最常用的内存模块规格,即双列直插式内存模块。

二、内存硬件内部结构

内存条上每个内存颗粒叫一个Chip,Rank指的是同一组Chip的集合。同一个Rank的Chip并行工作,每个Chip的比特位共同组成64比特的数据。

假设某条内存正面有字符:16 GB 2R×8

2R表示该内存有2个Rank,×8表示每个每个内存颗粒的位宽是8比特,由于CPU每次要同时读写64比特,所以:

  • 位宽为4比特的颗粒,需要16个Chip组成一个Rank
  • 位宽为8比特的颗粒,需要8个Chip组成一个Rank
  • 位宽为16比特的颗粒,需要4个Chip组成一个Rank
    chip内部bank
    每个Chip内部是由一层层Bank组成的,Bank内部是电容的二维行列矩阵结构。
    bank
    矩阵由多个方块元素组成,这个方块是最小的内存管理单位,叫内存颗粒位宽。

三、内存IO原理

(1)内存延迟

内存IO工作过程中,有几个比较重要的参数:

  • CL:发送一个列地址到内存与数据开始响应之间的周期数。
  • tRCD:打开一行内存并访问其中的列的最小周期数。
  • tRP:发出预充电命令和打开下一行之间所需的最小时钟周期数。
  • tRAS:行活动命令与发出充电命令之间的最小时钟周期数。

(2)内存IO过程

加入进程要读取内存地址0x0000的一个字节的数据,需要经过如下步骤:

  1. 内存控制器对每个Chip中指定行地址进行充电,需要tRP个时钟周期。
  2. 内存控制器向每个Chip发出打开一行内存的命令,需要tRCD个时钟周期。
  3. 内存控制器接着发送列地址,CL个时钟周期后,Chip就把数据聚合好了。

如果两次内存IO,行地址是同一行,那么第二次访问的时候就不需要第一二步了,Chip缓存中的数据可以直接使用。

如果行列地址变了,就是随机IO;如果行地址没变,列地址变了,就是顺序IO。在内存硬件中,随机IO比顺序IO速度慢。

四、存储性能测试

(1)延时测试

  1. 看各缓存大小对性能的影响
  2. 看访问随机性对性能的影响

测试原理就是定义数组,然后对数组进行访问,统计耗时情况。

测试步骤:

  1. 申请64MB内存,并进行初始化

    // 内存测试区域从 2 KB 开始,最大到 64 MB 
    #define MINBYTES (1 << 11)  // 2 KB
    #define MAXBYTES (1 << 26)  // 64 MB
    
    // 循环步长从1 到 64 字节
    #define MAXSTRIDE 64        
    #define MAXELEMS MAXBYTES/sizeof(double) 
    
    double data[MAXELEMS]; 
    
    // init_data 初始化要访问的内存数据
    void init_data(double *data, int n)
    {
    	int i;
    	for (i = 0; i < n; i++)
    	{
    		data[i] = i;
    	}
    }
    
    int main()
    {		
    	init_data(data, MAXELEMS); 	
    }
    
  2. 数组从2KB开始,每次测试都翻倍,知道64MB。刚开始的数组在L1还能装的下,随着数组变大,逐渐到L2、L3。

    for (size = MINBYTES; size <= MAXBYTES; size <<= 1) {	
        printf("%.2f\t", get_seque_access_result(size, stride, 1));
    }
    
  3. 访问顺序上,以步长为1对数组进行循环遍历。

    // 内存按照一定的步长进行顺序访问
    void seque_access(int elems, int stride) /* The test function */
    {
    	int i;
    	double result = 0.0; 
    	volatile double sink; 
    
    	for (i = 0; i < elems; i += stride) {
    		result += data[i];  
    	}
    
    	//这一行是为了避免编译器把循环给优化掉了
    	sink = result;
    }
    
  4. 将遍历步长设置为变量,每次都翻倍,1、2、…、32、64,。为了对数据访问局部性造成一定破坏。

    for (stride = 1; stride <= MAXSTRIDE; stride=stride*2) {
        ...
    }
    
  5. 还定义了一种随机访问的方式,使用提前随机生成的数组下标。这样就彻底的破坏了局部性。

    // 提前把要进行随机访问的数组下标准备好,用于随机访问测试
    void create_rand_array(int max, int count, int* pArr)
    {
    	int i;
    	for (i = 0; i < count; i ++,pArr++) {
    		int rd = rand();		
    		int randRet = (long int)rd * max / RAND_MAX;
    		*pArr = randRet;
    	}
    	return;
    }
    
  6. 测试的主要过程

    // 运行内存访问延时测试
    void run_delay_testing(){	
        ...
    	// 多次实验,进行内存顺序访问延时评估
    	// 外层循环控制步长依次从 1 到 64,目的是不同的顺序步长的访问效果差异
    	// 内存循环控制数据大小依次从 2KB 开始到 64MB,目的是要保证数据大小依次超过 L1、L2、L3
    	for (stride = 1; stride <= MAXSTRIDE; stride=stride*2) {
    		...
    		for (size = MINBYTES; size <= MAXBYTES; size <<= 1) {	
    			...
    		}
    	}
    	
    	// 多次实验,进行内存随机访问延时评估
    	for (size = MINBYTES; size <= MAXBYTES; size <<= 1) {		
    		...
    	}
    }
    

运行结果:
在这里插入图片描述

可以得出以下结论:

  • 在数组比较小的时候,访问延迟都很低。
  • **即使是比较大的数组,如果用局部性非常好的方式来访问,延时也很低。**因为步长为1的情况下遍历数组,大部分的数据会在缓存中命中。
  • **数组比较大,顺序访问步长也比较长,破坏了局部性,延时就会上涨。**步长较长时,提前缓存的64位CacheLine缓存就容易命中失败。之中情况下CPU缓存命中失败,就会到内存中的rowbuffer的数组中可能有数据,这就只需要CL个周期的延时。
  • **内存随机访问性能很差。**这时连内存中的rowbuffer的数组也没有用了,需要开始真正的内存访问。

(2)带宽测试

带宽是单位时间内可以从存储中读取多大的数据。

带宽计算方式为:

width = size(MB)/time(s)

带宽的性能和延时测试一样,受内存数组大小和局部性访问是否好的影响。
在这里插入图片描述

带宽测试的结果和延时测试的结果基本是一样的,数组比较小,局部性访问比较好的时候,带宽就很高,因为这是大部分是在CPU的高速缓存中完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值