什么是内存对齐?为什么需要它?
内存对齐(Memory Alignment)是计算机系统中数据在内存中存储的一种规则:数据在内存中的起始地址必须是其自身大小的整数倍。例如,一个 4 字节的整型变量(int),其起始地址必须是 4 的倍数(如地址 0x0000、0x0004、0x0008 等)。
而需要内存对齐主要基于以下三个原因:
- 硬件访问效率:
- CPU 通过内存总线从内存读取数据时,通常以固定大小的“块”为单位(例如 4 字节或 8 字节)。如果数据对齐,CPU 一次读取操作即可获取完整数据。
- 非对齐示例:假设一个
int变量(4 字节)存储在地址 0x0001(非 4 的倍数),CPU 需要分两次读取:先读取 0x0000-0x0003(包含前 3 字节),再读取 0x0004-0x0007(包含最后 1 字节),最后拼接数据。这会显著降低性能。
- 硬件兼容性:
- 部分架构(如 ARM、MIPS)的 CPU 无法直接访问非对齐内存。尝试访问时会导致硬件异常(如“总线错误”)。对齐保证了代码的跨平台兼容性。
- 缓存效率优化:
- 现代 CPU 使用缓存行(Cache Line,通常 64 字节)预加载数据。对齐的数据更可能完整地位于单个缓存行中。若数据跨缓存行存储,会引发两次缓存访问,降低效率。
alignas 和 alignof
c++11 以后引入两个关键字 alignas 与 alignof。
alignas 用于显式设置变量、类成员或类型的内存对齐要求;而 alignof 用于获取类型或变量的内存对齐要求。例如:
| |
alignas 支持三种语法形式:
alignas(expression):expression 必须是计算结果为零的整数常量表达式,或者是对齐或扩展对齐的有效值(2 的倍数)。alignas(type-id):等效于alignas(alignof(type-id))alignas(pack...):等效于应用于同一声明的多个 alignas 说明符,参数包的每个成员对应一个说明符,可以是类型参数包或常量参数包
备注注意:若指定的对齐值小于编译器默认对齐要求,部分编译器可能忽略该设置。
结构体字节对齐
结构体对齐的基本原则如下:
- 每个成员在其自身大小的整数倍地址上对齐
- 结构体本身的对齐方式等于其最大成员的对齐要求
- 结构体的大小是其对齐方式的整数倍(尾部可能填充)
举个🌰:
| |
| 成员 | 大小 | 对齐要求(alignment) |
|---|---|---|
char[5] | 5 字节 | 1 字节 |
int | 4 字节 | 4 字节 |
short | 2 字节 | 2 字节 |
结构体整体对齐通常等于 最大对齐成员的对齐要求 → max(1, 4, 2) = 4 字节。
其内存布局如下:
| |
如果采用 __attribute__((__packed__)) (GCC/Clang 编译器提供的一个属性)进行“单字节对齐”,则会去除所有自动添加的 padding,每个成员从它前一个成员紧挨着的下一个字节开始。
| |
| |
这种情况通常用于网络协议头、二进制文件结构、设备寄存器映射等。
位域
“位域“是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数,每个域有一个域名,允许在程序中按位域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
位域的对齐规则为:
- 位域的基础类型决定了分配单位,比如
unsigned int : 3表示分配在int的机器字上(通常是 4 字节) - 多个位域会尽量共享同一个机器字,但:
- 如果一个字段放不下了,会拆到下一个对齐单元
- 不同基础类型之间可能强制对齐
- 结构体大小和对齐也会根据最大基础类型来对齐
对于下面这个🌰:
| |
- 所有
int位域(a1 ~ a4)以int(4 字节 = 32 位)为分配单元。 - 多个
int位域可以共用同一个int单元,如果剩余位数够用。 - 一旦放不下一个字段,就会换下一个 4 字节单元。
- 如果换了基础类型(比如从
int→char),通常会进行类型对齐。 - 非位域成员如
char a6仍需按其类型对齐(通常是 1 字节,但结构体对齐会以最大成员为准)。
其内存布局如下:
| |