1、内存对齐基础
各位同学,今天咱们来聊聊内存对齐。说实话,我刚入行那会儿,觉得这东西就是个玄学——结构体里字段换个顺序,大小就变了?这不是编译器在搞鬼吗?后来踩了几次坑,才明白这背后是有道理的。
什么是内存对齐
内存对齐,说白了就是数据在内存里摆放时,要遵循某种"对齐规则"。比如一个4字节的int,它的地址必须是4的倍数;一个8字节的double,地址必须是8的倍数。这不是编译器闲得慌,而是CPU硬件的要求。
我打个比方你就懂了。你去图书馆借书,管理员告诉你:"每本书必须放在书架的起始位置,不能横跨两个格子。"这就是对齐。如果你非要把一本书斜着放,横跨两个格子,管理员取书时就得折腾两下——CPU也是这个道理。
核心定义:内存对齐是指数据在内存中的起始地址必须是其自身大小的整数倍(或者某个对齐系数的整数倍)。
为什么需要内存对齐
你可能会问:"不对齐会怎样?" 嗯,我直接告诉你答案:性能下降,甚至崩溃。
原因其实很简单。现代CPU读取内存,不是一次读1个字节,而是按"字"来读的。32位CPU一次读4字节,64位CPU一次读8字节。如果数据没对齐,CPU就得读两次,再把数据拼起来——这多累啊。
我记得有一次做嵌入式开发,一个结构体没注意对齐,结果在ARM芯片上直接报总线错误。查了半天,发现是某个short变量落在了奇数地址上。从那以后,我对齐这件事再也不敢马虎了。
- 性能原因:对齐的数据,CPU一次读取搞定;不对齐的数据,CPU可能要读2次甚至更多
- 硬件原因:有些CPU(比如ARM、RISC-V)根本不允许非对齐访问,一碰就崩
- 原子性原因:对齐的数据可以保证原子操作,非对齐的数据在多线程环境下容易出问题
注意:x86架构对非对齐访问比较宽容,只是慢一点。但ARM、MIPS等嵌入式芯片,非对齐访问直接触发异常。做嵌入式开发的同学,这点一定要牢记。
对齐边界与对齐系数
这两个概念容易搞混,我帮你理一理。
对齐边界,指的是某个数据类型自身要求的对齐值。比如:
| 数据类型 | 大小(字节) | 对齐边界 |
|---|---|---|
| char | 1 | 1(任意地址) |
| short | 2 | 2(偶数地址) |
| int | 4 | 4(4的倍数地址) |
| float | 4 | 4 |
| double | 8 | 8 |
| 指针(32位) | 4 | 4 |
| 指针(64位) | 8 | 8 |
对齐系数,则是编译器或平台设定的"最大对齐单位"。在大多数平台上,对齐系数就是8(64位系统)或4(32位系统)。结构体的整体对齐,取的是成员中最大对齐边界和对齐系数的较小值。
举个例子你就明白了:
struct Example {
char a; // 1字节,对齐边界1
int b; // 4字节,对齐边界4
short c; // 2字节,对齐边界2
};
这个结构体的大小是多少?直觉告诉你:1 + 4 + 2 = 7字节。但实际是12字节。为什么?
因为int b要对齐到4的倍数,所以char a后面要填充3个字节的"空洞"。short c后面还要填充2个字节,让整个结构体的大小是4的倍数(最大成员的对齐边界)。
我的经验:写结构体时,把大的成员放在前面,小的放后面,能减少填充。比如上面那个例子,改成 int b; short c; char a; 大小就变成8字节了。省了4个字节,这在嵌入式场景下很实用。
知识体系总览
下面这张图,帮你把内存对齐的核心逻辑串起来:
实际代码验证
光说不练假把式。咱们写段代码验证一下:
#include <stdio.h>
#include <stddef.h>
// 不优化顺序的结构体
struct BadOrder {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
// 优化顺序的结构体
struct GoodOrder {
int b; // 4字节
short c; // 2字节
char a; // 1字节
};
int main() {
printf("BadOrder size = %zu\n", sizeof(struct BadOrder));
printf("GoodOrder size = %zu\n", sizeof(struct GoodOrder));
printf("BadOrder 偏移: a=%zu, b=%zu, c=%zu\n",
offsetof(struct BadOrder, a),
offsetof(struct BadOrder, b),
offsetof(struct BadOrder, c));
printf("GoodOrder偏移: b=%zu, c=%zu, a=%zu\n",
offsetof(struct GoodOrder, b),
offsetof(struct GoodOrder, c),
offsetof(struct GoodOrder, a));
return 0;
}
运行结果:
BadOrder size = 12
GoodOrder size = 8
BadOrder 偏移: a=0, b=4, c=8
GoodOrder偏移: b=0, c=4, a=6
看到了吧?同样的三个成员,换个顺序,大小从12字节变成了8字节。这就是对齐的威力。
关键点:结构体的大小必须是最大成员对齐边界的整数倍。BadOrder中最大成员是int(4字节),所以最终大小是12(4的倍数)。GoodOrder同理,8也是4的倍数。
避坑指南
我曾经在做一个网络协议解析的项目时,直接用一个结构体去映射网络数据包。结果因为本地编译器的对齐规则和网络协议的对齐规则不一致,解析出来的数据全是乱的。后来加了 #pragma pack(1) 才搞定。
所以给你几个建议:
- 跨平台传输数据时,一定要用
#pragma pack(1)取消对齐,或者手动序列化 - 嵌入式开发中,注意芯片是否支持非对齐访问,不支持的话要小心处理
- 性能敏感场景,尽量让结构体成员按大小降序排列,减少填充
- 调试时,用
offsetof宏查看成员偏移,能帮你快速定位对齐问题
我个人习惯在结构体定义后面加个静态断言,确保大小符合预期:_Static_assert(sizeof(struct Foo) == 16, "unexpected size"); 这样编译时就能发现问题。
好了,内存对齐的基础就讲到这里。说白了就是一句话:数据放对位置,CPU才跑得快。下一节咱们聊聊位域,那个东西更绕,但用好了能省不少内存。
公众号:蓝海资料掘金营,微信deep3321