字节对齐
简介
探究字节对齐之前应当先思考一个问题,为什么需要字节对齐?或者说字节对齐有什么好处?
主要是为了提高访存的效率,因为对齐后的字节访存效率会更高。计算机底层存储硬件比如说内存、CPU cache、寄存器等的访问都不是一次一个字节,而是一次一批或者这一次一组字节的访问。
- 字(word):CPU指令处理的数据单元,分为WORD(16bits)、DWORD(32bits)、QWORD(64bits)
- 寄存器(register):CPU通用寄存器通常是64bits,也允许访问寄存器的前8bits、前16bits、前32bits。
- 页(page):一个页大小通常是4096字节。
假设一个4字节的整除原本只需要一次访问,如果字节分配不当会使得原本只需一次访问的操作,多增加一次访问,访问效率大大降低。
所以字节对齐的本质就是在内存空间占用和访存效率之间做折中。C/C++编译器会自动处理struct的内对齐,同时提供了一些机制让程序员手动控制内存对齐#pragma pack()
。
对齐方式
对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
- char偏移量必须为sizeof(char)即1的倍数
- int 偏移量必须为sizeof(int)即4的倍数
- float 偏移量必须为sizeof(float)即4的倍数
- double 偏移量必须为sizeof(double)即8的倍数
- Short 偏移量必须为sizeof(short)即2的倍数
成员变量根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。
同时,为了确保结构的大小是结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
举例说明
struct myStruct{
double a;
char b;
int c;
}; // 16 bytes
显然,该结构体内存并非是8 + 1 + 4 = 13,那么,这个空间大小是怎么来的?如下所示:
- 首先按照声明顺序进行内存分配,先为第一个成员a分配空间,该变量的起始地址与结构的起始地址一致(偏移量为0刚好为sizeof(double)的倍数),该成员变量占用了8个字节;
- 随后为第二个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,存放在偏移量为8的地方满足对齐方式。
- 最后为第三个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为9,显然并不是sizeof(int)的倍数,所以VC需要自动填充3个字节,使得偏移量为12,刚好是sizeof(int)=4的倍数,所以把c存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节。
- 这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。所以整个结构的大小为:sizeof(MyStruct)=8+1+ 3+4=16,其中有3个字节是VC自动填充的,没有放任何有意义的东西。
struct MyStruct {
char a;
double b;
int c;
}; // 24 bytes
- 首先按照声明顺序进行内存分配,先为第一个成员a分配空间,该变量的起始地址与结构的起始地址一致(偏移量为0刚好为sizeof(double)的倍数),该成员变量占用了1个字节;
- 随后为第二个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为1,显然并不是sizeof(double)的倍数,所以VC需要自动填充7个字节,使得偏移量为8,刚好是sizeof(double)=8的倍数,所以把b存放在偏移量为8的地方,该成员变量占用sizeof(double)=8个字节。
- 最后为第三个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为9,显然并不是sizeof(int)的倍数,所以VC需要自动填充3个字节,使得偏移量为12,刚好是sizeof(int)=4的倍数,所以把c存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节。
- 这时整个结构的成员变量已经分配了8+8+4 = 20个字节,不是结构的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数。
拓展
- 以
#pragma pack(x)
中x和结构体中占用空间最大的成员做比较,取两者的最小值为n - 以n值与结构体每个成员比较,得到的最小值赋值给m[x]。
- 据每个成员的大小依次向内存中填充数据,要求填充成员的起始地址减去结构体起始地址的差值(即偏移量)可以整除m[x],如不能整除则向后移动,直到可以整除再填充成员到内存中
- 当全部成员填充完毕后所占用的字节数若不能整除n,则扩充内存至可以整除n为止
例子一:
#pragma pack(4)// 编译器将按照n个字节对齐
struct myStruct
{
int a; // 4 bytes
char b; // 4 bytes
int c; // 4 bytes
short d; // 4bytes
}; // 16 bytes
#pragma pack() // 编译器将取消自定义字节对齐方式
例子二:
#pragma pack(4)// 编译器将按照n个字节对齐
struct myStruct
{
int a; // 4 bytes
char b; // 2 bytes
short d; // 2bytes
int c; // 4 bytes
}; // 12 bytes
#pragma pack() // 编译器将取消自定义字节对齐方式