ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

内存对齐:让数据“站队”提速关键

内存对齐:让数据“站队”提速关键

先说一句话:

内存对齐,就是让数据“站在整齐的位置上”,方便 CPU 一次就拿到它。


一、先看 CPU 是怎么读内存的

内存可以想象成一排很长的格子,每个格子放 1 个字节,并且有编号(地址):

地址: 0 1 2 3 4 5 6 7 8 9 10 11 …… [ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ][ ]

但 CPU不是一格一格地读,而是一整块一整块地搬(比如一次搬 4 个或 8 个字节),而且通常从“块的边界”开始搬:

┌── 第1块 ──┐┌── 第2块 ──┐┌── 第3块 ──┐ 地址: 0 1 2 3 4 5 6 7 8 9 10 11

用快递打个比方:

仓库里的货都装在固定的箱子里,每箱 4 格 快递员每次只能整箱搬,不能从箱子中间开始拿

二、对齐和不对齐的区别

假设要读一个int(4 个字节)。

✅ 对齐:放在地址 4

┌── 第1块 ──┐┌── 第2块 ──┐ 地址: 0 1 2 3 4 5 6 7 [ int 在这 ]

搬 1 次,正好整块拿到。

❌ 不对齐:放在地址 3

┌── 第1块 ──┐┌── 第2块 ──┐ 地址: 0 1 2 3 4 5 6 7 [ int 在这 ] ↑ 横跨了两块

CPU 要:

1. 搬第 1 块,取出最后 1 个字节 2. 搬第 2 块,取出前 3 个字节 3. 把它们拼起来

搬 2 次,还要拼接,慢了。

对齐的规则

一般规则很简单:

一个数据的地址,应该是它自身大小的整数倍。

类型大小应该放在
char1 字节任意地址
short2 字节0、2、4、6 ……
int / float4 字节0、4、8、12 ……
double / 指针(64位)8 字节0、8、16、24 ……

三、最常见的体现:结构体变大了

看这个结构体:

structA{chara;// 1 字节intb;// 4 字节charc;// 1 字节};

直觉上:1 + 4 + 1 =6 字节。

但实际sizeof(struct A)通常是12 字节。为什么?

地址: 0 1 2 3 4 5 6 7 8 9 10 11 [a] [空][空][空][ b b b b ] [c] [空][空][空] └─ 填充 ─┘ └─ 填充 ─┘
  • a放在 0
  • b是 int,必须放在 4 的倍数 → 跳过 1、2、3,放在 4
  • c放在 8
  • 末尾再补 3 个字节,让整个结构体是 4 的倍数

这些空出来的字节叫填充(padding),是编译器自动加的。

💡 为什么末尾也要补?因为如果是数组struct A arr[2],第二个元素紧挨着第一个,不补的话第二个元素里的b就不对齐了。


四、内存对齐的好处

1. 读写更快

数据不跨块,CPU 一次就能拿到,不用搬两次再拼接。

2. 有些 CPU 必须对齐,否则直接出错

x86(大部分电脑):不对齐也能读,只是可能慢一点 部分 ARM、MIPS 等芯片:不对齐的访问可能直接报错、程序崩溃

所以对齐能让程序在各种平台上都正常运行。

3. 保证读写是“一次完成”的(原子性)

多线程程序里,一个线程在写,另一个线程在读:

对齐:一次写完,别人要么看到旧值,要么看到新值 不对齐:分两次写,别人可能读到“一半旧、一半新”的错误值

4. 配合缓存和 SIMD 指令

  • CPU 缓存按“缓存行”(通常 64 字节)读取,数据跨两个缓存行会更慢
  • 游戏、图像处理常用的 SIMD 加速指令(一次处理多个数),很多要求数据按 16 或 32 字节对齐

五、内存对齐带来的问题

问题 1:浪费内存

上面的例子,6 字节的数据占了 12 字节,一半是空的。
如果有一百万个这样的结构体,就多浪费了好几 MB。

问题 2:结构体大小和你想的不一样

// 以为是 6 字节,按 6 字节去读写文件 → 数据全错位fwrite(&a,6,1,file);// ❌fwrite(&a,sizeof(a),1,file);// ✅ 永远用 sizeof

问题 3:跨平台、网络传输时容易出错

不同编译器、不同 CPU 的对齐规则可能不同:

电脑 A 把结构体直接发出去(含填充,12 字节) 设备 B 的规则不一样,按另一种布局去解析 → 读出来的数据对不上

所以写文件格式、网络协议时,一般一个字段一个字段地读写,而不是直接把整个结构体丢过去。

问题 4:强行取消对齐,可能出事

可以用#pragma pack(1)让编译器不加填充:

#pragmapack(1)structA{chara;intb;charc;};// 正好 6 字节#pragmapack()

省了空间,但b就不对齐了:访问变慢,在某些芯片上甚至会崩溃。
只在确实需要紧凑布局时用(比如解析固定格式的协议数据)。


六、怎么既对齐又不浪费?——调整成员顺序

把大的成员放前面,小的放后面:

structB{intb;// 4 字节chara;// 1 字节charc;// 1 字节};
地址: 0 1 2 3 4 5 6 7 [ b b b b ] [a] [c] [空][空]

同样的三个数据,从 12 字节变成 8 字节,而且依然是对齐的。

如果需要更严格的对齐(比如 SIMD),C/C++ 可以用:

alignas(16)floatdata[4];// 让数组按 16 字节对齐

一句话总结

CPU 是按“整块”读内存的,内存对齐就是让数据不跨块,这样读写更快、更安全,有些芯片还必须这样;代价是编译器会塞进一些空字节,浪费空间、让结构体大小和预想的不同。把成员按从大到小排列,可以减少浪费。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进