相信大家对 “内存对齐” 这个名词肯定不会陌生,作为一名程序员,合理的使用内存可以提高程序的运行效率。同时,内存对齐也是笔试、面试中常常会遇到的一个问题。
本篇文章就从内存对齐方式、内存对齐作用来讲讲内存对齐的那些事。
内存对齐一般会涉及到结构体、联合体等,本篇就以结构体为例展开,结构体示例如下:
struct A{
double a;
char b;
int c;
};
struct B{
char a;
double b;
int c;
};
结构体内存对齐的方式
内存对齐的规则如下:
- 对于结构体中的各个成员:第一个结构体成员位于偏移为 0 的位置,此后的每个结构体成员的偏移量都必须是 MIN(#pragma pack(X) , 结构体成员本身的类型长度) 的倍数(其中X的取值一般为 4 或 8 ,分别对应 32 位和 64 位操作系统)。
- 所有结构体成员各自对齐之后,结构体本身也要进行对齐。结构体整体长度应该是 MIN(#pragma pack(X), 结构体中长度最长的结构体成员数据类型的长度) 的倍数。
为什么需要内存对齐?
内存对齐的作用有三个方面:
- 内存对齐可以提升 CPU 的内存访问效率。因为 CPU 是按块去处理内存的,内存块的大小可以是 2、4、8、16 个字节,块的大小称为内存读取粒度。假设内存读取粒度为 4 ,CPU 从 0 字节开始的位置读取一个 4 字节的数据到寄存器中,那么 CPU 将会把 0 ~ 3 四个字节全部读取到寄存器中进行处理。
- 内存对齐可以提升 CPU 的性能。基于上述,若数据起点位置不在 0 字节呢?假设数据起始位置为 2 字节,那么读取这个数据就需要 CPU 读取两次,首先读取 0 ~ 3,接着再读取 4 ~ 7,然后再把 0、1、6、7 字节的数据去除,最后合并 2、3、4、5 字节的数据放入寄存器中。上述就是内存未对齐的场景,此场景下寄存器做了很多额外的操作,必然会影响到 CPU 的性能。
- 防止某些硬件平台拒绝处理数据。不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据(旧版 x86 架构等),否则就会抛出硬件异常。所以内存对齐时就不会存在平台差异,也就是代码的可移植性会更强。
案例分析
基于对结构体内存对齐方式的学习,我们现在可以试着分析一下文章开头所示的结构体,分别在 #pragma pack(4) 和 #pragma pack(8) 场景下的大小。
// #pragma pack(4)
struct A{
double a;
char b;
int c;
};
struct B{
char a;
double b;
int c;
};
在 32 位和 64 位机器上,以下数据类型的大小是相同的,各个数据类型本身长度如下:
- double 类型 8 字节
- char 类型 1 字节
- int 类型 4 字节
基于第一章节中的对齐方式,在 #pragma pack(4) 场景下,struct A 对应的内存布局为:
- double a 占用 8 字节(0 ~ 7)。
- char b 占用 1 字节(8)。
- 为了保持 int c 的 4 字节对齐,接下来会填充 3 字节的填充字节(9 ~ 11)。
- int c 占用 4 字节(12 ~ 15)
即:| double a (8 bytes) | char b (1 byte) | padding (3 bytes) | int c (4 bytes) |
在 #pragma pack(4) 场景下,struct B 对应的内存布局为:
- char a 占用 1 字节(0)。
- 为了保持 double b 的 4 字节对齐,接下来会填充 3 字节的填充字节(1 ~ 3)
- double a 占用 8 字节(4 ~ 11)
- int c 占用 4 字节 (12 ~ 15)
即:| char b (1 byte) | padding (3 bytes) | double a (8 bytes) | int c (4 bytes) |
所以,在 #pragma pack(4) 场景下,struct A 结构体的总长度为 16 字节,struct B 结构体的总长度为 16 字节。
// #pragma pack(8)
struct A{
double a;
char b;
int c;
};
struct B{
char a;
double b;
int c;
};
基于第一章节中的对齐方式,在 #pragma pack(8) 场景下,struct A 对应的内存布局为:
- double a 占用 8 字节(0 ~ 7)。
- char b 占用 1 字节(8)。
- 为了保持 int c 的 4 字节对齐,接下来会填充 3 字节的填充字节(9 ~ 11)。
- int c 占用 4 字节(12 ~ 15)
即:| double a (8 bytes) | char b (1 byte) | padding (3 bytes) | int c (4 bytes) |
在 #pragma pack(4) 场景下,struct B 对应的内存布局为:
- char a 占用 1 字节(0)。
- 为了保持 double b 的 8 字节对齐,接下来会填充 7 字节的填充字节(1 ~ 7)
- double a 占用 8 字节(8 ~ 15)
- int c 占用 4 字节 (15 ~ 19)
- 为了保持整个结构体总长度 8 字节对齐,接下来会填充 4 个 填充字节 (20 ~ 23)
即:| char b (1 byte) | padding (7 bytes) | double a (8 bytes) | int c (4 bytes) | padding (4 bytes) |
所以,在 #pragma pack(8) 场景下,struct A 结构体的总长度为 16 字节,struct B 结构体的总长度为 24 字节。