跳到主要内容

C++ 内存对齐与结构体布局

sizeof(struct) 常常比成员大小之和更大。理解对齐规则,既能解释"内存为什么超预算",也能写出避免伪共享的热点数据结构。

一、为什么需要对齐​

CPU 访问对齐的地址更快,某些架构甚至要求必须对齐(否则触发硬件异常)。于是编译器会在成员之间插入填充字节,让每个成员的地址是其对齐值的整数倍。

规则(主流平台):

  • 每个成员的偏移量必须是该成员对齐值的整数倍;
  • 结构体总大小必须是最大成员对齐值的整数倍;
  • 结构体的对齐值等于其成员对齐值的最大值。

二、实例分析​

struct A {
char c; // 1 字节,偏移 0
// 填充 3 字节
int i; // 4 字节,偏移 4
char d; // 1 字节,偏移 8
// 填充 3 字节(对齐值 4,总大小须为 4 的倍数)
}; // sizeof(A) == 12

重排序后:

struct B {
int i; // 偏移 0
char c; // 偏移 4
char d; // 偏移 5
// 填充 2 字节
}; // sizeof(B) == 8

同样的成员,只改顺序,从 12 降到 8。

三、查看实际布局​

#include <cstddef>
#include <iostream>

int main() {
std::cout << offsetof(A, c) << "\n"; // 0
std::cout << offsetof(A, i) << "\n"; // 4
std::cout << sizeof(A) << "\n"; // 12
}

offsetof 只对标准布局类型(standard-layout)保证可用,含有虚函数、私有非静态成员、多个基类带成员的类不适用。

四、对齐控制:alignas 与 alignof​

alignas(16) char buf[16]; // 强制按 16 字节对齐,适合 SIMD

struct alignas(64) Counter { // 独占缓存行,避免伪共享
std::atomic<long> value{0};
};

static_assert(alignof(Counter) == 64);

alignof(T) 查询类型的对齐要求,alignas(n) 提高对齐要求。C++11 之前只能用编译器扩展(__attribute__((aligned(16)))、__declspec(align(16)))。

五、伪共享:多核性能的隐形杀手​

两个线程分别写同一缓存行(通常 64 字节)里的不同变量时,缓存行会在核间来回失效:

struct Bad {
std::atomic<long> a{0};
std::atomic<long> b{0}; // 与 a 极可能在同一缓存行
};

修复:

struct alignas(64) Padded {
std::atomic<long> value{0};
};
// 或用 C++17 的 std::hardware_destructive_interference_size

反之,如果数据总是被同一线程访问,填充反而浪费缓存——按访问模式决定,不要无脑填充。

六、序列化与网络协议必须考虑的问题​

struct Header {
uint8_t version;
uint32_t length;
uint8_t type;
}; // sizeof 通常 12 而不是 6

直接把 struct 内存 dump 到网络或文件,会把填充字节(未定义内容)一起发出去,既泄漏信息又不可移植。正确做法:

  • 显式逐字段序列化(htonl 等);
  • 或使用 #pragma pack(1) / __attribute__((packed))(代价是非对齐访问变慢,某些平台直接崩溃);
  • 或使用序列化库(protobuf、FlatBuffers)。

packed 只在确有需要时使用,并且要保证该结构体不被取地址做非对齐访问。

七、内存占用的实战优化​

  1. 按对齐值降序排列成员:double/指针 → int → short → char/bool;
  2. 用位域或位运算紧凑存储小整数:
    struct Flags {
    unsigned a : 1;
    unsigned b : 1;
    unsigned c : 3;
    };
    注意位域布局是实现定义的,不能用于跨平台二进制格式;
  3. 避免为小对象分配堆内存:vector<Foo*> vs vector<Foo> 差别巨大;
  4. 检查大数组的 sizeof:Foo arr[1000000] 每个元素多 4 字节填充,就是 4 MB。

八、sizeof 的常见意外​

类型常见 sizeof备注
空结构体1必须可区分地址
含一个虚函数的类8(64 位)含 vptr
std::string32(libstdc++)含 SSO 缓冲
std::vector<T>24(64 位)三个指针
std::shared_ptr<T>16对象指针 + 控制块指针

九、自查清单​

  1. 热点结构体是否按对齐降序排布?
  2. 是否有跨线程共享的相邻计数器(伪共享风险)?
  3. 是否把结构体 memcpy 到网络/文件(填充与字节序风险)?
  4. 大数组的元素类型是否可以从 int 降到 int16_t/uint8_t?
  5. 是否使用了 static_assert(sizeof(Foo) == 预期) 锁定关键结构的布局?

对齐不是编译器的小把戏,是硬件与你订立的契约。知道契约内容,才能既快又不浪费。