3.19 数据对齐

数据对齐是计算机系统为了提高内存访问效率而采用的约束:数据的地址必须是其大小的倍数。违反对齐约束虽然可能仍能工作(在 x86-64 上),但会带来显著的性能损失。

对齐原则

基本类型的对齐要求

数据类型大小(字节)对齐要求典型起始地址
char11(无要求)任意地址
short22(偶数地址)0, 2, 4, 6, …
int44(4 的倍数)0, 4, 8, 12, …
float440, 4, 8, 12, …
long88(8 的倍数)0, 8, 16, 24, …
double880, 8, 16, 24, …
long double10/16160, 16, 32, …
指针(T*880, 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)根本不允许未对齐访问。

结构体对齐

对齐规则

  1. 成员对齐:每个成员的偏移量必须是其对齐要求的倍数
  2. 结构体对齐:结构体总大小必须是最大成员对齐要求的倍数
  3. 填充:在成员之间和结构体末尾插入填充字节

示例分析

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) 对齐

相关笔记

    • 结构体中的填充与对齐
    • 结构体数组的对齐
    • 数据段的布局
    • 字长与对齐的关系

链接到