Bootstrap

C++字节对齐详细解读

字节对齐

简介

 探究字节对齐之前应当先思考一个问题,为什么需要字节对齐?或者说字节对齐有什么好处?

 主要是为了提高访存的效率,因为对齐后的字节访存效率会更高。计算机底层存储硬件比如说内存、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,那么,这个空间大小是怎么来的?如下所示:

  1. 首先按照声明顺序进行内存分配,先为第一个成员a分配空间,该变量的起始地址与结构的起始地址一致(偏移量为0刚好为sizeof(double)的倍数),该成员变量占用了8个字节;
  2. 随后为第二个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,存放在偏移量为8的地方满足对齐方式。
  3. 最后为第三个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为9,显然并不是sizeof(int)的倍数,所以VC需要自动填充3个字节,使得偏移量为12,刚好是sizeof(int)=4的倍数,所以把c存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节。
  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

在这里插入图片描述

  1. 首先按照声明顺序进行内存分配,先为第一个成员a分配空间,该变量的起始地址与结构的起始地址一致(偏移量为0刚好为sizeof(double)的倍数),该成员变量占用了1个字节;
  2. 随后为第二个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为1,显然并不是sizeof(double)的倍数,所以VC需要自动填充7个字节,使得偏移量为8,刚好是sizeof(double)=8的倍数,所以把b存放在偏移量为8的地方,该成员变量占用sizeof(double)=8个字节。
  3. 最后为第三个成员变量分配内存,此时变量存放的起始地址相对于结构的起始地址的偏移量为9,显然并不是sizeof(int)的倍数,所以VC需要自动填充3个字节,使得偏移量为12,刚好是sizeof(int)=4的倍数,所以把c存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节。
  4. 这时整个结构的成员变量已经分配了8+8+4 = 20个字节,不是结构的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数

拓展

  1. #pragma pack(x)中x和结构体中占用空间最大的成员做比较,取两者的最小值为n
  2. 以n值与结构体每个成员比较,得到的最小值赋值给m[x]。
  3. 据每个成员的大小依次向内存中填充数据,要求填充成员的起始地址减去结构体起始地址的差值(即偏移量)可以整除m[x],如不能整除则向后移动,直到可以整除再填充成员到内存中
  4. 当全部成员填充完毕后所占用的字节数若不能整除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() // 编译器将取消自定义字节对齐方式

;