引言
内存对齐有利于从硬件层面提高cpu对内存的访问效率。通常情况下,cpu取数据是按照内存单元去取的。这就意味着,内存对齐的情况下,cpu取某个地址上的数据(它存储在某个内存单元),假设cpu一次只取一个内存单元的数据,那么此时cpu只需要访问一次内存即可。但是,如果内存非对齐,这个数据就有可能落在两个内存单元,那cpu就需要至少访问两次内存。由此可见,内存不对齐的情况下,访存效率降低。
基本数据类型的对齐
以下结果是在64位系统中测试的:
数据类型 | 大小(字节) |
---|---|
short | 2 |
char | 1 |
int | 4 |
float/double | 8 |
int* | 8 |
long | 8 |
long long | 8 |
对于32位系统,int*和long类型占用4字节。另外,数组是按照数组中元素的类型去对齐的,比如int[]就按照4字节对齐。
类或者结构体的对齐
首先看下面一段代码:
class A{
public:
int* a;
char b;
short c;
A() { a = new int(3); }
~A() { delete a; }
};
对于类或者结构体的对齐,首先考虑内部各变量自身的对齐,其次再考虑类作为一个整体,也需要按照最大的数据成员去对齐。所以综合这两个对齐要求,类A对齐之后的大小为16字节。那么A中成员变量在内存中具体是如何对齐的呢,可以参考下图:
成员a是一个指针,按照8字节对齐,其首地址为0,可以对齐。此时,b的偏移地址为8,b也可以对齐。b占用一个字节,那么c的偏移地址就是9,但c要按照2字节对齐,所以要填充一个字节,也就是x所占的位置,因此,c的偏移地址变成了10,c占用2字节,那么A的大小是12字节。但是,A需要按照8字节对齐,所以再填充4字节,总共占用16字节。
再来看看如下代码:
class A{
public:
char b;
int* a;
short c;
A() { a = new int(3); }
~A() { delete a; }
};
可以看到,调换了下成员变量的位置,直觉告诉我们类A的大小应该不变,但是实际上其大小变为了24字节。可以参考上述过程计算一遍。由此可见,数据成员的排列也会影响到内存对齐之后的结果。这里有一个原则,就是尽量将对占用空间大的成员声明在前面,使得内存更加紧凑。
如何控制内存对齐
控制内存对齐的方式有两种:(1)#pragma pack(size);(2)alignas(size)。size值必须是2、4、8、16…这种,两者的区别在于,pack可以设置不进行内存对齐,也即1字节对齐。并且其设置的对齐大小不受类或结构体成员的限制。而alignas设置的对齐大小不一定生效,对于上述类A,即使设置了alignas(4),而A.a的大小为8字节,所以还是按照8字节对齐,也即按照alignas和数据成员中最大的size进行对齐。另外,与alignas对应的,alignof(T),可以获取类型T的大小,如alignof(int)的值为4。
// 按照16字节对齐,对齐之后类B的大小为48字节
class alignas(16) B{
public:
int a;
int* b;
char c[20];
char d;
};
// 设置不进行内存对齐
#pragma pack(1)
// C的大小为33字节
struct C{
int a;
int* b;
char c[20];
char d;
};
#pragma pack() // 恢复内存对齐
通常情况下,我们不用设置内存对齐,只有在一些特殊场景下。比如网络通信,因为通信双方可能在不同的平台上运行程序,内存对齐的方式也可能不同,如果不去控制内存对齐的话,那么双方在解析收到的数据时,就有可能得不到正确的结果。因此双方会协定好对齐方式,比如取消内存对齐,也即按照1字节对齐。