内存对齐不是什么花里胡哨的理论玩意儿,它是硬件决定的铁律。忽略它,你的代码要么崩溃,要么性能烂到家。现代C++给了你工具,让你精确控制对齐,而不是祈祷编译器聪明。

1. 为什么对齐重要?

  1. 硬件要求:CPU从内存读取数据必须按特定边界(如4字节、8字节、16字节)。不对齐,访问非法,段错误伺候。
  2. 性能杀手:不对齐导致多次内存访问、缓存失效。SIMD指令(SSE/AVX)强制16/32字节对齐,否则零矢量惩罚。
  3. 缓存线:现代CPU缓存64字节。不对齐跨缓存线,延迟翻倍。

记住:对齐是优化,不是可选。过度对齐浪费空间,但不足齐更糟。

2. alignof:查询对齐需求

alignof(T) 返回类型T的最小对齐字节数。简单、直观。

#include <print>

int main() {
    std::print("alignof(char): {}\n", alignof(char));
    std::print("alignof(int): {}\n", alignof(int));
    std::print("alignof(double): {}\n", alignof(double));
    std::print("alignof(long long): {}\n", alignof(long long));
    return 0;
}

平台相关,但x86_64通常这样。结构体对齐取最大成员对齐,并padding填充。

3. alignas:强制指定对齐(C++11)

alignas(N)alignas(T) 指定对齐。N必须是2的幂。

alignas(16) char buffer[64];  // 16字节对齐,SIMD友好

struct alignas(32) SseVector {  // AVX对齐
    float data[8];
};

4. struct/class 对齐规则

这是实战中最常踩坑的地方。规则简单,但细节致命。

基本规则

  1. 成员对齐:每个成员按其类型的对齐要求放置,地址必须是alignof(类型)的整数倍。
  2. 整体对齐:struct/class的整体大小必须是max(所有成员alignof)的整数倍,不足则尾部padding。
  3. 整体对齐值:struct/class的alignof等于max(所有成员alignof),除非显式alignas指定更大值。
struct Example {
    char a;       // 偏移0,1字节
    // 3字节padding(因为int需要4字节对齐)
    int b;        // 偏移4,4字节
    char c;       // 偏移8,1字节
    // 3字节padding(整体大小必须是max(1,4,1)=4的倍数)
};
// sizeof(Example) == 12
// alignof(Example) == 4

成员顺序决定空间

声明顺序直接影响内存布局。把大的放前面,小的放后面,减少padding浪费。

struct Bad {
    char a;       // 1字节 + 3字节padding
    int b;        // 4字节
    char c;       // 1字节 + 3字节padding
    // sizeof(Bad) == 12
};

struct Good {
    int b;        // 4字节(偏移0)
    char a;       // 1字节(偏移4)
    char c;       // 1字节(偏移5)
    // 2字节padding
    // sizeof(Good) == 8
};

同样的成员,不同的顺序,节省33%空间。这就是"好品味"。

继承与对齐

单继承:派生类在基类之后布局,整体对齐取所有成员(含基类)的最大对齐值。

struct Base {
    char a;       // 1字节 + 3字节padding
    int b;        // 4字节
};  // sizeof == 8

struct Derived : Base {
    char c;       // 1字节 + 3字节padding(整体对齐到4)
};  // sizeof == 12

多继承:每个基类按声明顺序依次布局,各自满足对齐要求。

struct A { char a; };           // sizeof == 1
struct B { int b; };            // sizeof == 4
struct C : A, B {               // A先布局(1字节+3字节padding),然后B
    char c;                     // 1字节 + 3字节padding
};  // sizeof == 12

虚继承:虚基类指针(vptr)通常8字节(64位系统),影响布局。编译器实现相关,但通常vptr放在对象开头。

struct VirtualBase { int a; };
struct Derived : virtual VirtualBase {
    // vptr (8字节) + padding + VirtualBase::a (4字节)
    // 具体布局依赖编译器
};

虚函数与vptr

有虚函数的类,编译器插入vptr(指向虚函数表)。64位系统上vptr是8字节,8字节对齐。

struct NoVirtual {
    char a;       // 1字节 + 3字节padding
    int b;        // 4字节
};  // sizeof == 8

struct WithVirtual {
    char a;       // 1字节 + 7字节padding(vptr需要8字节对齐)
    // vptr: 8字节(通常放在开头或末尾,编译器决定)
    int b;        // 4字节 + 4字节padding
    virtual void foo();
};  // sizeof == 24(常见布局:vptr + a + padding + b + padding)

不同编译器vptr位置不同:GCC/Clang通常放开头,MSVC可能放末尾。别猜,用offsetof验证。

空类与EBO

空类sizeof至少1字节(C++标准要求,确保不同对象地址不同)。

struct Empty {};
// sizeof(Empty) == 1

struct HoldsEmpty {
    Empty e;      // 1字节
    int x;        // 4字节 + 3字节padding(因为e在偏移0)
};
// sizeof(HoldsEmpty) == 8

空基类优化(EBO):空类作为基类时,编译器可优化为0字节。

struct Empty {};
struct UseEBO : Empty {
    int x;
};
// sizeof(UseEBO) == 4(Empty被优化掉)

标准库容器大量使用EBO(如std::vector的allocator)。写模板时记住这点。

alignas对struct的影响

alignas可以指定struct整体对齐,但不能小于自然对齐值。

struct alignas(16) Aligned {
    int x;        // 4字节
    // 12字节padding(整体16字节对齐)
};
// sizeof(Aligned) == 16
// alignof(Aligned) == 16

struct alignas(2) TooSmall {  // 错误!不能小于自然对齐
    int x;  // alignof(int) == 4
};

实战建议

  1. 重排成员:大对齐类型在前,小对齐类型在后。
  2. alignof验证:别猜,打印出来看。
  3. 小心继承:多继承和虚继承的布局复杂,避免过度依赖内存布局。
  4. EBO友好:空类作基类,不是成员。
struct Bad {
    char a;       // 1字节 + 3字节padding
    int b;        // 4字节
    // sizeof(Bad) == 8
};

struct Good {
    alignas(int) char a;  // 强制int对齐
    int b;
    // sizeof(Good) == 8,无浪费
};

5. std::align:手动内存对齐(C++11)

动态分配不对齐内存时,用std::align调整指针。

#include <memory>
#include <cstddef>

void* raw = operator new(1024);  // 原始内存
size_t space = 1024;
void* aligned = nullptr;

aligned = std::align(32, 1, raw, space);  // 32字节对齐,分配1字节
if (aligned) {
    // 用aligned...
}
operator delete(raw);  // 释放原始

完美用于自定义allocator或对象池。别手动算偏移,那是脑残行为。

6. 对齐的new/delete(C++11+)

C++11引入over-aligned new:

struct alignas(64) CacheLine {  // 缓存线对齐,避免false sharing
    int data;
};

CacheLine* p = new CacheLine;  // 自动64字节对齐
delete p;

编译器生成特殊new/delete处理over-alignment。C++17引入std::align_val_t标签,支持显式over-aligned分配:

#include <new>  // std::align_val_t

struct alignas(64) CacheAligned {
    int data[16];
};

auto p = new(std::align_val_t(64)) CacheAligned();
delete p;

此语法允许自定义allocator精确控制对齐。注意:delete无需标签,编译器自动匹配。

7. 现代特性与最佳实践(C++17/20/23)

  • std::hardware_destructive_interference_size:硬件破坏性干扰大小(C++17,<new>),通常64字节(缓存线大小)。多线程神器:用alignas(此值)对齐独立变量,避免false sharing——同一缓存线修改一个,全线失效,性能暴跌。
  • std::hardware_constructive_interference_size:硬件构造性干扰大小,通常同上。同一缓存线内相关访问可共享预取,优化性能。
alignas(std::hardware_destructive_interference_size) struct Padding {};  // 防false sharing
  • SIMDalignas(64)数组用于AVX512。
  • Allocator:自定义std::allocator支持align_val_t
  • constexpr alignof/alignas:C++17起constexpr友好。

陷阱

  1. alignas(3)非法,必须2幂。
  2. 全局/静态变量对齐影响整个段。
  3. 数组第一个元素决定对齐。
  4. 过度对齐:alignas(4096)浪费页。

忠告

  • 先用alignof,别猜。
  • 小函数测试对齐,别写100行调试。
  • 性能敏感?基准测试,别幻想。
  • 兼容性:MSVC/GCC/Clang行为一致,但ARM不同。

对齐简单,做对就行。别让padding毁了你的数据布局。