C++内存对齐
0x1 什么是内存对齐,为什么需要它?
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,4字节,8字节,16字节甚至32字节为单位来存取内存,这些存取单位称为内存存取粒度。
现在考虑4字节存取粒度的处理器取int类型变量(32位系统),该处理器只能从地址为4的倍数的内存开始读取数据。
假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的连续四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作。
0x2 内存对齐规则
-
结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
-
结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
C++11后可以使用alignof获取一个结构体类型的有效对齐值
#include <iostream>
struct Info {
uint8_t x1;
uint32_t x2;
uint64_t x3;
};
int main(int argc, char *argv[]) {
std::cout << alignof(Info) << std::endl;// 输出8
std::cout << sizeof(Info) << std::endl;// 输出16
return 0;
}
/*
这里因为Info的有效对齐值为8,那么x2的对齐值是min(sizeof(uint32_t), 8) = 4,即其offset为4的整数倍,
x3的对齐值为8,其offset为8的整数倍。
所有Info的大小为 4 + 4 + 8 = 16
*/
如果调换x2和x3的顺序,看看会发生什么...
#include <iostream>
struct Info {
uint8_t x1;
uint64_t x3;
uint32_t x2;
};
int main(int argc, char *argv[]) {
std::cout << alignof(Info) << std::endl;// 输出8
std::cout << sizeof(Info) << std::endl;// 输出24
return 0;
}
/*
这里因为Info的有效对齐值为8,x3的offset需要为8的整数倍,会导致Info对象的字节数变大
所有Info的大小为 8 + 8 + 8 = 24
*/
0x3 改变对齐规则
-
alignas加在结构体前,如果小于有效对齐值会被忽略(只能加大对齐值)
struct alignas(8) S {}; struct alignas(1) U { S s; }; // 错误:如果没有 alignas(1) 那么 U 的对齐将会是 8
-
使用
#pragma pack
#pragma pack (n) // 作用:C编译器将按照n个字节对齐。
#pragma pack () // 作用:取消自定义字节对齐方式。
#pragma pack (push,n) // 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为n个字节对齐
#pragma pack(pop) // 作用:恢复对齐状态
举个例子
#pragma pack (1) // 设置对齐方式
struct A
{
int a;
char b;
short c;
double d;
};
#pragma pack () // 取消自定义对齐方式
#pragma pack (push, 1) // 设置对齐方式
struct B
{
int a;
char b;
short c;
double d;
};
#pragma pack (pop) // 恢复对齐方式
/*
这里sizeof(A) = 15,sizeof(B) = 15,因为它们的对齐值都为1
*/