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 只在确有需要时使用,并且要保证该结构体不被取地址做非对齐访问。
七、内存占用的实战优化
- 按对齐值降序排列成员:
double/指针 →int→short→char/bool; - 用位域或位运算紧凑存储小整数:
注意位域布局是实现定义的,不能用于跨平台二进制格式;struct Flags {unsigned a : 1;unsigned b : 1;unsigned c : 3;};
- 避免为小对象分配堆内存:
vector<Foo*>vsvector<Foo>差别巨大; - 检查大数组的 sizeof:
Foo arr[1000000]每个元素多 4 字节填充,就是 4 MB。
八、sizeof 的常见意外
| 类型 | 常见 sizeof | 备注 |
|---|---|---|
| 空结构体 | 1 | 必须可区分地址 |
| 含一个虚函数的类 | 8(64 位) | 含 vptr |
std::string | 32(libstdc++) | 含 SSO 缓冲 |
std::vector<T> | 24(64 位) | 三个指针 |
std::shared_ptr<T> | 16 | 对象指针 + 控制块指针 |
九、自查清单
- 热点结构体是否按对齐降序排布?
- 是否有跨线程共享的相邻计数器(伪共享风险)?
- 是否把结构体
memcpy到网络/文件(填充与字节序风险)? - 大数组的元素类型是否可以从
int降到int16_t/uint8_t? - 是否使用了
static_assert(sizeof(Foo) == 预期)锁定关键结构的布局?
对齐不是编译器的小把戏,是硬件与你订立的契约。知道契约内容,才能既快又不浪费。