68机器人 C++17 alignas 缓存行对齐实战
---
一、基本用法
#include <iostream>
// 对齐到 64 字节(典型的缓存行大小)
struct alignas(64) CacheLineAligned {
int value;
};
int main() {
std::cout << sizeof(CacheLineAligned) << std::endl; // 64
std::cout << alignof(CacheLineAligned) << std::endl; // 64
}
alignas(64) 做两件事:
对象的起始地址是 64 的倍数
对象的 sizeof 会被补齐到 64 的倍数(所以上面输出 64,不是 4)
---
二、消除伪共享:两种写法
写法 A:每个成员单独对齐
struct alignas(64) Counter {
alignas(64) std::atomic<int> counter_a{0}; // 线程 1
alignas(64) std::atomic<int> counter_b{0}; // 线程 2
};
countera 的地址是 64 的倍数,counterb 的地址是 counter_a + 64 的倍数,两者一定不在同一条缓存行里。
写法 B:结构体内嵌 padding
struct Counter {
std::atomic<int> counter_a{0};
char pad_a[60]; // 4 + 60 = 64,凑满一条缓存行
std::atomic<int> counter_b{0};
char pad_b[60];
};
写法 A 更好——少一个 magic number,编译器帮你算 padding。
---
三、std::hardwaredestructiveinterference_size
C++17 标准库提供了一个常量,告诉你当前平台实际的缓存行大小:
#include <new>
struct alignas(std::hardwaredestructiveinterference_size) Counter {
std::atomic<int> counter_a{0};
std::atomic<int> counter_b{0};
};
std::hardwaredestructiveinterferencesize 在大多数 x8664 平台上就是 64,但用这个常量比硬编码 64 更可移植。ARM 上可能是 32 或 64。
---
四、数组场景(最常用)
多线程直方图之类的高争抢场景:
// ❌ 错误:counter[0] 和 counter[1] 在同一条缓存行
std::atomic<int> counters[8];
// ✅ 正确:每个元素独占缓存行
struct alignas(64) PaddedAtomic {
std::atomic<int> value{0};
};
PaddedAtomic counters[8];
// counters[0].value 和 counters[1].value 相隔 64 字节
验证:
static_assert(sizeof(PaddedAtomic) == 64, "Must be exactly one cache line");
auto addrdiff = reinterpretcast<char*>(&counters[1])
reinterpret_cast<char*>(&counters[0]);
std::cout << "gap between elements: " << addr_diff << std::endl; // 64
---
五、放在堆上
alignas 对栈上和全局变量直接生效,但 new 出来的对象需要特殊处理:
// ❌ 可能不对齐
auto* p = new PaddedAtomic();
// ✅ C++17 用 std::aligned_alloc 或 over-aligned new
auto* p = new PaddedAtomic(); // C++17 起,over-aligned 类型自动走 aligned new
// 或显式分配
auto* buf = std::aligned_alloc(64, sizeof(PaddedAtomic));
auto* p = new (buf) PaddedAtomic();
C++17 起 new PaddedAtomic() 会自动处理——前提是结构体用了 alignas。
---
六、完整示例
#include <atomic>
#include <thread>
#include <iostream>
#include <new>
#include <vector>
// 缓存行对齐的计数器
struct alignas(std::hardwaredestructiveinterference_size) PaddedCounter {
std::atomic<long long> value{0};
// 编译器自动补齐到 64 字节
};
void testfalsesharing() {
PaddedCounter counters[4];
std::vector<std::thread> threads;
// 四个线程各写自己的槽位
for (int t = 0; t < 4; ++t) {
threads.emplace_back(&, t {
for (int i = 0; i < 100'000'000; ++i) {
counters[t].value.fetchadd(1, std::memoryorder_relaxed);
}
});
}
for (auto& th : threads) th.join();
long long total = 0;
for (auto& c : counters) total += c.value.load();
std::cout << "total: " << total << std::endl;
}
对比测试:去掉 alignas,同样的代码跑 4 线程,你很可能看到 2~3 倍的速度差。
---
七、避坑清单
| 坑 | 说明 |
|:--|:------|
| alignas 不能加在 using 别名上 | 必须加在 struct/class 定义处 |
| alignas 值必须是 2 的幂 | alignas(48) 编译报错 |
| 不要乱对齐 | 只在 profile 确认伪共享是瓶颈时才加,否则浪费内存 |
| malloc 不保证 64 字节对齐 | 堆上对象用 std::aligned_alloc 或 new |
| alignas 不保证成员本身对齐 | 要在成员上也加 alignas,或者用结构体包裹 |
| ARM 和 x86 的缓存行大小可能不同 | 用 std::hardwaredestructiveinterference_size 而不是硬编码 64 |
---
八、一句话总结
alignas(64) 加在 struct 上让对象对齐到缓存行边界、sizeof 补齐到 64 字节;多个变量各自对齐后就不会落在同一条缓存行里,伪共享自然消失。 数组场景最常用,堆上注意对齐分配器,检查 static_assert(sizeof(T) == 64) 确认 padding 有效。