整理:内存一致模型

1. CPU Cache 内部结构

CPU structure

一个core内部结构:

  • cache
  • store buffer
  • invalidate queue

结构如下图所示:

CPU cache structure

1.1. Cahce一致性协议 MESI

MESICPU内部多个core同步通讯协议,保证多个core中的cache的数据一致性。MESI这四个字母分别代表了每一个cache line可能处于的四种状态:ModifiedExclusiveSharedInvalid

通过给cache line设置状态位,以及CPU core(也可能有内存控制器参与)之间的消息同步逻辑,让多个core中的cache数据保持一致性。

在没有store buffer, invalidate queue之前,MESI可以保证不需要memory fence指令也可以保证数据的一致性。

1.2. False sharing

False sharing的原因是两个CPU访问的变量,在内存中的位置,同时落入一个cache line范围内,根据MESI协议,一个CPU写操作,将导致另一个CPU的读写操作之前,需要进行memory及两个CPUcache line同步操作。通常发生在两个线程操作同一个数据结构体的时候。

false sharing

#define CACHE_ALIGN_SIZE 64
#define CACHE_ALIGNED __attribute__((aligned(CACHE_ALIGN_SIZE)))

struct aligned_value {
  int64_t val;
} CACHE_ALIGNED; // Note: aligning the struct to a cache line size
aligned_value aligned_data[2] CACHE_ALIGNED;

// sizeof(aligned_value) == 128

1.3. 现代CPUMESI的局限

由于MESI同步协议导致处理器之间同步的代价很高,现代处理器再每个core里面增加两个异步队列: store bufferinvalidate queue来减少CPU的空闲等待。这两个异步队列,导致MESI协议失效。

  • store buffer: CPUwrite/store操作数据放入store buffercache负责flush操作。
  • invalidate queue: cache收到Invalidate消息,不是马上执行,而是放入invalidate queue,等待CPU空闲时,再执行。

注意: Store Buffer / Load Buffer 在硬件中并不是简单的 FIFO 队列,而是具有调度逻辑的结构(类似 CAM — Content Addressable Memory),支持乱序发射、乱序完成。Intel 官方手册称之为 “Memory Ordering Buffer”,ARM 架构称之为 “Load-Store Queue (LSQ)”。但即使内部结构支持乱序,x86 TSO 模型仍然保证 Store Buffer 按程序顺序提交到 cache(FIFO 语义),这是 x86 强内存模型的关键。

其原因是:针对发起方CPU,其认为自己的store / invalidate操作已经完成,但是由于数据/消息是放在store buffer / invalidate queue中,所以可能还没来得及被其他CPU看到,导致数据不一致。

其中的一个解决办法是:发起方的store buffer被清空,接收方的invalidate queue被处理掉。在此之后,MESI协议可以正常工作。

1.4. 补充:CPU指令流水线

1. Fetch         <-- ✅ 顺序取指,从指令缓存中取出下一条指令
2. Decode        <-- 顺序解码,分析操作数和目的寄存器
3. Rename        <-- 寄存器重命名
4. Dispatch      <-- 投递到调度窗口,等待执行条件满足
5. Execute       <-- ✅ 乱序执行(由调度器决定),实际在执行单元上运行指令
6. Writeback     <-- 写结果到 ROB
7. Commit        <-- ✅ 按程序顺序提交(retire),更新寄存器状态或进行内存写入

2. memory barrier

2.1. 概念及理论

  • 同步点:针对同一个原子变量load操作与store操作,分别构成一个同步点。其概念有三要素:(1):load/store操作,(2):针对同一个原子变量,(3):以及在不同线程中;
  • synchronize-with 关系,该概念包含两个含义:(1):同一个同步点,(2):读取的值是另一个同步点写入的值;
  • happens-before 关系;

memory fence定义的是同步点操作,即分别在store一方插入一个write barrier指令,在load一方插入一个read barrier指令。

因此,memory barrier需要成对出现,否则达不到同步效果。

2.2. 详细解释

由于多核处理器 CPU 之间独立的L1/L2 cache,会出现cache line不一致的问题,为了解决这个问题,有相关协议模型,比如 MESI 协议来保证 cache 数据一致,同时由于 CPUMESI 进行的异步优化,对写和读分别引入了「store buffer」和「invalid queue」,很可能导致后面的指令查不到前面指令的执行结果(各个指令的执行顺序非代码执行顺序),这种现象很多时候被称作「CPU乱序执行」。

为了解决乱序问题(也可以理解为可见性问题,修改完没有及时同步到其他的CPU),又引出了「内存屏障」的概念;内存屏障可以分为三种类型:写屏障读屏障以及全能屏障(包含了读写屏障),屏障可以简单理解为:在操作数据的时候,往数据插入一条特殊的指令。只要遇到这条指令,那前面的操作都得「完成」。

  1. 写屏障指令(write barrier, or sfence),等待之前的写操作完成,并把该指令「之前」存在于「store Buffer」中的所有写指令刷入cache。就可以让CPU修改的数据马上暴露给其他CPU(MESI),达到「写操作」可见性的效果。

  2. 读屏障指令(read barrier, or lfence),会把该指令「之前」存在于「invalid queue」中的所有的指令都处理掉。通过这种方式就可以确保当前CPU的缓存状态是准确的,达到「读操作」一定是读取最新的效果。

由于不同CPU架构的缓存体系不一样、缓存一致性协议不一样、重排序的策略不一样、所提供的内存屏障指令也有差异,所以一些语言c++/java/go/rust 都有实现自己的内存模型, 比如golang大牛Russ Cox写的内存模型系列文章 Memory Models 值得深入了解。

2.3. x86上面的fence实操演示

ARM架构CPU有Store BufferInvalidate Queue,是一个松散内存一致性模型。x86架构只有Store Buffer,是一个强内存一致性模型。

x86架构下,对StoreLoad操作进行重排(乱序)。其余几种保持顺序:StoreStore, LoadLoad, LoadStore,即不需要设置fence指令也可以保持CPU之间的内存一致性。

禁止编译器重排:

X = 1;
asm volatile("" ::: "memory"); // Prevent compiler reordering
r1 = Y;

禁止编译器及CPU重排:

X = 1;
asm volatile("mfence" ::: "memory"); // Prevent compiler and CPU reordering
r1 = Y;

详细知识参考:

3. C++11 内存一致性模型定义

内存序 语义 x86-64 实现 ARM64 实现
relaxed 只保证原子性,不保证顺序 普通 MOV 普通 LDR/STR
acquire (load) 此操作之后的读写不能重排到此操作之前 普通 MOV + 编译器屏障 LDAR
release (store) 此操作之前的读写不能重排到此操作之后 普通 MOV + 编译器屏障 STLR
acq_rel (RMW) 同时具有 acquire 和 release 语义 LOCK 前缀指令(隐含全屏障) LDAXR/STLXR
seq_cst 全局唯一顺序一致性,所有线程看到相同的 seq_cst 操作顺序 store: XCHG 或 MOV+MFENCE; load: MOV LDAR/STLR (+ 额外屏障)

关键认知:C++ 内存序是语言层面的抽象,其硬件实现因架构而异。

  • x86 TSO (Total Store Order)模型下,硬件已经保证了 LoadLoadLoadStoreStoreStore 顺序,只允许 StoreLoad 重排。因此 acquirereleasex86 上不需要任何硬件屏障指令,只需编译器屏障(asm volatile("" ::: "memory"))来阻止编译器重排。
  • ARM 弱内存模型下,所有四种重排都可能发生,因此需要显式的硬件指令(LDAR/STLR/DMB)。
  • seq_cstx86 上需要额外处理 StoreLoad 重排——编译器在 store 端使用 XCHG(隐含 LOCK)或 MOV + MFENCEload 端使用普通 MOV

参见:https://www.felixcloutier.com/x86/sfencehttps://www.felixcloutier.com/x86/lfencehttps://www.felixcloutier.com/x86/mfence

3.1. release/acquire 如何实现局部同步

release/acquire 提供的是成对的、局部的同步关系:当线程 B 的 acquire load 读到线程 A 的 release store 写入的值时,线程 A 在 release store 之前的所有写入,对线程 B 在 acquire load 之后的所有读取都可见。

概念层面:

Thread A (producer):                 Thread B (consumer):

  data = 42;                           while (!(p = flag.load(acquire)));
  str = "Hello";                       // 以下读取能看到 Thread A 在 release 之前的所有写入
  flag.store(true, release);  ──→      assert(data == 42);   // ✅ 保证成立
     ↑                                 assert(*p == "Hello"); // ✅ 保证成立
     |
     └── release 保证:之前的写入(data, str)
         不会被重排到此 store 之后

x86-64 编译器实际生成的汇编(GCC/Clang):

;; release store (x86-64)
;; std::atomic<int> x; x.store(val, std::memory_order_release);
mov DWORD PTR [rdi], esi          ; 普通 MOV 指令!
                                   ; x86 TSO 已保证 StoreStore 顺序
                                   ; 编译器只需确保不重排(编译器屏障)

;; acquire load (x86-64)
;; int r = x.load(std::memory_order_acquire);
mov eax, DWORD PTR [rdi]          ; 普通 MOV 指令!
                                   ; x86 TSO 已保证 LoadLoad、LoadStore 顺序
;; release store (ARM64)
;; x.store(val, std::memory_order_release);
stlr w1, [x0]                     ; Store-Release 指令
                                   ; 硬件保证:之前的读写不会重排到此 store 之后

;; acquire load (ARM64)
;; int r = x.load(std::memory_order_acquire);
ldar w0, [x0]                     ; Load-Acquire 指令
                                   ; 硬件保证:之后的读写不会重排到此 load 之前

从硬件角度理解 release/acquire 的作用:

Release Store 的效果:
  ┌──────────────────────────────┐
  │  之前的所有 store 已经进入   │
  │  Store Buffer(x86: FIFO    │
  │  保证顺序提交到 cache)     │
  ├──────────────────────────────┤
  │  Release Store 本身          │ ← 保证在之前的 store 之后可见
  └──────────────────────────────┘
  x86: Store Buffer 是 FIFO,天然保证 StoreStore 顺序
  ARM: STLR 指令包含隐式屏障,阻止重排

Acquire Load 的效果:
  ┌──────────────────────────────┐
  │  Acquire Load 本身           │ ← 此时可能触发 cache miss,
  │                              │   从其他 CPU 的 cache 获取最新值
  ├──────────────────────────────┤
  │  之后的 load/store           │ ← 保证在 acquire load 之后执行
  │  能看到 release 之前的写入   │
  └──────────────────────────────┘
  x86: 硬件已保证 LoadLoad/LoadStore 顺序,Invalidate Queue 中的
       失效消息在 load 前被处理(x86 不真正延迟处理 invalidation)
  ARM: LDAR 指令包含隐式屏障,且强制处理 Invalidate Queue

3.2. memory_order_seq_cst 如何实现全局同步

seq_cstrelease/acquire 更强:它提供一个全局唯一的操作顺序(single total order),所有线程看到的 seq_cst 操作顺序完全一致。

x86-64 编译器实际生成的汇编(GCC/Clang):

;; seq_cst store (x86-64)
;; x.store(val, std::memory_order_seq_cst);
xchg DWORD PTR [rdi], esi         ; XCHG 隐含 LOCK 前缀
                                   ; 效果:
                                   ;   1. 排空 Store Buffer(之前的 store 全部提交)
                                   ;   2. 原子地写入新值
                                   ;   3. 等待本次写入被其他 CPU 的 cache 确认(ACK)
                                   ;   4. 隐含全屏障(相当于 MFENCE)

;; 或者等价写法:
mov DWORD PTR [rdi], esi           ; 普通 store
mfence                             ; 排空 Store Buffer + 阻止 StoreLoad 重排

;; seq_cst load (x86-64)
;; int r = x.load(std::memory_order_seq_cst);
mov eax, DWORD PTR [rdi]           ; 普通 MOV 指令!
                                   ; ⚠️ 注意:x86 上 seq_cst load 不需要 MFENCE
                                   ; 因为屏障已经放在 store 端,而 x86 TSO 保证
                                   ; load 不会越过之前的 load(LoadLoad 有序)
;; seq_cst store (ARM64)
;; x.store(val, std::memory_order_seq_cst);
stlr w1, [x0]                     ; Store-Release(与 release 相同指令)
                                   ; ARMv8 中 STLR+LDAR 组合天然提供 seq_cst 语义

;; seq_cst load (ARM64)
;; int r = x.load(std::memory_order_seq_cst);
ldar w0, [x0]                     ; Load-Acquire(与 acquire 相同指令)

为什么 x86 的 seq_cst 把屏障放在 store 端而不是 load 端?

x86 TSO 下,唯一允许的重排是 StoreLoad(后面的 load 可能在前面的 store 提交到 cache 之前就执行了)。因此只需要在 store 之后 / load 之前插入屏障。编译器选择在 store 端处理,使用 XCHG(隐含 LOCK)或 MOV + MFENCE,这样 load 端可以保持为普通 MOV 指令,性能更好(因为 load 通常比 store 更频繁)。

关键差异:

Release Store(局部同步,成对使用):
-----------------------------------------
CPU 0: store data=42          ; 进入 Store Buffer
       store flag=true        ; release store,也进入 Store Buffer
                              ; x86 FIFO 保证 data 先于 flag 提交
                                    ↓ cache coherence (MESI)
CPU 1: load flag (acquire)    ; 如果读到 true,
       load data              ; 则保证读到 42


Seq_cst Store(全局同步,强制唯一顺序):
-----------------------------------------
CPU 0: XCHG [x], 1            ; 隐含 LOCK 前缀
         ↓
       1. 排空 Store Buffer(之前所有 store 提交到 cache)
       2. 锁定 cache line,原子写入
       3. 通过 MESI 发送 Invalidate 消息
       4. 等待所有 CPU 的 Invalidate ACK
         ↓
       此时所有 CPU 都知道 x 已经被修改
         ↓
CPU 1,2,3: 后续的 seq_cst load
           必须看到一致的全局顺序

3.3. release/acquire 与 seq_cst 的本质区别:IRIW 问题

release/acquire 只提供成对的局部同步,不保证全局顺序。经典的 IRIW(Independent Reads of Independent Writes) 问题展示了这一区别:

std::atomic<int> x{0}, y{0};

// Thread 1: 只写 x
void thread1() { x.store(1, memory_order); }

// Thread 2: 只写 y
void thread2() { y.store(1, memory_order); }

// Thread 3: 先读 x,再读 y
void thread3() {
    int r1 = x.load(memory_order);  // 读到 1
    int r2 = y.load(memory_order);  // 读到 0
}

// Thread 4: 先读 y,再读 x
void thread4() {
    int r3 = y.load(memory_order);  // 读到 1
    int r4 = x.load(memory_order);  // 读到 0
}
memory_order r1=1, r2=0, r3=1, r4=0 是否可能? 原因
acquire/release 可能 没有全局顺序要求;Thread 3 和 Thread 4 可能各自看到不同的写入顺序
seq_cst 不可能 全局唯一顺序:如果 Thread 3 看到 x=1 在 y=1 之前,Thread 4 也必须看到相同的顺序

这就是 seq_cst 的额外开销所换来的保证:所有线程对所有 seq_cst 操作看到完全相同的执行顺序。

3.4. 示例代码

#include <thread>
#include <atomic>
#include <cassert>
#include <string>

std::atomic<std::string*> ptr{nullptr};
int data{42};

void producer() {
  std::string* p  = new std::string("Hello");
  data = 42;
  ptr.store(p, std::memory_order_release);
}

void consumer() {
  std::string* p2;
  while (nullptr == (p2 = ptr.load(std::memory_order_acquire)));
  assert(*p2 == "Hello"); // never fires
  assert(data == 42); // never fires
}

int main() {
  std::thread t1(producer);
  std::thread t2(consumer);
  t1.join(); t2.join();
  return 0;
}

4. 原子操作的硬件实现

上面讨论的 memory order 解决的是可见性和顺序问题。而原子操作本身要解决的是不可分割性(atomicity) 问题——确保一个操作在观察者看来要么完全完成,要么完全没有发生。

4.1. 原子性的基础:自然对齐的 load/store

在大多数现代 CPU 上,自然对齐的基本类型(≤ 机器字长)的 load 和 store 天然就是原子的

x86-64:
  - 对齐的 1/2/4/8 字节 load/store 是原子的
  - 即使是普通的 MOV 指令,只要地址对齐,就是原子操作
  - 这就是为什么 relaxed load/store 在 x86 上就是普通 MOV

ARM64:
  - 对齐的 1/2/4/8 字节 LDR/STR 是原子的

因此,std::atomic<int>load()store() 本质上不需要特殊的硬件支持来保证原子性——对齐的 MOV/LDR/STR 天然就是原子的。std::atomic 在这里主要提供的是编译器屏障内存序语义

std::atomic(即使 relaxed)与普通变量的区别

即使使用最弱的 memory_order_relaxedstd::atomic 仍然比普通变量多做了关键的几件事:

特性 普通变量 atomic(relaxed)
操作原子性 不保证(可能被拆分为多条指令) 保证不可分割
编译器优化 可合并、删除、重排 禁止合并/删除(每次操作都会发出指令)
cache coherence 可能一直停留在寄存器中 触发 MESI 的 invalidate,保证跨核可见性
顺序保证 不保证顺序(relaxed),但保证最终可见
// 普通变量:编译器可能优化掉前一条
int x;
x = 42;
x = 43;   // 编译器可能直接只保留 x = 43

// atomic:每条 store 都必须执行
std::atomic<int> x;
x.store(42, std::memory_order_relaxed);  // 不会被优化掉
x.store(43, std::memory_order_relaxed);  // 也不会被优化掉

关键理解:atomic 的”原子性”和”内存序”是两个独立的概念relaxed 只放弃了顺序保证,但原子性、编译器屏障(禁止优化)、cache coherence 触发一个都不少。

4.2. Read-Modify-Write (RMW) 操作

真正需要特殊硬件支持的是 RMW 操作fetch_addcompare_exchangeexchange 等。这些操作需要在”读取-计算-写入”这个过程中保证没有其他 CPU 插入修改。

x86 实现:LOCK 前缀

;; std::atomic<int> x; x.fetch_add(1, relaxed);
lock add DWORD PTR [rdi], 1        ; LOCK 前缀保证原子 Read-Modify-Write

;; x.compare_exchange_strong(expected, desired);
lock cmpxchg DWORD PTR [rdi], esi  ; LOCK + CMPXCHG = 原子 CAS

;; x.exchange(val);
xchg DWORD PTR [rdi], esi          ; XCHG 隐含 LOCK(不需要显式写)

lock cmpxchg 为例,它在硬件中不是拆成多条独立微指令,而是作为一组融合微操作(fused micro-op) 执行:CPU 在执行期间锁定资源,保证”比较-交换”整个过程不可被打断。具体流程:(1) 发出 LOCK# 信号或锁住 cache line → (2) 通过 MESI 抢占独占权限 → (3) 等待其他核 invalidation ACK → (4) 比较并条件写入 → (5) 完成后更新 MESI 状态为 Modified。

LOCK 前缀的硬件实现(现代 x86):

┌──────────────────────────────────────────────────┐
│ 1. 检查目标地址是否在 L1 Cache 中                 │
│    ├─ 是 → Cache Line Locking(锁定 cache line) │
│    │       - 将 cache line 设为 Modified 状态      │
│    │       - 在操作期间拒绝其他 CPU 的访问请求     │
│    │       - 操作完成后释放                        │
│    └─ 否 → Bus Locking(锁定总线,较慢)          │
│            - 发出 LOCK# 信号                      │
│            - 阻止其他 CPU 访问内存                 │
│            - 现代 CPU 极少走到这条路径              │
├──────────────────────────────────────────────────┤
│ 2. 额外效果:                                     │
│    - LOCK 前缀指令隐含全内存屏障(等同 MFENCE)    │
│    - 排空 Store Buffer                            │
│    - 即使使用 memory_order_relaxed,               │
│      x86 上的 RMW 也有 MFENCE 效果                │
└──────────────────────────────────────────────────┘

重要: x86 上 LOCK 前缀指令始终隐含全屏障。这意味着 x.fetch_add(1, relaxed) 在 x86 上的性能和 x.fetch_add(1, seq_cst) 几乎相同——都会生成 lock add。relaxed 的性能优势主要体现在 ARM 等弱内存模型架构上。

ARM 实现:LL/SC 或 LSE 原子指令

;; fetch_add (ARM64, 传统 LL/SC 方式)
.retry:
    ldxr  w1, [x0]               ; Load-Exclusive:加载值并标记为"独占"
    add   w2, w1, #1              ; 计算新值
    stxr  w3, w2, [x0]           ; Store-Exclusive:仅当独占标记仍有效时写入
    cbnz  w3, .retry             ; 如果 stxr 失败(其他 CPU 写了同一 cache line),重试

;; fetch_add (ARM64, ARMv8.1 LSE 原子指令)
    ldadd w1, w0, [x0]           ; 硬件原子 load-add,无需重试循环

LL/SC(Load-Linked / Store-Conditional)机制:

┌───────────────┐              ┌───────────────┐
│    CPU 0      │              │    CPU 1      │
│               │              │               │
│ LDXR [addr]   │              │               │
│  ↓ 标记 addr  │              │               │
│  为 exclusive │              │               │
│               │              │ STR [addr]    │
│               │              │  ↓ 清除 CPU0  │
│               │              │  的 exclusive │
│               │              │  标记         │
│ STXR [addr]   │              │               │
│  ↓ 发现标记   │              │               │
│  已被清除     │              │               │
│  → 返回失败   │              │               │
│  → 跳回 LDXR  │              │               │
│  重试         │              │               │
└───────────────┘              └───────────────┘

4.3. 三种内存序对 RMW 的影响(以 fetch_add 为例)

;; x86-64: 三种内存序生成的汇编完全相同!
;; x.fetch_add(1, relaxed / acq_rel / seq_cst)
lock add DWORD PTR [rdi], 1       ; 都是 lock add(LOCK 隐含全屏障)

;; ARM64: 不同内存序生成不同汇编
;; relaxed:
    ldxr  w1, [x0]
    add   w2, w1, #1
    stxr  w3, w2, [x0]           ; 普通 LL/SC

;; acquire-release:
    ldaxr w1, [x0]               ; Load-Acquire-Exclusive
    add   w2, w1, #1
    stlxr w3, w2, [x0]           ; Store-Release-Exclusive

;; seq_cst:
    ldaxr w1, [x0]               ; Load-Acquire-Exclusive
    add   w2, w1, #1
    stlxr w3, w2, [x0]           ; Store-Release-Exclusive
    dmb   ish                    ; 额外的全屏障(某些实现)

4.4. 总结对比

┌──────────────┬─────────────────┬─────────────────────────┬────────────────────┐
│   操作类型    │   保证什么       │  x86-64 实现             │  ARM64 实现         │
├──────────────┼─────────────────┼─────────────────────────┼────────────────────┤
│ relaxed      │ 原子性           │ MOV                     │ LDR/STR            │
│ load/store   │                 │                         │                    │
├──────────────┼─────────────────┼─────────────────────────┼────────────────────┤
│ acquire      │ 原子性 +         │ MOV(编译器屏障)        │ LDAR               │
│ load         │ 后续操作不前移   │                         │                    │
├──────────────┼─────────────────┼─────────────────────────┼────────────────────┤
│ release      │ 原子性 +         │ MOV(编译器屏障)        │ STLR               │
│ store        │ 之前操作不后移   │                         │                    │
├──────────────┼─────────────────┼─────────────────────────┼────────────────────┤
│ seq_cst      │ 原子性 +         │ store: XCHG/MOV+MFENCE │ LDAR/STLR          │
│ load/store   │ 全局唯一顺序     │ load: MOV               │ (+ 可能额外 DMB)   │
├──────────────┼─────────────────┼─────────────────────────┼────────────────────┤
│ RMW          │ 原子性 +         │ LOCK 前缀指令            │ LDXR/STXR          │
│ (any order)  │ 不可分割的       │ (始终隐含全屏障)         │ 或 LSE 原子指令     │
│              │ 读-改-写         │                         │ (屏障取决于 order)  │
└──────────────┴─────────────────┴─────────────────────────┴────────────────────┘

参考资料

更多资料




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • NVIDIA GPU 架构:SP、SM 与 LSU 工作原理详解
  • al-folio 模板定制修改总结
  • al-folio 本地部署记录(Ubuntu 24.04)
  • C++ Traits
  • 道格拉斯-普克算法(Douglas–Peucker algorithm)