内存对齐
计算机对基本类型数据在内存中存放的位置有限制,会要求这些数据的首地址的值是某个数(通常它是 4 或 8 或 16)的倍数,这个就是内存对齐
一、必要性
假设一个 int 变量(32 位系统,4字节)存放在从地址 0x1 开始的连续 4 个字节地址中,处理器去读取数据时,得先从 0x0 地址开始读取第一个 4 字节块,剔除不想要的字节(0x0 地址),然后从地址 0x4 开始读取下一个 4 字节块,同样剔除不想要的数据(0x5、6、7 地址),最后留下的两块数据合并放入寄存器。这样的话,访问一个数据需要做很多工作。
在内存对齐的情况下,一个 int 变量(32 位系统,4字节),处理器可以一次性将它读出来,效率大大提高。这是性能原因
为什么需要内存对齐:
- **平台原因(移植原因)**:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
二、内存对齐规则
不同平台、不同编译器有不同的“对齐系数”。32位系统中 gcc 对齐数默认为:#pragma pack(4)
内存对齐需要遵循的规则
- 结构体第一个成员的偏移量(offset)为 0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐数中较小的那个整数倍,如有需要编译器会在成员之间加上填充字节。
- 结构体的总大小为(有效对齐数和结构体中最大数据成员对齐长度的较小值的整数倍),如有需要编译器会在最末一个成员之后加上填充字节
1 | // 32 位系统,对齐数为 4 |
首先使用规则 1,对成员变量进行对齐:
sizeof(c1) = 1 <= 4
,按照 1 字节对齐,占用第 0 单元sizeof(i) = 4 <= 4
,相对于结构体首地址的偏移要为 4 的倍数,占用第 4、5、6、7 单元sizeof(c2) = 1 <= 4
,相对于结构体首地址的偏移要为 1 的倍数,占用第 8 单元
然后使用规则 2,对结构体整体进行对齐:
- 对齐数最大的元素是变量 i,占用 4 字节,有效对齐数也是 4 字节,因此按照对齐数为 4 字节。因此结构体总共占 9 个字节,按 4 字节对齐总共占用 12 字节。
三、技巧
更改编译器的缺省字节对齐方式:
- 使用
#pragma pack(n)
,编译器将按照 n 个字节对齐 - 使用
#pragma pack()
,取消自定义字节对齐方式
四、位域
结构体中某字段的希望占用一位或者几位。因此可以把多个字段用一个字节来表示,如下:
1 | struct { |
有几点需要注意事项:
一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。
1
2
3
4
5
6struct {
unsigned a:6;
unsigned 0; // 空域
unsigned b:4; // 从下一单元开始存放
unsigned c:4;
}由于位域不允许跨 2 个字节,因此位域的长度不能大于一个字节的长度,也就是不能超过 8 位。
位域可以无位域名,只用来填充或调整位置。无名的位域是不能使用的。
1
2
3
4
5
6struct {
int a:1;
int :2;
int b:3;
int c:2;
}