文章

字节对齐

字节对齐提高访问效率,按照数据类型对齐规则调整内存布局,避免性能损失。

字节对齐

字节对齐

字节对齐是一种约定和优化策略:要求数据在内存中的起始地址必须是某个特定数字的整数倍(通常与数据类型的大小有关)。

为什么需要字节对齐

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~10030x1004~1007 两个字。

2. 总线/缓存对齐

CPU 通常按「字」单位(4 或 8 字节)从内存中取数据;

如果数据没对齐,就可能跨越两个 cache line(典型为 64 字节),导致:

  • 2 次 cache 访问(Cache Miss 增加)
  • 内存带宽浪费

编译器对齐策略(C/C++ 为例)

对于结构体 struct S,编译器会遵循:

  1. 每个成员地址必须是其类型对齐数的整数倍
  2. 整个结构体大小是最大对齐数的整数倍
  3. 必要时插入 padding 字节

对齐数通常为类型大小(也可用 __alignof__ 获得)

示例:

1
2
3
4
5
struct S {
    char a;   // 1字节
    int  b;   // 4字节
    short c;  // 2字节
};

在 32 位平台上(假设默认对齐):

成员偏移占用说明
a01char 占1字节
pad11~33填充,使 b 从地址 4 开始
b4~74int 对齐到 4
c8~92short 对齐到 2
pad210~112结构体对齐到最大成员 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 倍)

如何规避非对齐访问?

  1. 避免强制类型转换指针造成非对齐访问
  2. 使用 memcpy() 安全加载不对齐数据
  3. 结构体字段对齐或 #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 c101 
pad11~33填充到 4 字节对齐
int i4~74已对齐
char c281 
pad29~113填充到结构体整体对齐(4字节对齐)

总大小:12 字节

2. 优化结构体:减少 padding

1
2
3
4
5
struct Good {
    int  i;    // 4 bytes
    char c1;   // 1 byte
    char c2;   // 1 byte
};

优化后布局

成员偏移大小说明
int i0~34已对齐
char c141 
char c251 
pad6~72补到 4 字节对齐倍数

总大小:8 字节

3. 对齐优化技巧:

技巧原理
成员按从大到小排序减少中间 padding 空隙
使用 #pragma pack强制压缩结构体,节省空间
拆成多个结构热数据放一起,冷数据放另一结构(cache 优化)
本文由作者按照 CC BY 4.0 进行授权