3.19 数据对齐
数据对齐是计算机系统为了提高内存访问效率而采用的约束:数据的地址必须是其大小的倍数。违反对齐约束虽然可能仍能工作(在 x86-64 上),但会带来显著的性能损失。
对齐原则
基本类型的对齐要求
| 数据类型 | 大小(字节) | 对齐要求 | 典型起始地址 |
|---|---|---|---|
char | 1 | 1(无要求) | 任意地址 |
short | 2 | 2(偶数地址) | 0, 2, 4, 6, … |
int | 4 | 4(4 的倍数) | 0, 4, 8, 12, … |
float | 4 | 4 | 0, 4, 8, 12, … |
long | 8 | 8(8 的倍数) | 0, 8, 16, 24, … |
double | 8 | 8 | 0, 8, 16, 24, … |
long double | 10/16 | 16 | 0, 16, 32, … |
指针(T*) | 8 | 8 | 0, 8, 16, 24, … |
内存地址对齐图示
地址: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
字符: [c][c][c][c][c][c][c][c][c][c][c][c][c][c][c][c] ← 任意地址
短整: [ s ][ s ][ s ][ s ][ s ][ s ]... ← 偶数地址
整型: [ i ][ i ][ i ][ i ]... ← 4 的倍数
长整: [ l ][ l ]... ← 8 的倍数
为什么需要对齐
性能原理
现代 CPU 通过内存总线以字(word)为单位读取内存。对于 64 位 CPU,一次总线传输读取 8 字节。
# 对齐访问(高效)
读取地址 8 的 8 字节数据:
一次总线传输:字节 8-15
# 一次完成!
# 未对齐访问(低效)
读取地址 5 的 8 字节数据:
第一次总线传输:字节 0-7 → 获取字节 5-7
第二次总线传输:字节 8-15 → 获取字节 8-12
数据拼接:合并两次读取的结果 # 两次传输 + 数据处理
对齐 vs 未对齐的代价
| 访问类型 | x86-64 性能 | 其他架构(如 ARM) |
|---|---|---|
| 对齐访问 | 1 次内存事务 | 1 次内存事务 |
| 未对齐访问 | 2 次内存事务 + 拼接 | 通常导致异常(fault) |
x86-64 硬件处理未对齐访问是透明的(不会崩溃),但性能会下降。某些架构(如早期的 ARM、MIPS)根本不允许未对齐访问。
结构体对齐
对齐规则
- 成员对齐:每个成员的偏移量必须是其对齐要求的倍数
- 结构体对齐:结构体总大小必须是最大成员对齐要求的倍数
- 填充:在成员之间和结构体末尾插入填充字节
示例分析
struct S1 {
char c; // 偏移 0(对齐 1 ✓)
// 填充 3 字节 → 偏移 4(int 要求对齐 4)
int i; // 偏移 4(对齐 4 ✓)
double d; // 偏移 8(对齐 8 ✓)
}; // 当前 16 字节,已是 8 的倍数 ✓
struct S2 {
char c; // 偏移 0(对齐 1 ✓)
double d; // 偏移 8(对齐 8 ✓,填充 7 字节!)
int i; // 偏移 16(对齐 4 ✓)
// 填充 4 字节 → 总大小 24(8 的倍数 ✓)
}; // 总大小 24
struct S3 {
double d; // 偏移 0(对齐 8 ✓)
int i; // 偏移 8(对齐 4 ✓)
char c; // 偏移 12(对齐 1 ✓)
// 填充 3 字节 → 总大小 16(8 的倍数 ✓)
}; // 总大小 16(比 S2 节省 8 字节!)结构体对齐可视化
struct S2 (24 字节):
偏移: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
[c] [pad][pad][pad][pad][pad][pad][pad] [ d ] [ i ] [pad][pad][pad][pad]
struct S3 (16 字节):
偏移: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
[ d ] [ i ] [c] [pad][pad][pad]结构体数组对齐
结构体末尾填充对于数组正确对齐至关重要:
struct S {
int i; // 偏移 0
short s; // 偏移 4
// 填充 2 字节 → 总大小 8
};
struct S arr[2]; // arr[0] 在 0x1000, arr[1] 在 0x1008如果末尾不填充 2 字节,arr[1] 的起始地址将是 0x1006(不是 4 的倍数),arr[1].i 将未对齐。
没有末尾填充(错误):
arr[0] arr[1]
[i][i][i][i][s][s][i][i][i][i][s][s]
0 1 2 3 4 5 6 7 8 9 10 11
↑ arr[1].i 未对齐!
有末尾填充(正确):
arr[0] arr[1]
[i][i][i][i][s][s][p][p][i][i][i][i][s][s][p][p]
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
↑ arr[1].i 对齐!
节省内存的技巧总结
1. 按大小降序排列成员
// 低效:24 字节
struct Bad {
char a;
int b;
short c;
double d;
};
// 高效:16 字节
struct Good {
double d;
int b;
short c;
char a;
};2. 合并小成员
// 低效:两个 short 分别占用
struct Bad {
short a;
int flag; // 实际上只需要 int 来对齐?不需要
short b;
};
// 高效:合并小成员
struct Good {
int flag;
short a;
short b;
};编译器对齐控制
// 取消对齐(packed)— 嵌入式编程常用
#pragma pack(1)
struct Packed {
char c; // 偏移 0
int i; // 偏移 1(无填充!)
short s; // 偏移 5
}; // 大小 7
#pragma pack()
// 指定对齐
struct Aligned {
int x;
} __attribute__((aligned(64))); // 按 cache line (64B) 对齐相关笔记
-
- 结构体中的填充与对齐
-
- 结构体数组的对齐
-
- 数据段的布局
-
- 字长与对齐的关系
链接到
- 上一个知识点:3.18 结构体与联合
- 下一个知识点:3.20 内存布局