字节对齐与字节填充:从 CPU 到 Go
前言
这篇文章主要讲字节对齐(Alignment)和字节填充(Padding),这是 C/C++、Go、Rust、Java 虚拟机底层实现以及操作系统领域经常出现的知识点。我们从「CPU 为什么需要对齐」开始讲,更容易建立完整认知。
一、什么是字节对齐
假设有一个 int32:
int a = 10;
int32 占 4 个字节。如果把它放到 0x1000 开始的位置,那么它是 4 字节对齐 的,因为 0x1000 % 4 = 0:
0x1000 10
0x1001
0x1002
0x1003
如果放到 0x1001 开始,那么 0x1001 % 4 != 0,这叫未对齐(Unaligned):
0x1000
0x1001 10
0x1002
0x1003
0x1004
二、CPU 为什么喜欢对齐
现代 CPU 读取数据不是一次读 1 字节,而是 4 / 8 / 16 / 32 / 64 字节成块读取。例如 CPU 一次读取 8 字节,内存 0x1000 ~ 0x1007 一个周期即可完成。
对齐情况:int32 放在 0x1000,CPU 读一次即可得到数据:
┌─────────────┐
│0x1000~1007 │
└─────────────┘
↑
int32
未对齐情况:如果放在 0x1007,数据 0x1007 ~ 0x100A 横跨两个缓存块。CPU 需要先读 0x1000~1007,再读 0x1008~100F,然后拼接,变成两次访存,性能下降。
三、什么是字节填充(Padding)
看一个结构体:
struct Test {
char a;
int b;
};
大小是多少?很多人第一反应是 1 + 4 = 5,实际上 sizeof = 8。内存布局:
offset
0 a (1 byte)
1
2
3 <-- padding
4 b (4 byte)
5
6
7
图示:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ a │pad │pad │pad │ b │
└────┴────┴────┴────┴────┴────┴────┴────┘
为什么?因为 int 需要 4 字节对齐,编译器必须让 b 的地址 % 4 == 0,因此补了 3 字节,这就是 Padding。
四、结构体对齐规则
大多数平台遵循两条规则:
规则 1:成员地址必须是 min(成员大小, 对齐系数) 的整数倍。
char 1
short 2
int 4
double 8
规则 2:结构体总大小必须是最大对齐数的整数倍。
例子:
struct S {
char a;
int b;
char c;
};
推算过程:
| 成员 | offset | 说明 |
|---|---|---|
a |
0 | size = 1 |
b |
4 | 需要 4 字节对齐,补 3 字节 |
c |
8 | size = 1,到 9 |
| 末尾 | — | 当前大小 9,最大对齐数 4,补 3 字节到 12 |
最终布局:
0 a
1-3 padding
4-7 b
8 c
9-11 padding
sizeof(S) = 12。
五、为什么结构体末尾也要填充
struct S {
int a;
char b;
};
明明 4 + 1 = 5,为什么 sizeof(S) = 8?原因是数组:
S arr[100];
如果结构体大小是 5,那么 arr[0] 占 0~4,下一项 arr[1] 从地址 5 开始,但 int a 需要 4 字节对齐,地址 5 显然不满足。所以编译器直接让 sizeof(S) = 8,这样每个结构体首地址(0、8、16、24…)都天然对齐。
六、优化结构体大小
经典问题:
struct A {
char a;
int b;
char c;
};
大小为 12。调整顺序:
struct A {
int b;
char a;
char c;
};
布局变成 4 + 1 + 1 + 2(padding),得到 8,从 12 → 8,减少 33% 内存。
大规模对象时收益非常明显,例如 1000 万对象,原来 120MB,优化后 80MB,直接少 40MB。
七、Go 中的字节对齐
type User struct {
Age int64
Flag bool
}
布局:Age 8字节 + Flag 1字节 + Pad 7字节,总大小 16 字节:
fmt.Println(unsafe.Sizeof(User{})) // 16
字段顺序颠倒也一样:
type User struct {
Flag bool
Age int64
}
仍然是 16,因为 bool 后面补 7 字节,让 Age 对齐到 8 字节边界。
再复杂一点:
type User struct {
A bool
B bool
C bool
D int64
}
布局:A 1 + B 1 + C 1 + Pad 5 + D 8,总计 16。
八、为什么 Go 的 Map 和 Slice 也依赖对齐
研究 Go 底层时这个知识非常重要。例如:
type bmap struct {
tophash [8]uint8
keys ...
values ...
}
Go Runtime 在设计 Bucket 时会保证 key 区域对齐、value 区域对齐、overflow 指针对齐,原因:CPU 读取更快,GC 扫描更方便。
Slice 底层:
type slice struct {
array unsafe.Pointer
len int
cap int
}
64 位机器上是 8 + 8 + 8,总共 24 字节,天然按 8 字节对齐。
总结(一句话版)
字节对齐是让数据地址满足 CPU 要求的边界规则,从而提高访存效率;字节填充是编译器为了满足对齐规则自动插入的空白字节。结构体大小不仅取决于成员大小之和,还取决于成员对齐和结构体整体对齐,因此合理调整字段顺序能够显著减少内存占用并提高缓存利用率。
可以记成一条链路:
CPU要对齐
↓
成员要对齐
↓
产生Padding
↓
结构体总大小也要对齐
↓
字段顺序影响内存占用
这条链路基本覆盖了关于「字节对齐」80% 以上的追问。