内存对齐
1、为什么要内存对齐
#include<iostream>
using namespace std;
struct A{
char a;
int b;
short c;
};
struct B{
short c;
char a;
int b;
};
int main(){
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
return 0;
}
程序的输出结果为:
sizeof(st1) is 12
sizeof(st2) is 8
问题出来了
1、这两个一样的结构体,为什么sizeof的时候大小不一样呢?
2、 编译器为什么要进行内存对齐呢?程序1中结构体按常理来理解sizeof(st1)和sizeof(st2)结果都应该是7,4(int) + 2(short) + 1(char) = 7 。经过内存对齐后,结构体的空间反而增大了。
第二节再说明。
关于数据的排放
这是普通程序员心目中的内存印象,由一个个的字节组成,而CPU并不是这么看待的。
CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度) 本人把它翻译为“内存读取粒度” 。
为什么要内存对齐?
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,也就是上面所说的内存存取粒度
.
现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。
假如没有内存对齐机制,数据可以任意存放。现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,
- 1、要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),
- 2、然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),
- 3、最后留下的两块数据合并放入寄存器.这需要做很多工作.
现在有了内存对齐的,int类型数据只能存放在按照对齐规则的内存中,比如说0地址开始的内存。那么现在该处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作,提高了效率。
为什么要数据对齐?不对齐的话会产生什么样的结果? - 1、
平台原因(移植原因)
:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,一般可能以双字节、4字节等为单位存取内存,为了保证处理器正确存取数据,否则抛出硬件异常。(平台移植是驱动程序开发者经常需要考虑的问题)。 - 2、
性能原因
:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。一般处理器的内存存取粒度都是N的整数倍,假如访问N大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来,提高效率。
2、什么是内存对齐
1、内存对齐
现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
2、基本概念
1、每个特定平台上的编译器都有自己的默认“对齐系数”
(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。
2、有效对其值:是给定值#pragma pack(n)
和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位。
3、内存对齐的规则:
- 1、 第一个成员位于偏移为0的位置,变量的起始地址能够被其对齐值整除,结构体变量的对齐值为最宽的成员大小。
- 2、以后每个数据成员的偏移量必须是
min(#pragma pack()指定的数,这个数据成员的自身长度)
的倍数。如果不能则在前一个成员后面补充字节。 - 3、 在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,也就是
结构体总体大小
能够被最宽的成员的大小整除,如不能则在后面补充字节。- 也就是,整体对齐,将按照
#pragma pack
指定的数值和结构(或联合)最大数据成员长度
- 也就是,整体对齐,将按照
3、实例说明
linux下默认#pragma pack(4)
//32位系统
#include<stdio.h>
struct
{
int i;
char c1;
char c2;
}x1;
struct{
char c1;
int i;
char c2;
}x2;
struct{
char c1;
char c2;
int i;
}x3;
int main()
{
printf("%d\n",sizeof(x1)); // 输出8
printf("%d\n",sizeof(x2)); // 输出12
printf("%d\n",sizeof(x3)); // 输出8
return 0;
}
结构体中最长的数据类型为4个字节,所以有效对齐单位为4字节,下面根据上面所说的规则以x2结构体
来分析其内存布局:
- Step1 :
使用规则1
,char占一个字节,起始偏移为0 ,规则1的后半段在Step2
说明。 - Step2:
使用规则2
,int 占4个字节,min(#pragma pack()大小,int字节数) = 4
。- 基于规则1,所以
int按4字节对齐
,起始偏移必须为4的倍数,所以起始偏移为4
,在char后编译器会添加3个字节的额外字节,不存放任意数据。
- 第二个char占1个字节,按1字节对齐,起始偏移为8,正好是1的倍数,无须添加额外字节。到此规则1的数据成员对齐结束,此时的内存状态为:
- 基于规则1,所以
oxxx|oooo|o
0123 4567 8 (地址)
其中x为补上的字节
- Step3:
使用规则3
,目前共占9个字节。还要继续进行结构本身的对齐。对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,选择较小的那个进行。x2结构中最大数据成员长度为int,占4字节
,而默认的#pragma pack 指定的值为4
,所以结果本身按照4字节对齐,结构总大小必须为4的倍数,需添加3个额外字节使结构的总大小为12
。此时的内存状态为:
oxxx|oooo|oxxx
0123 4567 89ab (地址)
到此内存对齐结束。x2结构占用了12个字节而非6个字节。
根据上面的分析,不难得出上面例子三个结构体的内存布局如下:
4、#pragma pack(n)
不同平台上编译器的 pragma pack 默认值不同。而我们可以通过预编译命令#pragma pack(n), n= 1,2,4,8,16来改变对齐系数。
例如,对于上个例子的三个结构体,如果前面加上#pragma pack(1),那么此时有效对齐值为1字节,此时根据对齐规则,不难看出成员是连续存放的,三个结构体的大小都是6字节。
如果前面加上#pragma pack(2),有效对齐值为2字节,此时根据对齐规则,三个结构体的大小应为6,8,6。内存分布图如下:
参考
1、https://zhuanlan.zhihu.com/p/30007037