A09 —— 内存屏障和缓存一致性:多核访问的乱序问题
金句:在多核 CPU 里,你看到的内存顺序,不一定是真正的顺序。
多核 CPU 的内存访问乱序
现代 CPU 会为了性能优化而重新排序内存访问指令。但在多核程序中,这种乱序可能导致严重的问题。
内存访问乱序的例子:
线程 A:
data = 42; // Store A
flag = true; // Store B
线程 B:
if (flag) { // Load B
print(data); // Load A
}
期望:flag=true 时,data 应该是 42
但 CPU 乱序后可能:flag=true 时,data 还是旧值!
原因:CPU 为了减少 stall,会重排指令CPU 的内存重排类型(x86/x64):
1. 编译器重排:
- 编译器优化代码顺序
- 取决于编译器和优化级别
2. CPU 运行时重排:
- Store Buffer(写缓冲)
- Invalidate Queue(失效队列)
- 流水线并行
x86 的内存模型(Total Store Order):
- 所有核看到 Store 的顺序相同(TSO)
- 但 Store 可能被缓冲,导致其他核看到"先读后写"
- Store Buffer 的存在导致弱保证
ARM/RISC-V 的内存模型(Relaxed):
- 更多的重排可能
- 更容易出现内存顺序问题内存屏障(Memory Barrier)
内存屏障(Memory Fence/Memory Barrier)是告诉 CPU “在此之前的所有内存访问必须在此之前完成”的指令。
内存屏障的作用:
1. 写屏障(Store Barrier / SFence):
- 确保所有之前的写操作对其他核可见
- 刷新 Store Buffer 到 L1 Cache
2. 读屏障(Load Barrier / LFence):
- 确保所有之前的读操作从全局内存读取
- 清空 Invalidate Queue
3. 全屏障(Full Barrier):
- SFence + LFence
- 所有内存操作必须完成
代码示例(无屏障):
flag = true; // Store
data = 42; // Store
// 可能:data=42 在 flag=true 之前对其他核可见
代码示例(有屏障):
data = 42; // Store
asm volatile ("sfence" ::: "memory"); // 写屏障
flag = true; // Store
// flag=true 时,data=42 一定对其他核可见Linux 内核的内存屏障接口:
#include <linux/barrier.h>
mb(); // 全屏障(读写)
smp_mb(); // 多核全屏障(SMP)
smp_wmb(); // 多核写屏障
smp_rmb(); // 多核读屏障
dma_rmb(); // DMA 读屏障
dma_wmb(); // DMA 写屏障
用户态 C11/C++11:
#include <stdatomic.h>
atomic_thread_fence(memory_order_seq_cst); // 全屏障
atomic_thread_fence(memory_order_release); // 写屏障
atomic_thread_fence(memory_order_acquire); // 读屏障缓存一致性(MESI 协议)
多核 CPU 的每个核有自己的 L1/L2 Cache,而主存是共享的。MESI 协议确保每个核看到一致的内存数据。
MESI 协议的状态:
M(Modified):脏行,本核独占修改,其他核无效
E(Exclusive):干净行,本核独占,无修改
S(Shared):干净行,多核共享,内容相同
I(Invalid):无效行,本核没有这行数据
状态转换:
1. 读 Modified 行(另一个核请求):
- 本核先写回主存
- 状态 → Shared
2. 写 Shared 行(另一个核也有):
- 先发 Invalidate 给其他核
- 其他核置 Invalid
- 本核状态 → Modified
3. 写 Exclusive 行:
- 直接修改,状态 → Modified
- 不需要通知其他核(只有本核有)
MESI 的开销:
- Invalidate 消息需要总线带宽
- 写竞争严重时,性能下降
- 需要内存屏障来保证顺序Store Buffer 和 Invalidate Queue:
Store Buffer(写缓冲):
- 本核的写先放入 Store Buffer
- 不立即写 L1/L2 Cache
- 其他核可能暂时看不到
- 解决:写屏障刷新 Store Buffer
Invalidate Queue(失效队列):
- 其他核的 Invalidate 消息先放入队列
- 不立即处理
- 导致读到的可能是旧数据
- 解决:读屏障清空 Invalidate Queue
这就是为什么内存屏障在多核编程中如此重要:
- 屏障保证 Store Buffer 刷新
- 屏障保证 Invalidate Queue 处理完毕volatile 的作用和局限
volatile 的作用:
volatile int flag;
flag = 1; // 不会被编译器优化掉
volatile 的局限:
- 防止编译器优化(读到缓存而不是内存)
- 不保证 CPU 的运行时重排
- 不保证多核之间的可见性
- 不保证原子性
Linux 内核的做法:
volatile int flag; // 单核有用,多核不够
// 多核需要:
{ smp_mb(); } // 内存屏障
或者用原子操作:
#include <linux/atomic.h>
atomic_t flag;
atomic_set(&flag, 1); // 原子写,有屏障语义C11/C++11 原子操作替代 volatile:
#include <stdatomic.h>
atomic_int flag = ATOMIC_INIT(0);
// 写(Release):
atomic_store(&flag, 1); // 有 release 语义
// 或
flag.store(1, std::memory_order_release);
// 读(Acquire):
int x = atomic_load(&flag); // 有 acquire 语义
// 或
int x = flag.load(std::memory_order_acquire);
memory_order_seq_cst:全屏障(最严格)
memory_order_release:写屏障
memory_order_acquire:读屏障总结
- 内存乱序:CPU 为了性能重排内存访问,多核场景下可能导致错误
- 内存屏障:sfence(写屏障)/ lfence(读屏障)/ mfence(全屏障),确保顺序
- MESI:Modified/Exclusive/Shared/Invalid 四种状态,保证多核缓存一致性
- Store Buffer:写先缓冲,需要 sfence 刷新;Invalidate Queue:需要 lfence 清空
- volatile:只防止编译器优化,不保证 CPU 重排和多核可见性
- 正确做法:用 C11/C++11 原子操作或 Linux 内存屏障 API
下篇预告(A10):用户程序怎么分析和优化内存使用?malloc_stats、mallinfo、valgrind 的 massif、以及 pmap 的使用。
关注公众号「AI不着急」,回复”资料”获取内存学习路线图。
评论