为什么需要内存对齐以及对齐规则的简单分析

Ubuntu 16.04.2编译环境  arm-linux3.4.5  linux-2.6.22.6

首先需要知道的是:CPU每次从内存中取出数据或者指令时,并非想象中的一个一个字节取出拼接的,而是根据自己的字长,也就是CPU一次能够处理的数据长度取出内存块,比如32位处理器将取出32位也就是4个字节的内存块进行处理。这里有一个问题:是只需要两个字节怎么办?答案是还是取出4个字节,然后内存处理器会帮忙完成数据挑拣在传送给CPU。

总之,CPU会以它“最舒服的”数据长度来读取内存数据,由此就引发又一个问题:

如果有一个4字节长度的指令准备被读取进CPU处理,就会有两种情况出现:

    14个字节起始地址刚好就在CPU读取的地址处,这种情况下,CPU可以一次就把这个指令读出,并执行,内存情况如下

     2而当4个字节按照如下图所示分布时

   

假设CPU还在同一个地址取数据,则取到第一个4字节单元得到了1、2字节的数据,但是这个数据不符合需要的数啊,所以CPU就要在后续的内存中继续取值,这才取到后面的4字节单元得到3、4字节数据,从而和前面取到的1、2字节拼接成一个完整数据,而本次操作进行了两次内存读取,相较第一种直接取出多了一次操作,咋看一下好像就多一次没什么影响,但是考虑到CPU做大量的数据运算和操作,如果遇到这种情况很多的话,CPU将做出不可忽略的量的“多余操作”,严重影响处理速度。


因此,系统需要进行内存对齐,从而提高CPU处理速率,而这项任务就交给编译器进行相应的地址分配和优化,编译器会根据提供参数或者目标环境进行相应的内存对齐。(当然内存对齐也有硬件方面的原因,有些硬件规定了读取地址,如果指令地址和其规定地址不一致,则该硬件可能会发生崩溃等未知情况)


而内存对齐又分为自然对齐规则对齐

其中自然对齐指的是将对应变量类型存入对应地址值的内存空间,即数据要根据其数据类型存放到以其数据类型为倍数的地址处。例如char类型占1个字节空间,1的倍数是所有数,因此可以放置在任何允许地址处,而int类型占4个字节空间,以4为倍数的地址就有0,4,8等。编译器会优先按照自然对齐进行数据地址分配。


而规则对齐以结构体为例就是在自然对齐后,编译器将对自然对齐产生的空隙内存填充无效数据,且填充后结构体占内存空间为结构体内占内存空间最大的数据类型成员变量的整数倍。下面对以上提到的对齐规则进行解释:

(注:无特殊说明都为32位环境,使用gcc -m32进行指定环境编译)

首先,下面的测试代码都为
#include <stdio.h>
int main(int argc,char** argv)
{
    printf("%d",sizeof(struct name));
    return 0;
}

输出结果即为结构体的所占内存空间。


1.首先看这个结构体

typedef struct test_32
{
	char a;
	short b;
	short c;
	char d;
}test_32;

首先按照自然对齐,得到如下图的内存分布位置(第一个格子地址为0,后面递增,下面测试的同样)


然后按照规则对齐,编译器将对空白处进行无效数据填充,最后将得到此结构体占内存空间为8字节,这个数值也是最大的数据类型short的2个字节的整数倍,将程序编译,得到也是8字节的结果。


2.再看如果稍微调换一下位置的结构体

typedef struct test_32
{
	char a;
	char b;
	short c;
	short d;
}test_32;

同样按照自然对齐如下图分布:



可以看到按照自然对齐,变量之间没有出现间隙,所以规则对齐也不用进行填充,而这里有颜色的方格有6个,也就是6个字节,按照规则对齐,6字节是此结构体中最大数据类型short的整数倍,因此此结构体为6字节,后面的空白不需理会,可以实际编译。一下运行,结果和分析一致为6个字节。


从上面两个例子基本上可以知道内存对齐的具体情况,还有一种情况需要补充,就是double的情况,我们知道32位处理器一次只能处理32位也就是4个字节的数据,而double是8字节数据类型,这要怎么处理呢?如果是64位处理器,8字节数据可以一次处理完毕,而在32位处理器下,为了也能处理double8字节数据,在处理的时候将会把double拆分成两个4字节数进行处理,从这里就会出现一种情况如下:

typedef struct test_32
{
	char a;
	char b;
	double c;
}test_32;  

这个结构体在32位下所占内存空间为12字节,而在64位环境下所占内存空间为16字节,原因就是上述的处理方式不同导致的,32位下只能拆分成两个4字节进行处理,所以这里规则对齐将判定该结构体最大数据类型长度为4字节,因此总长度为4字节的整数倍,也就是12字节。而64位判定最大为8字节,所以结果也是8字节的整数倍:16字节。这里的结构体中的double没有按照自然对齐放置到理论上的8字节倍数地址处,我认为这里编译器也有根据规则对齐做出相应的优化,节省了4个多余字节。这部分各位可以按照上述规则自行分析测试。


(注:下述内容和硬件相关性比较大,与内存对齐内容关系并不大,可以不看)

再拓展下,上述对齐对于C语言是适用的,但是对于汇编,由于汇编对于地址的控制对用户更加透明化,基本上可以称为随心所欲了,所以只能尽可能地对齐,一般情况也会正常对齐,但是笔者曾经遇到过在arm架构下使用汇编编写启动代码,其中在汇编代码中调用printf函数做测试时,使用类似

.ascii  "Hello ARM!\0"

的方式定义了一个字符串,并在其前面放置标号,以便作为参数传入printf函数,而在此命令后是我的bl main的主函数跳转指令,

结果实际测试发现系统会崩溃,且上述输出正常。反汇编后发现由于字符串的地址分配是按需分配,导致后面的指令没能对齐,

记得当时内存不是4的倍数,知道这种情况后在其前面添加.align 4对齐伪指令便解决问题了。

所以内存对齐对于一些硬件还是很重要的。

也提醒了自己编写程序时也要有这些底层思维方面的考虑,写出来的代码才会是最优化,更靠近完美···总之坚持学习~



欢迎交流、讨论或者指正!共同进步!

展开阅读全文

没有更多推荐了,返回首页