“内存对齐“和“cache line“伪共享的个人理解

先上一张图,如有侵权麻烦告知;这张图描述了不同存储介质的存取速度:

寄存器>>cache(l1/l2/l3)>>ram>>flash>>硬盘>>网络存储.

由于cpu的速度要远快于存储介质的读写速度,且存在不同类型的存储介质,受他们的体积,成本,效率等因素的影响,产生了我们今天用到的计算机的存储结构.

内存对齐

前言

我们一直会听到"内存对齐"这一名词,但是不清楚为什么内存需要对齐.好像就是一个默认的定理,只管拿来用,确不懂该如何证明.最好的办法,就是从芯片最底层的工作原理去搞明白为什么要"内存对齐".

接着会讲述结构体"struct"的内存对齐.当然这只是我的个人理解,如有错误还请帮忙指正,以免误导他人.

以32位系统为例,它的"地址总线"和"数据总线"都是32位.

一般的我们程序运行时的变量都是存储在ram中的,我们今天就从ram的角度去看为什么要"内存对齐".

正文

我们知道cpu的速度是很快的,ram的读写速度较慢,这样就会有点拉跨.那么32bits的数据总线在从ram中读数据的时候我们当然是希望尽量一次多读一点了,毕竟我们有32bit的宽度.假如每次只读一个存储单位也就是8bits,这样也未免浪费"数据总线"的宽度,而且也会使cpu不高兴.这样的话ram的4个存储单元就需要并行工作,也就是不管每次你问我要多少数据我每次都给你32bits数据(实际上ram的工作原理比这复杂的多,详情还需要自行研究).那么ram每次都传输32bits数据,也就是说它传输的基本单位为4bytes.

接着想除了内存一般还需要访问寄存器(cache line和flash,硬盘不在我们的讨论之列,cache line一般都是32bytes或者64bytes之多,flash,硬盘之类的访问方式差异更大,譬如按页读,按块擦除,内存卷等概念),那32位机器上我们的寄存器都是32bit位宽的,这样操作效率最高.

字节对齐的使用,这里举一个例子:

我们在学C语言的时候有讲到过结构体的内存对齐,譬如对于下面一个结构:

struct test {
    char a;
    int b;
};

如果没有采用内存对齐:

a占一个字节, b占4个字节.假如没有内存对齐这一说.当程序访问这一结构体变量时,变量位于物理内存中.那么假设a占据内存地址0,b则占据内存地址1~4.由于ram访问是每次读4bytes,那么我们想要访问b的时候,需要先读地址0~3,再读地址4~7,然后把地址1~3和地址4处的值拼出变量b的值放到寄存器里.这样在没有内存对齐的情况下,我们需要读两次内存,并且还需要进行相应的转换.

采用内存对齐:

那么如果我们采用内存对齐呢,编译的时候编译器给a分配0~3的地址,b分配4~7的地址,这样我们想要读b的值时,只需要读一次地址4~7就可以了.是不是节省了很多的cpu指令周期呀.

注意一点:

这里结构体内存对齐是在编译阶段确定的,编译器在分配内存地址的时候做了对齐.那么我们知道编译器影响到的是虚拟地址,而我们读内存读的是物理地址啊.了解过linux页表机制的同学都知道,内存页一般都是4kBytes大小,虚拟内存的低12位(4kBytes)是页内的偏移地址,剩余的高几位才是用来映射物理页帧(page frame,得到该地址在物理内存的哪一页).也就是说虚拟内存的低12位和物理内存的低12位是对应的.所以这里我们讲内存对齐时用到的地址概念,无所谓是虚拟地址还是物理地址.

 

Cahche Line伪共享

我们知道为了解决ram的速度拉跨cpu的问题,出现了cache这一存储介质,由于它的成本原因以及体积原因,所以它的结构被做的比较巧妙,但是对于我们学习者来说,就显得复杂了.这里默认大家都有cache的相关概念.

单核cpu下的伪共享:

以32bytes大小的"Cache Line"为例:

struct test1 {
    int a;
    int b;
};

a和b各占4bytes,假设a占据地址0~3,b占据地址4~7.当我们访问a的时候,会把地址0~31的数据加载到一条cache line中.当我们更新b中的数据时,cache line会变为invalid;下一次我们再要访问的时候需要重新加载cache line.

如果我会经常读a中的数据,经常更新b中的数据,那么更新b会导致cache line的效率下降,因为我每次访问a的时候,只要在这之前cache line被b给更新了,那么我就得重新加载cache line.

如果我们在a和b之间插入一些pad,使他们放在两条不同的cache line中,就可以避免了.譬如:

struct test2 {
    int a;
    int c[7]; //padding
    int b;
};

或者你也可以测试一下下面的程序(注意运行时间可能收系统loading波动的影响,存在波动,可能会存在伪共享耗时更少的现象)

#include <stdio.h>
#include <sys/time.h>

struct test1 {
	volatile int a;
	volatile int b;
};

struct test2 {
	volatile int a;
	volatile int c[7];
	volatile int b;
};

int main(int argc, char *argv[])
{
	int epoch_tm = 0;
	struct timeval tm;
	int current_tm = 0;
	int i;
	struct test1 value1;
	struct test2 value2;
	int tmp;

	gettimeofday(&tm, NULL);
	current_tm = tm.tv_sec * 1000000 + tm.tv_usec;

	for (i = 0; i < 1000000000; i++) {
		tmp = value1.a;
		value1.b = i;
	}

	gettimeofday(&tm, NULL);
	epoch_tm = (tm.tv_sec * 1000000 + tm.tv_usec) -
			current_tm;
	printf("cache phony share cost [%d] usec\n", epoch_tm);

	gettimeofday(&tm, NULL);
	current_tm = tm.tv_sec * 1000000 + tm.tv_usec;

	for (i = 0; i < 1000000000; i++) {
		tmp = value2.a;
		value2.b = i;
	}

	gettimeofday(&tm, NULL);
	epoch_tm = (tm.tv_sec * 1000000 + tm.tv_usec) -
			current_tm;
	printf("cache not share cost [%d] usec\n", epoch_tm);
}

多核CPU下的伪共享:

我也是看的其他人的文章,简单做个记录吧.这个是说,多核CPU的每个核都有自己的cache设备,根据cache的一致性协议.多核的cache line是保持一致的,假如A核的某一cache line变成invalid的了,那么其它核的该cache line也是invalid状态.解决的办法应该也是加pad吧,跟前面讲的差不多.不对的还望大佬们教育.谢谢~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值