编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
        • 1. 案例引入
          • 1.1 Peterson 锁的诡异失效
          • 1.2 标志位反转的先写后读崩溃
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 从 C++ 代码到 CPU 执行的五层下落
          • 2.2 为何这么切
        • 3. 多核 CPU 缓存架构
          • 3.1 L1/L2/L3 的拓扑与延迟
          • 3.2 cache line 与 false sharing
          • 3.3 缓存一致性的直观理解
        • 4. MESI 协议的状态机
          • 4.1 四个状态的语义
          • 4.2 状态转换的完整触发条件
          • 4.3 为什么 CPU 不能无限等确认
          • 4.4 MESI 为什么能保证一致性
        • 5. Store Buffer 与内存重排
          • 5.1 store buffer 的物理位置与作用
          • 5.2 Store-Load 重排的经典例子
          • 5.3 x86 的 TSO 模型为什么只允许 Store-Load 重排
        • 6. Invalidate Queue 与可见性延迟
          • 6.1 为什么需要 invalidate queue
          • 6.2 可见性延迟的量化
        • 7. 从硬件到 C++ 内存模型
          • 7.1 C++11 之前的可移植并发困境
          • 7.2 内存模型的三组核心关系
          • 7.3 从硬件内存序到 std::memory_order 的映射
        • 8. 各 CPU 架构的内存序全景
          • 8.1 x86-TSO、ARM、PowerPC 的对比表
          • 8.2 为什么 x86 上 relaxed 和 seq_cst 常常没区别
          • 8.3 ARM 上的 dmb 与 acquire/release 的真实代价
        • 9. 汇编证据:store buffer 与无效队列的可见影响
          • 9.1 Store-Load 重排在 x86 上的汇编重现
          • 9.2 ARM 上的四类重排重现
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次写操作的完整可见性旅程
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • 六大内存序详解
      • atomic原子操作原理
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

C++内存模型基石

# 40.C++内存模型基石

# 目录介绍

  • 1. 案例引入
    • 1.1 Peterson 锁的诡异失效
    • 1.2 标志位反转的先写后读崩溃
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 从 C++ 代码到 CPU 执行的五层下落
    • 2.2 为何这么切
  • 3. 多核 CPU 缓存架构
    • 3.1 L1/L2/L3 的拓扑与延迟
    • 3.2 cache line 与 false sharing
    • 3.3 缓存一致性的直观理解
  • 4. MESI 协议的状态机
    • 4.1 四个状态的语义
    • 4.2 状态转换的完整触发条件
    • 4.3 为什么 CPU 不能无限等确认
  • 5. Store Buffer 与内存重排
    • 5.1 store buffer 的物理位置与作用
    • 5.2 Store-Load 重排的经典例子
    • 5.3 x86 的 TSO 模型为什么只允许 Store-Load 重排
  • 6. Invalidate Queue 与可见性延迟
    • 6.1 为什么需要 invalidate queue
    • 6.2 可见性延迟的量化
  • 7. 从硬件到 C++ 内存模型
    • 7.1 C++11 之前的可移植并发困境
    • 7.2 内存模型的三组核心关系
    • 7.3 从硬件内存序到 std::memory_order 的映射
  • 8. 各 CPU 架构的内存序全景
    • 8.1 x86-TSO、ARM、PowerPC 的对比表
    • 8.2 为什么 x86 上 relaxed 和 seq_cst 常常没区别
    • 8.3 ARM 上的 dmb 与 acquire/release 的真实代价
  • 9. 汇编证据:store buffer 与无效队列的可见影响
    • 9.1 Store-Load 重排在 x86 上的汇编重现
    • 9.2 ARM 上的四类重排重现
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次写操作的完整可见性旅程
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 Peterson 锁的诡异失效

某嵌入式团队为新 SoC 写了一个自旋锁——用经典的 Peterson 算法(纯软件互斥,不用硬件 CAS)。在 x86 验证通过,换到 ARM 板子上——两个线程同时进入临界区:

// ====== 事故代码 V1:Peterson 锁在 ARM 上失效 ======
std::atomic<bool> flag[2] = {false, false};
std::atomic<int> turn = 0;

// 线程 0                            线程 1
void lock0() {                      void lock1() {
    flag[0].store(true, relaxed);       flag[1].store(true, relaxed);
    turn.store(1, relaxed);             turn.store(0, relaxed);
    while (flag[1].load(relaxed)        while (flag[0].load(relaxed)
           && turn.load(relaxed) == 1);        && turn.load(relaxed) == 0);
}                                   }
// 期望:两个线程永不同时进入临界区
// ARM 实测:偶尔同时进入——Peterson 锁失效!
1
2
3
4
5
6
7
8
9
10
11
12
13

根因:ARM 处理器上的 store 指令可以重排——flag[0]=true 和 turn=1 这两个 store 的顺序可能在另一个核心的视角里是反的。CPU 的 store buffer 让写操作延迟对其他核心的可见——即使这条 store 在程序里写在前面,另一个核心可能先看到后面的 store。

relaxed 内存序不提供任何跨线程的顺序保证——编译器和 CPU 都可以任意重排 relaxed 操作的顺序。

# 1.2 标志位反转的先写后读崩溃

同一个团队的数据消费——生产者先写 data、再写 flag。消费者先读 flag、再读 data。用 relaxed:

// ====== 事故代码 V2:relaxed 的先写后读失效 ======
int data = 0;                       // 普通变量
std::atomic<bool> ready = false;

// 生产者                            // 消费者
data = 42;                          while (!ready.load(relaxed));
ready.store(true, relaxed);         std::cout << data;  // ← 可能输出 0!
1
2
3
4
5
6
7

即使消费者已经看到 ready == true——data 仍然可能是 0。不是编译器优化的问题——是 CPU 在硬件层面重排了 store 的可见性顺序。 当 CPU 0 执行 ready=true 时,这条写可能在 CPU 1 看到 data=42 之前就已经被 CPU 1 观察到。

# 1.3 七个待解疑问

① CPU 缓存 L1/L2/L3 是怎么组织的? cache line 是什么?                → 第 3 章
② MESI 协议是什么? 四个状态怎么切换? 为什么需要它?                    → 第 4 章
③ Store Buffer 是什么? 为什么 CPU 需要它? 它导致了什么重排?           → 第 5 章
④ Invalidate Queue 是什么? 为什么导致可见性延迟?                       → 第 6 章
⑤ C++11 内存模型怎么把这些硬件特性抽象成可移植的规则?                   → 第 7 章
⑥ x86 和 ARM 的内存序有什么不同? 为什么 x86 上 relaxed 常等于 seq_cst?  → 第 8 章
⑦ 怎么在汇编层看到 store buffer 和 invalidate queue 的影响?            → 第 9 / 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 从 C++ 代码到 CPU 执行的五层下落

                    C++ 源代码
                         │
                    ┌────▼─────┐
                    │ ① 编译器 │    → 指令重排、常量折叠、死代码消除
                    │  优化    │       (as-if rule: 单线程行为不变)
                    └────┬─────┘
                         │
                    ┌────▼─────┐
                    │ ② CPU    │    → Store Buffer → Store-Load 重排
                    │  微架构  │       Invalidate Queue → 可见性延迟
                    └────┬─────┘
                         │
                    ┌────▼─────┐
                    │ ③ 缓存   │    → MESI 协议 → 缓存行状态切换
                    │  一致性  │       总线嗅探 → 多核同步
                    └────┬─────┘
                         │
                    ┌────▼─────┐
                    │ ④ 互连   │    → Ring/Mesh → NUMA 延迟差异
                    │  网络    │       跨 socket → 远程内存访问
                    └────┬─────┘
                         │
                    ┌────▼─────┐
                    │ ⑤ 物理   │    → DRAM 访问 60-100ns
                    │  内存    │
                    └──────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

C++ 内存模型是第 ① 和第 ②/③ 层之间的契约——它告诉编译器「在什么条件下可以重排我的代码」,同时通过 FENCE 指令告诉 CPU「在这里停下、把所有挂起的写操作提交」。

# 2.2 为何这么切

疑惑:为什么 C++ 需要内存模型——不能直接暴露 x86/ARM 的硬件语义吗?

论证:

  1. 不同 CPU 架构有根本不同的内存序——x86 是 TSO(Total Store Order),只有 Store-Load 重排。ARM 是 Relaxed 内存模型——四类重排(Store-Store / Load-Load / Load-Store / Store-Load)都存在。如果 C++ 直接暴露硬件语义——同一份代码在 x86 和 ARM 上的行为完全不同。
  2. 编译器也在重排你的代码——relaxed 编译器可以任意重排(只要单线程 as-if 规则允许)。acquire/release 编译器不能把读写移过这个边界。内存序告诉编译器和 CPU「你在这里不能越界」——两层约束、一层抽象。
  3. 反向验证:C++03 没有内存模型——只能用 volatile 和各种平台特定的 asm 屏障。结果就是 std::atomic 在 boost 里靠汇编实现、不可跨平台。

结论:C++ 内存模型是把不同 CPU 架构的内存序统一抽象为 6 个 memory_order 标签。写一次代码、编译器在不同架构上生成不同的 fence 指令——这是可移植并发的基石。


# 3. 多核 CPU 缓存架构

# 3.1 L1/L2/L3 的拓扑与延迟

         Core 0                          Core 1
    ┌──────────────┐              ┌──────────────┐
    │ Execution    │              │ Execution    │
    │ Unit         │              │ Unit         │
    ├──────────────┤              ├──────────────┤
    │ Store Buffer │              │ Store Buffer │
    ├──────────────┤              ├──────────────┤
    │ L1 Cache (32KB)  ~1ns      │ L1 Cache     │
    │ L2 Cache (256KB) ~3ns      │ L2 Cache     │
    ├──────────────┤              ├──────────────┤
    │  Invalidate  │              │  Invalidate  │
    │    Queue     │              │    Queue     │
    └──────┬───────┘              └──────┬───────┘
           │           共享 L3            │
           └────────────┬────────────────┘
                        │  ~12ns
                   ┌────▼─────┐
                   │ L3 Cache │ (8-32MB, 共享)
                   └────┬─────┘
                        │  ~40ns
                   ┌────▼─────┐
                   │   DRAM   │ (主内存 60-100ns)
                   └──────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

关键:每个 Core 有自己的 L1/L2——两个 Core 的 L1 之间不直接通信。所有跨核的缓存同步都通过共享的 L3 或总线协议(MESI)。

# 3.2 cache line 与 false sharing

struct Counters { int a; int b; };  // a 和 b 在同一条 cache line (64B) 内
Counters c;

// 线程 0:不停读 c.a         线程 1:不停写 c.b
// c.a 和 c.b 在同一条 cache line:
//  线程 1 写 c.b → MESI 让缓存行失效 → 线程 0 的读触发重新加载
//  → 尽管 c.a 没有变——false sharing——性能下降 10-100×
1
2
3
4
5
6
7

# 3.3 缓存一致性的直观理解

缓存一致性的核心问题:当 Core 0 写了一个值,Core 1 如何「看到」这个新值?

答案——没有中央控制器。每个 Core 通过总线协议广播自己的写操作,其他 Core 听到广播后使自己对应的缓存行失效。一致性的保证是一套分布式的协商协议——MESI。


# 4. MESI 协议的状态机

# 4.1 四个状态的语义

每条 cache line 在任意时刻处于四种状态之一:

Modified  (M):该 cache line 只在本核,已被修改(dirty),与主存不一致
              本核可以写——不需要通知任何人

Exclusive (E):该 cache line 只在本核,没有被修改(clean),与主存一致
              本核可以写——写后切为 M——不需要通知任何人

Shared    (S):该 cache line 可能在其他核也有副本(clean),与主存一致
              本核读——不改变状态
              本核写——需要 RFO (Request For Ownership) → M

Invalid   (I):该 cache line 无效——被其他核的写操作广播失效了
              本核读写都 miss——必须从 L3/主存/其他核加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.2 状态转换的完整触发条件

当前状态 → 触发事件 → 新状态

I → E (本核读 miss,且没有其他核持有该 line)
I → S (本核读 miss,且其他核持有该 line——shared)
E → M (本核写 hit——不需要通知任何人)
S → M (本核写 hit——需要 RFO 广播→其他核切为 I)
M → S (其他核读 hit——提供数据→shared)
E → S (其他核读 hit——提供数据→shared)
S → I (其他核写——接收 RFO→自己切 I)
M → I (其他核读——提供数据→自己切 S→其他核写→切 I)
1
2
3
4
5
6
7
8
9
10

# 4.3 为什么 CPU 不能无限等确认

RFO 的流程——Core 0 要写一个 M 状态的 cache line:

① Core 0 发 RFO 广播 → 告诉所有其他核「我要独占这条 cache line」
② 所有其他核 ACK → Core 0 收到全部 ACK
③ Core 0 把 cache line 切为 M → 执行写操作

如果没有 store buffer → 步骤 ② 可能花费 ~100 cycles
→ CPU 在等 ACK 的 100 个周期里什么都不敢做——管道干等
→ store buffer 就是为这个等待发明的(第 5 章)
1
2
3
4
5
6
7

# 4.4 MESI 为什么能保证一致性

疑惑:MESI 没有中央协调器——两个核同时发 RFO 会怎样?

论证——硬件层的分布式共识:

Core 0 和 Core 1 同时发 RFO 广播,争抢同一条 cache line:

① 两个信号同时在总线上传播
② 总线仲裁器(硬件电路)检测冲突 → 物理层串行化
   → 只允许一个信号「胜出」(先到先得——但「同时」的电学定义是微妙而精确的)
③ 胜出的核的 RFO 先被其他核看到 → 其他核的 cache line 被标记为 I
④ 另一个核的 RFO 到达时 → cache line 已经是 I → 重新请求

结果:双重 M 不会出现——物理层的串行化是最终仲裁。
1
2
3
4
5
6
7
8
9

这是分布式共识的硬件版本——不需要中央控制器,通过「广播+ACK」机制在物理层保证全局一致性。软件层的 atomic RMW 本质上是同一机制的上层接口——lock cmpxchg 的背后就是总线仲裁的串行化。


# 5. Store Buffer 与内存重排

# 5.1 store buffer 的物理位置与作用

Store Buffer 位于 Core 和 L1 Cache 之间——写操作先进 store buffer,不等 ACK 直接继续执行后续指令:

Core 执行 store A = 1;
  → A=1 进入 Core 的 store buffer(不等 RFO ACK)
  → Core 继续执行 load B;
  → load B 从 L1 缓存中读到值(可能是旧值——因为 store buffer 里的 A=1 还没提交)

Store-Load 重排:后面的 load 可能先于前面的 store 完成!
  因为 store 在 buffer 里等待——load 直接从缓存读——不需要等 store 提交
1
2
3
4
5
6
7

# 5.2 Store-Load 重排的经典例子

int a = 0, b = 0;

// Core 0              // Core 1
a = 1;                  b = 1;
int r1 = b;             int r2 = a;

// 期望:不可能 r1==0 && r2==0(至少有一个人先写)
// 实际:x86 上可以出现 r1==0 && r2==0!
//
// 根因:Core 0 的 a=1 在 store buffer 里等待 L1 的 a 缓存行失效 ACK
//       这段时间里 Core 0 执行 r1=b → 从 L1 读 b(Core 1 的 b=1 可能还没失效 Core 0 的 b)
//       → r1 读到了旧值 0!同样 Core 1 也读到了旧值 a=0
1
2
3
4
5
6
7
8
9
10
11
12

# 5.3 x86 的 TSO 模型为什么只允许 Store-Load 重排

x86 是 TSO(Total Store Order) 模型——所有核看到的所有 store 的顺序是一致的(total order)。但 store 可以和非 wait 的 load 重排。

x86 允许的重排:只有 Store-Load(store buffer 造成)
x86 不允许的重排:Store-Store(store 之间有序)、Load-Load、Load-Store

ARM 允许的重排:四类全部都存在
1
2
3
4

# 6. Invalidate Queue 与可见性延迟

# 6.1 为什么需要 invalidate queue

当 Core 0 收到 RFO 广播(「我要失效你的 cache line」):

① Core 0 需要把自己的 cache line 标记为 I
② 操作本身很快——但不一定立即影响 Core 0 正在执行的 load

如果没有 invalidate queue → Core 0 必须等 cache line 完全失效后才发 ACK
→ Core 1 在等 ACK——两个核都在等——延迟叠加

为了解决:把失效请求放进 Invalidate Queue
→ Core 0 立即 ACK(不等实际失效完成)
→ 延迟从 ~100 cycles 降到 ~10 cycles
→ 代价:Core 0 后续的 load 可能在 cache line 实际失效之前就执行了
1
2
3
4
5
6
7
8
9
10

# 6.2 可见性延迟的量化

事件 延迟 说明
L1 读 hit 1 ns (~4 cycles) —
L2 读 hit 3 ns (~12 cycles) —
跨核 cache line 迁移(M→I→E) 20-50 ns MESI 全流程
跨 socket cache line 迁移 80-150 ns NUMA 远程访问
主存访问 60-100 ns DRAM

# 7. 从硬件到 C++ 内存模型

# 7.1 C++11 之前的可移植并发困境

// C++03 的「并发」——完全依赖平台
#ifdef __GNUC__
    __sync_synchronize();  // GCC 的 full memory barrier
#elif _MSC_VER
    _ReadWriteBarrier();   // MSVC 的编译器屏障(不是 CPU 屏障!)
#endif
// 每种平台不同的 asm barrier——不可移植
1
2
3
4
5
6
7

C++11 统一了这一切——std::atomic + std::memory_order 把平台差异封装在标准库中。

# 7.2 内存模型的三组核心关系

① Sequenced-before (SB) —— 同一线程内的代码顺序
    单线程内的求值顺序——标准第 [intro.execution] 定义

② Happens-before (HB) —— 跨线程的事件顺序
    如果 A happens-before B → A 的所有副作用对 B 可见

③ Synchronizes-with (SW) —— acquire/release 配对
    release store SW acquire load → release 之前的所有写对 acquire 之后可见
1
2
3
4
5
6
7
8

Happens-before 是核心——所有 memory_order 的意义最终都归结为「建立 happens-before 关系」。

# 7.3 从硬件内存序到 std::memory_order 的映射

memory_order x86 指令 ARM 指令
relaxed mov (无 fence) str/ldr (无 fence)
acquire mov (隐含 Load-Load + Load-Store 有序) ldr; dmb ishld
release mov (隐含 Store-Load + Store-Store 有序) dmb ish; str
seq_cst mfence (或 lock xchg) dmb ish (全屏障)
acq_rel mov (隐含全部有序,仅剩 Store-Load) dmb ish

x86 的优势:强内存模型——大部分操作不需要显式 fence。ARM 几乎每次 acquire/release 都需要 dmb。


# 8. 各 CPU 架构的内存序全景

# 8.1 x86-TSO、ARM、PowerPC 的对比表

重排类型 x86 ARM (Cortex) PowerPC
Store-Store ❌ 不允许 ✅ 允许 ✅ 允许
Load-Load ❌ 不允许 ✅ 允许 ✅ 允许
Load-Store ❌ 不允许 ✅ 允许 ✅ 允许
Store-Load ✅ 允许 (Store Buffer) ✅ 允许 ✅ 允许

# 8.2 为什么 x86 上 relaxed 和 seq_cst 常常没区别

x86 的 Load 自带 acquire 语义(不允许后续 Load/Store 重排到前面)。 x86 的 Store 自带 release 语义(不允许前面的 Load/Store 重排到后面)。

所以在 x86 上——只有两种操作需要显式 fence:

  1. atomic RMW(lock xadd 等):隐含全屏障
  2. seq_cst store:需要 mfence(或 xchg)

relaxed load 和 acquire load 在 x86 上生成的汇编通常一样——因为 Load 自带有序。

# 8.3 ARM 上的 dmb 与 acquire/release 的真实代价

std::atomic<int> x;

// relaxed store:   str  r1, [x]          — 1 条指令
// release store:   dmb ish; str r1, [x]  — 2 条指令——多一条屏障

// 屏障的代价 ≈ 20-50 cycles(等待 store buffer 排空)
1
2
3
4
5
6

# 9. 汇编证据:store buffer 与无效队列的可见影响

# 9.1 Store-Load 重排在 x86 上的汇编重现

int a=0, b=0;

// Core 0:     mov [a], 1;  mov r1, [b]   → 没有 fence,可以重排
// Core 1:     mov [b], 1;  mov r2, [a]
// 结果:r1=0 && r2=0 是可能的(Store-Load 重排)
//
// 如果改成: mov [a], 1;  mfence;  mov r1, [b]  → 不可能 r1=r2=0
1
2
3
4
5
6
7

# 9.2 ARM 上的四类重排重现

ARM 上 str [x], 1; str [y], 1 在另一核可能看到 y=1, x=0——Store-Store 重排。x86 不会。


# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① CPU 缓存架构? 第 3 章:L1/L2 每核独立、L3 共享、cache line 64B
② MESI? 第 4 章:M/E/S/I 四状态——跨核同步——RFO 广播
③ Store Buffer? 第 5 章:写操作不等 ACK 先缓冲——导致 Store-Load 重排
④ Invalidate Queue? 第 6 章:失效请求异步处理——导致可见性延迟
⑤ C++ 内存模型抽象? 第 7 章:6 个 memory_order——编译器+CPU 双屏障
⑥ x86 vs ARM? 第 8 章:x86 强一致性(仅 S-L 重排),ARM 弱一致性(四类全有)
⑦ 汇编证据? 第 9 章:x86 Store-Load 重排重现、ARM dmb 屏障指令

案例①修复(Peterson 锁):把所有 relaxed 换成 seq_cst(或至少 acquire/release 配对)。

案例②修复(标志位):ready.store(true, release) + ready.load(acquire)——release 保证 data=42 在 ready=true 之前对所有观察者可见。

# 10.2 一次写操作的完整可见性旅程

Core 0: a.store(42, release);
Core 1: a.load(acquire);

═══════ Core 0 视角 ═══════
① 编译期:release → 编译器不能把前面的写重排到 store 后面
② Store Buffer:a=42 进入 store buffer
③ MESI:广播 RFO → 其他核失效 a 的 cache line → 收到全部 ACK
④ 提交:a=42 写入 L1 cache line(状态=M)

═══════ Core 1 视角 ═══════
⑤ acquire load → 不能在 a.load 之后重排前面的 load
⑥ a 的 cache line 已被失效(Core 0 的 RFO)→ 读 miss
⑦ 向 Core 0 发请求 → 获取 a 的最新值(M→S 迁移)
⑧ 读 a = 42 → 同时所有 release 之前的写对 Core 1 也可见了
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 10.3 设计哲学回扣

哲学 1:硬件不是「所见即所得」——CPU 在执行你写的代码之前已经重排了它

Store Buffer 和 Invalidate Queue 是 CPU 设计师为了性能而加入的两层缓冲区。它们不是 bug——是硬件为并行性付出的代价。 一台 5GHz 的 CPU 等一次 RFO ACK 就是 100 个周期——没有 store buffer,这 100 个周期里流水线是完全停滞的。C++ 内存模型的存在就是让程序员在不了解每款 CPU 微架构细节的前提下,写出正确的并发代码——但同时理解硬件是写出高效并发代码的前提。

哲学 2:x86 的「仁慈」是暂时的——弱内存模型是未来的趋势

x86 的强一致性(TSO)让很多并发代码「碰巧通过」。但 Apple M 系列、AWS Graviton、手机芯片——都是 ARM 弱内存模型。如果你的并发代码只依赖 relaxed 在 x86 上的「碰巧有序」——迁移到 ARM 时就是定时炸弹。 正确的做法:在暴露弱一致性最明显的 ARM 平台上测试你的并发代码。

哲学 3:C++ 内存模型是把硬件差异抽象为 6 个可移植的标签

6 个 memory_order 的根本意义:让同一份 C++ 并发代码在不同的 CPU 架构上生成不同的 fence 指令——但始终保持相同的 happens-before 保证。x86 上 acquire 退化为 mov(零开销),ARM 上 acquire 生成 dmb ishld(20-50 cycles)。这是「零开销抽象」在并发领域的终极体现:在不需要屏障的架构上不生成屏障指令。

哲学 4:理解硬件才能理解为什么需要 acquire/release——它们是 Store Buffer 和 Invalidate Queue 的精确控制

为什么 release store + acquire load 能建立 happens-before?因为 release 在硬件层是「排空 store buffer + 让所有之前的写对其他核可见」。acquire 在硬件层是「等 invalidate queue 处理完再读」。acquire/release 不是概念游戏——是对 store buffer 和 invalidate queue 的精确控制指令。

哲学 5:MESI 是全分布式的缓存同步——没有中央协调

MESI 不是「有一个中央仲裁器告诉所有核谁有哪条 cache line」——而是每个核通过总线监听(snoop)其他核的请求,独立修改自己的缓存行状态。这是一个纯粹的分布式协议——一致性来自局部规则(每个核的状态机)遵守全局约定(广播+ACK)。

# 10.4 速查表合集

x86 vs ARM 指令映射:

C++ x86 ARM
load(relaxed) mov ldr
load(acquire) mov (隐含有序) ldr; dmb ishld
store(relaxed) mov str
store(release) mov (隐含有序) dmb ish; str
RMW lock xadd (全屏障) ldrex/strex 或 ldaddal

下一篇:CPU 缓存和 MESI 是硬件基础。下一篇进入 41.六大内存序详解——relaxed/consume/acquire/release/acq_rel/seq_cst 每种内存序的精确语义、happens-before 与 synchronizes-with 的证明、acquire-release 配对的正确姿势、什么时候用 relaxed 是安全的。

上次更新: 2026/06/10, 11:13:41
Allocator分配器机制
六大内存序详解

← Allocator分配器机制 六大内存序详解→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式