什么是内存对齐?
为什么结构相同,内存大小却不同?
该博客上撰写的文章是出于教学目的而创建的。 另外,文章中的信息可能不正确,请谨慎。
在编码时,有时结构的大小与我的预期不同。这种现象正式称为内存对齐和填充。
CPU从内存中读取变量时,并不是以1字节为单位读取的。 由于它是按块、逐字(通常是 4 字节或 8 字节)读取的,因此变量在内存中的排列方式对于性能极其重要。
结构的总体大小可能会根据结构的成员在内存中的排列方式而变化。 这是由于填充的概念。填充是编译器自动插入的空白空间,以帮助将结构成员与 CPU 上的字边界对齐。 由于中间有一个空的空间,结构的尺寸自然增大。
我们自己用代码来验证一下。
#include <iostream>
#include <cstddef> //使用offsetof宏
//1.内存布局效率低下(共24字节)
struct BadOrder {
int A; //4 个字节,下一个变量需要 8 个字节对齐,从而产生 4 个字节的填充(总共使用 8 个字节)。
long long B; //8个字节,单独使用8个字节。 (总共使用了 16 个字节)
bool C; //使用 1 字节后会发生 1 字节、7 字节填充(总共使用 24 字节)。
};
//2.考虑编译器优化的高效内存放置(共16字节)
struct GoodOrder {
long long A; //8个字节,单独使用8个字节。 (总共使用了 8 个字节)
int B; //4个字节,剩下的4个字节可以使用后面的变量,所以不会发生填充。 (总共使用了 12 个字节)
bool C; //使用 1 字节后会发生 1 字节、3 字节填充。 (总共使用了 16 个字节)
};
//3. 强制去除填充的结构(共13字节)
#pragma pack(push, 1)
struct PackedStruct {
int A; // 4 Byte
long long B; // 8 Byte
bool C; // 1 Byte
};
#pragma pack(pop)
这里出现一个问题。为什么BadOrder 末尾额外有7 个字节的填充,GoodOrder 末尾有3 个字节的填充? 这是由于结构分组为数组等时的对齐要求。 结构体的总大小必须是内部成员中最大数据类型(上例中的 8 字节 long long)的倍数。 这是因为移动到数组的下一个元素时可以保证相同的 8 字节对齐。
第三步是使用#pragma pack 强制删除填充。 这只能节省 13 个字节的内存,但可能会导致致命的性能下降。 如果CPU逐字读取并且数据的起始点未对齐, CPU必须读取内存两次并执行无用的额外操作才能强制合并。在某些硬件环境下,程序甚至可能崩溃。
此外,缓存行效率也不容忽视。 现代CPU通常以64字节为单位导入整个内存,但如果像数字1那样是24字节,那么64字节只能存储2。 如果将其优化为 16 字节(如数字 2 所示),则所有 4 个字节都将完全适合。循环数组时,处理速度存在巨大差异。
除非有特殊情况要求内存布局必须准确无1字节错误,例如网络通信标准或文件输入/输出, 在一般情况下,允许填充具有更大的性能优势。 因此,最好养成通过从最大尺寸开始(或按相反顺序)对成员进行分组来最小化不必要填充的习惯,如数字 2 所示。