A09 — Memory Barriers and Cache Coherence: Out-of-Order Access on Multi-Core

Key Insight: In a multi-core CPU, the memory order you observe is not necessarily the true order.


Out-of-Order Memory Access on Multi-Core CPUs

Modern CPUs reorder memory access instructions for performance optimization. In multi-core programs, this reordering can cause serious problems.

Bash
内存访问乱序的例子:

线程 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,会重排指令

Bash
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

A memory barrier (Memory Fence) tells the CPU: all memory accesses before this must complete before this.

Bash
内存屏障的作用:

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 一定对其他核可见

Bash
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);  // 读屏障

Cache Coherence (MESI Protocol)

Each core of a multi-core CPU has its own L1/L2 Cache. Main memory is shared. The MESI protocol ensures all cores see consistent memory data.

Bash
MESI 协议的状态:

M(Modified):脏行,本核独占修改,其他核无效
E(Exclusive):干净行,本核独占,无修改
S(Shared):干净行,多核共享,内容相同
I(Invalid):无效行,本核没有这行数据

状态转换:

1. 读 Modified 行(另一个核请求):
   - 本核先写回主存
   - 状态 → Shared

2. 写 Shared 行(另一个核也有):
   - 先发 Invalidate 给其他核
   - 其他核置 Invalid
   - 本核状态 → Modified

3. 写 Exclusive 行:
   - 直接修改,状态 → Modified
   - 不需要通知其他核(只有本核有)

MESI 的开销:
  - Invalidate 消息需要总线带宽
  - 写竞争严重时,性能下降
  - 需要内存屏障来保证顺序

Bash
Store Buffer 和 Invalidate Queue:

Store Buffer(写缓冲):
  - 本核的写先放入 Store Buffer
  - 不立即写 L1/L2 Cache
  - 其他核可能暂时看不到
  - 解决:写屏障刷新 Store Buffer

Invalidate Queue(失效队列):
  - 其他核的 Invalidate 消息先放入队列
  - 不立即处理
  - 导致读到的可能是旧数据
  - 解决:读屏障清空 Invalidate Queue

这就是为什么内存屏障在多核编程中如此重要:
  - 屏障保证 Store Buffer 刷新
  - 屏障保证 Invalidate Queue 处理完毕

The Role and Limitations of volatile

Bash
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);  // 原子写,有屏障语义

Bash
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:读屏障

Summary

  • Memory Reordering: CPU reorders memory accesses for performance; can cause errors in multi-core scenarios
  • Memory Barriers: sfence (write barrier) / lfence (read barrier) / mfence (full barrier); ensure ordering
  • MESI: Modified/Exclusive/Shared/Invalid; four states ensuring multi-core cache coherence
  • Store Buffer: Writes are buffered; requires sfence to flush. Invalidate Queue: requires lfence to drain
  • volatile: Only prevents compiler optimization; does not guarantee CPU reordering or multi-core visibility
  • Correct Approach: Use C11/C++11 atomic operations or Linux memory barrier API

Next (A10): How do user programs analyze and optimize memory usage? malloc_stats, mallinfo, valgrind massif, and pmap.


Last modified: 2024年8月22日

Author

Comments

Write a Reply or Comment

Your email address will not be published.