字节对齐
字节对齐提高访问效率,按照数据类型对齐规则调整内存布局,避免性能损失。
字节对齐
字节对齐
字节对齐是一种约定和优化策略:要求数据在内存中的起始地址必须是某个特定数字的整数倍(通常与数据类型的大小有关)。
为什么需要字节对齐
1. 硬件访问限制与效率
不同平台对内存访问有不同要求:
平台 | 对未对齐访问的处理方式 |
---|---|
x86(Intel) | 可以访问未对齐地址,但效率低 |
ARM/MIPS/SPARC | 访问未对齐地址可能直接触发异常 |
举个例子(32 位 ARM):
1
2
int *p = (int *)0x1003; // 非对齐访问
int val = *p; // CPU 报错:Alignment fault
为什么会慢?
- 对齐访问:只需一次 memory access(aligned word fetch)
- 非对齐访问:CPU 可能需要两次访问(split fetch),然后通过内部逻辑拼装结果
🧬 举例:访问
0x1003
开头的 4 字节int
,CPU 要读0x1000~1003
和0x1004~1007
两个字。
2. 总线/缓存对齐
CPU 通常按「字」单位(4 或 8 字节)从内存中取数据;
如果数据没对齐,就可能跨越两个 cache line(典型为 64 字节),导致:
- 2 次 cache 访问(Cache Miss 增加)
- 内存带宽浪费
编译器对齐策略(C/C++ 为例)
对于结构体 struct S
,编译器会遵循:
- 每个成员地址必须是其类型对齐数的整数倍
- 整个结构体大小是最大对齐数的整数倍
- 必要时插入 padding 字节
对齐数通常为类型大小(也可用
__alignof__
获得)
示例:
1
2
3
4
5
struct S {
char a; // 1字节
int b; // 4字节
short c; // 2字节
};
在 32 位平台上(假设默认对齐):
成员 | 偏移 | 占用 | 说明 |
---|---|---|---|
a | 0 | 1 | char 占1字节 |
pad1 | 1~3 | 3 | 填充,使 b 从地址 4 开始 |
b | 4~7 | 4 | int 对齐到 4 |
c | 8~9 | 2 | short 对齐到 2 |
pad2 | 10~11 | 2 | 结构体对齐到最大成员 4 字节的倍数 |
总大小:12 字节
强制修改对齐方式:
1
2
3
#pragma pack(1)
struct S { char a; int b; short c; };
#pragma pack()
- 强制结构体以 1 字节对齐,压缩空间,但性能可能下降(特别在非 x86 平台上)
对齐对性能的真实影响(汇编视角)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdint.h>
int main() {
// 定义一个长度为 8 的字节数组,内容从 1 到 8
// arr 的内存布局如下(每个数字代表一个字节):
// 地址: 0 1 2 3 4 5 6 7
// 内容: 1 2 3 4 5 6 7 8
uint8_t arr[8] = {1, 2, 3, 4, 5, 6, 7, 8};
// 对齐访问:从 arr[4] 开始,按 uint32_t 读取 4 字节(即读取 5,6,7,8)
// &arr[4] 是按 4 字节对齐的地址(假设 arr 本身是对齐的),所以是“对齐访问”
// val1 读取的是:
// 在小端机器上(x86/x64):val1 = 8 << 24 | 7 << 16 | 6 << 8 | 5
// => val1 = 0x08070605 = 134678021
// 在大端机器上: => val1 = 0x05060708 = 84281096
uint32_t *p_aligned = (uint32_t *)&arr[4];
uint32_t val1 = *p_aligned;
// 非对齐访问:从 arr[3] 开始,按 uint32_t 读取 4 字节(即读取 4,5,6,7)
// &arr[3] 是不是 4 的倍数(是 3),所以是“非对齐访问”
// 一些平台可能支持,但效率低;某些平台(如 ARM)甚至可能直接崩溃(SIGBUS)
uint32_t *p_unaligned = (uint32_t *)&arr[3];
uint32_t val2 = *p_unaligned;
// 打印两个值(按实际平台字节序决定结果)
// 在小端平台(如 x86),输出:
// val1 = 0x08070605 = 134678021
// val2 = 0x07060504 = 117835012
printf("%u %u\n", val1, val2);
return 0;
}
uint8_t
是 1 字节类型,相当于unsigned char
。uint32_t
是 4 字节类型(32 位无符号整数)。小端序:低位字节在低地址 → 读取顺序为:最低字节在前。
对齐访问更快;非对齐访问在某些平台上性能差甚至出错。
uint32_t *
强转后从非对齐地址读取是未定义行为(但 x86 通常会允许这么做)。
在 x86 平台上的行为(使用 GCC 编译 -O0 -m32
)
编译器生成的汇编:
1. 对齐访问(aligned):
mov eax, DWORD PTR [ebp-4]
DWORD PTR
表示一次性取 4 字节(32 位)数据- 地址是 4 的倍数,CPU 直接使用 一个指令 完成加载
2. 非对齐访问(unaligned):
movzx eax, BYTE PTR [ebp-5] ; 读第1个字节
movzx ecx, BYTE PTR [ebp-4] ; 读第2个字节
shl ecx, 8
or eax, ecx
movzx ecx, BYTE PTR [ebp-3] ; 读第3个字节
shl ecx, 16
or eax, ecx
movzx ecx, BYTE PTR [ebp-2] ; 读第4个字节
shl ecx, 24
or eax, ecx
movzx
把一个字节扩展成 32 位整数shl
是左移操作(拼接 4 个字节组成 1 个整数)or
是拼装的方式
总结:
- 对齐访问:1 条指令
mov
完成 - 非对齐访问:至少 4 条加载 + 3 条移位 + 3 条或运算
在 ARM、MIPS 平台(非 x86):
非对齐访问 会直接触发 SIGBUS 错误(对齐错误):
1
Bus error (core dumped)
除非编译时启用 -mno-unaligned-access
(ARMv7 及更高)或者使用 memcpy
规避。
性能影响测试(以 GCC 为例)
对 aligned 与 unaligned 循环访问 1 亿次:
1
2
3
4
5
6
7
for (int i = 0; i < 100000000; i++) {
sum += *(uint32_t *)(arr + 4); // aligned
}
for (int i = 0; i < 100000000; i++) {
sum += *(uint32_t *)(arr + 3); // unaligned
}
实测:
- 对齐访问:约 50ms
- 非对齐访问:约 200ms+(慢了 3~5 倍)
如何规避非对齐访问?
- 避免强制类型转换指针造成非对齐访问
- 使用
memcpy()
安全加载不对齐数据 - 结构体字段对齐或
#pragma pack
后用memcpy
装载大数据
对齐与数据结构设计的深远影响
1. 未优化结构体:浪费空间
1
2
3
4
5
struct Bad {
char c1; // 1 byte
int i; // 4 bytes
char c2; // 1 byte
};
分析布局(在 32 位/64 位平台上)
成员 | 偏移 | 大小 | 说明 |
---|---|---|---|
char c1 | 0 | 1 | |
pad1 | 1~3 | 3 | 填充到 4 字节对齐 |
int i | 4~7 | 4 | 已对齐 |
char c2 | 8 | 1 | |
pad2 | 9~11 | 3 | 填充到结构体整体对齐(4字节对齐) |
总大小:12 字节
2. 优化结构体:减少 padding
1
2
3
4
5
struct Good {
int i; // 4 bytes
char c1; // 1 byte
char c2; // 1 byte
};
优化后布局
成员 | 偏移 | 大小 | 说明 |
---|---|---|---|
int i | 0~3 | 4 | 已对齐 |
char c1 | 4 | 1 | |
char c2 | 5 | 1 | |
pad | 6~7 | 2 | 补到 4 字节对齐倍数 |
总大小:8 字节
3. 对齐优化技巧:
技巧 | 原理 |
---|---|
成员按从大到小排序 | 减少中间 padding 空隙 |
使用 #pragma pack | 强制压缩结构体,节省空间 |
拆成多个结构 | 热数据放一起,冷数据放另一结构(cache 优化) |
本文由作者按照 CC BY 4.0 进行授权