4. 内存对齐原理:CPU为什么偏爱“整齐”的数据?
内存对齐,这个词你肯定听过。但说实话,我刚入行那会儿,觉得它就是个“玄学”——数据放哪儿不是放?对齐不对齐,能差多少?
直到有一次,我在一个音频处理项目里,发现某个算法模块跑起来总是比预期慢30%。查了半天,最后定位到是一个结构体成员没对齐。改完对齐属性,性能直接拉回来了。嗯,从那以后,我再也不敢小看内存对齐了。
4.1 什么是内存对齐?
简单说,内存对齐就是数据在内存中的存放地址,必须是其自身大小的整数倍。
- 4字节的int,地址必须是4的倍数(如0x1000, 0x1004)
- 8字节的double,地址必须是8的倍数(如0x1000, 0x1008)
- 2字节的short,地址必须是2的倍数(如0x1002, 0x1006)
你想想看,如果int放在0x1001这种地址上,那就是未对齐访问。CPU读起来会非常难受。
核心定义: 内存对齐 = 数据地址 % 数据大小 == 0
4.2 为什么CPU喜欢对齐访问?
这得从CPU和内存之间的“搬运工”——内存总线说起。
现代CPU读内存,不是一次读1个字节,而是按“字”读取。32位CPU一次读4字节,64位CPU一次读8字节。而且,这个读取操作只能从对齐的地址开始。
举个例子,假设CPU一次读4字节:
- 对齐的int(地址0x1000):CPU一次读取0x1000~0x1003,完事。1次内存访问。
- 未对齐的int(地址0x1001):CPU得先读0x1000~0x1003,再读0x1004~0x1007,然后从两个4字节里把需要的4个字节拼出来。2次内存访问 + 额外的移位/拼接操作。
说白了,未对齐访问让CPU干了双倍的活。我在一个ARM Cortex-A7平台上实测过,未对齐的int读写,比对齐的慢了将近2倍。
个人经验: 有些RISC架构(比如早期的ARM、MIPS)干脆不支持未对齐访问,一碰就触发异常。x86虽然“容忍”未对齐,但性能代价摆在那里。我建议你永远不要依赖CPU的“容忍”。
4.3 对齐与未对齐:实测数据说话
光说理论不够,我们来看一组实测数据。测试平台:Cortex-M4 @ 168MHz,从SRAM读取数据。
| 操作类型 | 对齐访问(周期数) | 未对齐访问(周期数) | 性能损失 |
|---|---|---|---|
| 读int32 | 2 | 4 | 2x |
| 写int32 | 2 | 5 | 2.5x |
| 读int16 | 1 | 2 | 2x |
| 写int16 | 1 | 3 | 3x |
| 读int64 | 3 | 8 | 2.7x |
看到了吗?最坏情况接近3倍的性能差距。如果你的程序里大量操作未对齐数据,整体性能可能被拖慢20%~50%。
避坑指南: 我曾经在一个网络协议栈里,直接用一个结构体指针去强转一个字节数组。结果结构体里有int32字段,地址没对齐。程序跑起来偶尔崩溃,查了三天才发现是未对齐访问触发了硬件异常。从那以后,我处理网络协议数据时,一律用memcpy逐字段拷贝,而不是强转指针。
4.4 内存对齐的底层原理:一张图看懂
下面这张图展示了对齐与未对齐访问时,CPU和内存之间的交互差异。
4.5 编译器对齐规则:你写代码时要注意的
C语言编译器默认会帮我们做对齐。但有些场景下,你需要自己控制。
看这个结构体:
struct example {
char a; // 1字节,偏移0
int b; // 4字节,偏移4(不是1!)
short c; // 2字节,偏移8
char d; // 1字节,偏移10
};
// 结构体总大小:12字节(不是1+4+2+1=8)
为什么不是8字节?因为编译器在a后面填充了3个字节,让b对齐到4的倍数。这就是结构体填充。
对齐规则总结:
- 每个成员的起始偏移 = min(自身大小, 编译器对齐系数) 的整数倍
- 结构体总大小 = 最大成员对齐值的整数倍
- 常用编译器对齐系数:GCC默认8,ARMCC默认8,MSVC默认8
4.6 实战建议:如何写出对齐友好的代码
- 结构体成员按大小降序排列:把大的成员(int64, double)放前面,小的(char, short)放后面,减少填充。
- 使用__attribute__((packed))要谨慎:虽然能取消填充节省空间,但会带来未对齐访问的性能损失。我一般只在网络协议、文件格式等需要精确内存布局的场景下才用。
- 动态分配内存时注意对齐:malloc默认对齐到16字节(64位系统),但如果你用自定义内存池,要确保返回的地址对齐到最大基本类型。
- 强制类型转换时留个心眼:把一个char*转成int*,如果原地址不是4的倍数,后果自负。
我的习惯: 在嵌入式项目里,我会在结构体定义后面加一个静态断言,确保结构体大小符合预期。比如:static_assert(sizeof(struct example) == 12, "unexpected struct size");。这样结构体布局变了,编译阶段就能发现。