前置知识: C

对齐与内存布局

00:00
3 min Intermediate 2026/6/14

内存对齐与结构体布局

概述

内存对齐是编译器在安排数据在内存中的位置时遵循的规则。处理器访问对齐的内存地址比访问未对齐的地址更高效,某些架构甚至不允许未对齐访问。理解对齐规则对于编写高性能代码、处理二进制协议和与硬件交互至关重要。

基础概念

什么是对齐

每个数据类型都有一个对齐要求(alignment),即该类型变量的地址必须是其对齐值的整数倍。例如,int 类型对齐值通常为4,意味着 int 变量的地址必须是4的倍数。

为什么需要对齐

  1. 硬件处理器一次读取对齐的内存,未对齐的访问可能需要读取
  2. 硬件要求:某些架构(如ARM、MIPS)不支持对齐访问,会触发异常
  3. 性:某些原子操作要求变量必须对齐

对齐值

类型典型对齐(32位)典型对齐(64位)
char11
short22
int44
long48
float44
double4或88
指针48

快速上手

结构体对齐与填充

#include <stdio.h>
#include <stddef.h>

// 未优化的结构体布局
struct Example {
    char a;    // 1字节 + 3字节填充
    int b;     // 4字节
    short c;   // 2字节 + 2字节填充
};
// 总大小: 12字节

// 优化后的结构体布局
struct Optimized {
    int b;     // 4字节
    short c;   // 2字节
    char a;    // 1字节 + 1字节填充
};
// 总大小: 8字节

int main(void) {
    printf("Example 大小: %zu\n", sizeof(struct Example));   // 12
    printf("Optimized 大小: %zu\n", sizeof(struct Optimized)); // 8

    // 查看字段偏移量
    printf("Example.a 偏移: %zu\n", offsetof(struct Example, a));   // 0
    printf("Example.b 偏移: %zu\n", offsetof(struct Example, b));   // 4
    printf("Example.c 偏移: %zu\n", offsetof(struct Example, c));   // 8

    return 0;
}

使用 alignas 和 alignof

#include <stdio.h>
#include <stdalign.h>
#include <stddef.h>

// 强制变量按16字节对齐
alignas(16) int aligned_var;

// 查看类型的对齐要求
int main(void) {
    printf("int 的对齐: %zu\n", alignof(int));       // 通常为4
    printf("double 的对齐: %zu\n", alignof(double));  // 通常为8
    printf("aligned_var 的地址: %p\n", (void *)&aligned_var);

    // 检查是否16字节对齐
    if ((uintptr_t)&aligned_var % 16 == 0) {
        printf("aligned_var 是16字节对齐的\n");
    }

    return 0;
}

详细用法

结构体对齐规则

  1. 结构体第一个成员的偏移量为0
  2. 后续成员的偏移量必须是其对齐值的整数
  3. 结构体的总大小必须是其最大对齐值的整数
  4. 嵌套结构体的对齐值为其最大成员对齐
#include <stdio.h>
#include <stddef.h>

struct Complex {
    char a;       // 偏移0, 大小1, 对齐1
    // 填充3字节
    int b;        // 偏移4, 大小4, 对齐4
    short c;      // 偏移8, 大小2, 对齐2
    double d;     // 偏移16(需要8对齐), 大小8, 对齐8
    char e;       // 偏移24, 大小1, 对齐1
    // 填充7字节(总大小必须是8的倍数)
};
// 总大小: 32字节

int main(void) {
    printf("Complex 大小: %zu\n", sizeof(struct Complex));
    printf("a 偏移: %zu\n", offsetof(struct Complex, a)); // 0
    printf("b 偏移: %zu\n", offsetof(struct Complex, b)); // 4
    printf("c 偏移: %zu\n", offsetof(struct Complex, c)); // 8
    printf("d 偏移: %zu\n", offsetof(struct Complex, d)); // 16
    printf("e 偏移: %zu\n", offsetof(struct Complex, e)); // 24
    return 0;
}

使用 alignas 控制对齐

#include <stdio.h>
#include <stdalign.h>

// 强制结构体按缓存行大小对齐(通常64字节)
typedef struct {
    alignas(64) char data[64]; // 缓存行对齐
} CacheLine;

// 防止伪共享:每个变量独占缓存行
typedef struct {
    alignas(64) int counter_a; // 线程A专用
    alignas(64) int counter_b; // 线程B专用
} SharedCounters;

int main(void) {
    printf("CacheLine 大小: %zu\n", sizeof(CacheLine));         // 64
    printf("SharedCounters 大小: %zu\n", sizeof(SharedCounters)); // 128

    SharedCounters sc;
    printf("counter_a 地址: %p\n", (void *)&sc.counter_a);
    printf("counter_b 地址: %p\n", (void *)&sc.counter_b);
    printf("地址差: %ld 字节\n", (long)((char *)&sc.counter_b - (char *)&sc.counter_a));

    return 0;
}

使用 packed 取消填充

#include <stdio.h>

// 正常结构体
struct Normal {
    char a;
    int b;
    short c;
};

// 打包结构体(无填充)
#pragma pack(push, 1)
struct Packed {
    char a;    // 偏移0
    int b;     // 偏移1(未对齐!)
    short c;   // 偏移5
};
#pragma pack(pop)

// GCC/Clang 方式
struct __attribute__((packed)) PackedAttr {
    char a;
    int b;
    short c;
};

int main(void) {
    printf("Normal 大小: %zu\n", sizeof(struct Normal));   // 通常12
    printf("Packed 大小: %zu\n", sizeof(struct Packed));   // 7
    printf("PackedAttr 大小: %zu\n", sizeof(struct PackedAttr)); // 7
    return 0;
}

常见场景

场景一:网络协议结构体

#include <stdio.h>
#include <stdint.h>
#include <string.h>

// TCP头部结构体(必须精确匹配网络格式)
#pragma pack(push, 1)
typedef struct {
    uint16_t src_port;    // 源端口
    uint16_t dst_port;    // 目的端口
    uint32_t seq_num;     // 序列号
    uint32_t ack_num;     // 确认号
    uint16_t data_offset : 4; // 数据偏移
    uint16_t reserved : 6;    // 保留
    uint16_t flags : 6;       // 标志
    uint16_t window;      // 窗口大小
    uint16_t checksum;    // 校验和
    uint16_t urgent_ptr;  // 紧急指针
} TCPHeader;
#pragma pack(pop)

int main(void) {
    printf("TCPHeader 大小: %zu 字节\n", sizeof(TCPHeader)); // 20
    return 0;
}

场景二:缓存友好的数据布局

#include <stdio.h>
#include <stdalign.h>
#include <time.h>

#define N 1000000

// 缓存不友好:AoS(Array of Structures)
typedef struct {
    double x, y, z;     // 位置
    double r, g, b;     // 颜色
    double mass;         // 质量
} ParticleAoS;

// 缓存友好:SoA(Structure of Arrays)
typedef struct {
    double *x, *y, *z;
    double *r, *g, *b;
    double *mass;
} ParticlesSoA;

// 只更新位置时,SoA只需加载3个数组,AoS需要加载7个字段
double sum_positions_aos(ParticleAoS *particles, int n) {
    double sum = 0;
    for (int i = 0; i < n; i++) {
        sum += particles[i].x + particles[i].y + particles[i].z;
    }
    return sum;
}

int main(void) {
    ParticleAoS *aos = malloc(N * sizeof(ParticleAoS));
    for (int i = 0; i < N; i++) {
        aos[i].x = aos[i].y = aos[i].z = i * 0.001;
    }

    clock_t start = clock();
    double result = sum_positions_aos(aos, N);
    clock_t end = clock();

    printf("结果: %f, 耗时: %.3f ms\n", result,
           (double)(end - start) * 1000 / CLOCKS_PER_SEC);

    free(aos);
    return 0;
}

注意事项

对齐与可移植性

不同平台对齐要求可能不同,不要假设特定的对齐值:

// 错误:假设int总是4字节对齐
// int *p = (int *)0x1001; // 可能未对齐

// 正确:使用alignof查询
#include <stdalign.h>
printf("int 对齐: %zu\n", alignof(int));

packed 的性能影响

packed 结构体访问未对齐成员时,某些架构会产生性能惩罚或异常

struct __attribute__((packed)) Packed {
    char a;
    int b; // 可能未对齐
};

struct Packed p;
// 在x86上可以工作但较慢
// 在ARM上可能触发异常
int val = p.b; // 潜在问题

// 安全方式:使用memcpy
int val;
memcpy(&val, &p.b, sizeof(int));

malloc 返回的内存总是最大对齐

malloc 返回的内存保证对齐到任何标准类型,无需额外处理

void *ptr = malloc(sizeof(double));
double *d = (double *)ptr; // 保证对齐

进阶用法

自定义内存分配器(对齐分配)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

// 分配指定对齐的内存
void *aligned_alloc_custom(size_t alignment, size_t size) {
    // 分配额外空间用于对齐调整和存储原始指针
    void *original = malloc(size + alignment + sizeof(void *));
    if (!original) return NULL;

    // 计算对齐后的地址
    uintptr_t addr = (uintptr_t)original + sizeof(void *);
    addr = (addr + alignment - 1) & ~(alignment - 1);

    // 在对齐地址前存储原始指针
    ((void **)addr)[-1] = original;

    return (void *)addr;
}

// 释放对齐分配的内存
void aligned_free(void *ptr) {
    if (ptr) {
        void *original = ((void **)ptr)[-1];
        free(original);
    }
}

int main(void) {
    // 分配64字节对齐的内存
    double *data = aligned_alloc_custom(64, 100 * sizeof(double));
    if (data) {
        printf("分配地址: %p\n", (void *)data);
        printf("64字节对齐: %s\n",
               (uintptr_t)data % 64 == 0 ? "是" : "否");

        data[0] = 3.14;
        printf("data[0] = %f\n", data[0]);

        aligned_free(data);
    }

    return 0;
}

使用 _Alignof 和 _Alignas(C11 标准写法)

#include <stdio.h>
#include <stdalign.h>

// C11 标准关键字(与 alignas/alignof 等价)
_Alignas(32) int cache_aligned_var;

int main(void) {
    printf("int 对齐要求: %zu\n", _Alignof(int));
    printf("double 对齐要求: %zu\n", _Alignof(double));
    printf("long long 对齐要求: %zu\n", _Alignof(long long));

    printf("cache_aligned_var 地址: %p\n", (void *)&cache_aligned_var);
    printf("32字节对齐: %s\n",
           (uintptr_t)&cache_aligned_var % 32 == 0 ? "是" : "否");

    return 0;
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式