内存模型的定义
内存模型(Memory Model)是定义数据一致性与执行顺序规则的一组规范。
备忘录
- 编程语言保证单线程的“程序顺序一致性”,比如为了防止CPU乱序执行,甚至主动对有依赖关系的变量插入内存屏障。
多核缓存
分层:
- 寄存器:最快,由重命名机制管理
- L1/L2/L3 Cache:核心本地缓存,按缓存行一致性(MESI)维护共享状态
- 主内存(DRAM):CPU访问需经总线或内存控制器
缓存级别 | 位置 | 速度(快) | 容量(小) | 每核私有/共享 | 说明 |
---|---|---|---|---|---|
L1 Cache | CPU核心内部 | ✅最快 | ❌最小 | 每核私有 | 通常分为指令缓存(L1I)和数据缓存(L1D) |
L2 Cache | 核心内部或旁边 | 较快 | 较小 | 每核私有或共享 | 有助于减缓L1未命中后的访问压力 |
L3 Cache | 核心集群共享 | 慢一些 | 更大 | 多核共享 | 减少跨核访问主存的延迟,常位于芯片中心 |
L4 Cache | 一些高端CPU或封装外DRAM | 最慢 | 最大 | 多芯片共享 | 非常少见(例如Intel的eDRAM) |
1 | [CPU Core] |
Store/Load Buffer
Load Buffer:
- 位置:在 CPU 核心内部,靠近 Load/Store Unit。
- 功能:暂存“load”(读取)指令的结果,等待内存或缓存返回数据。
- 使用场景:比如你访问一个变量 x,如果还没拿到内存值,它就被放在 load buffer 等待。
Store Buffer:保存尚未提交的 store 写操作(延迟写回),可被后续 load 查询(store-to-load forwarding)。内存屏障(memory fence)可强制“写入完成后再继续”。
- 位置:同样在 CPU 核心内部,紧挨 Load Buffer。
- 功能:当执行 x = 5 这样的写操作时,并不会立刻写入内存,而是先写入 store buffer。
- 延迟写回:写操作会被延后,在合适时机批量提交到缓存或主存中。
- 注意:这就是为什么需要内存屏障(memory fence)来强制“写入完成后再继续”。
Store/Load Buffer是一个具备调度逻辑的指令队列,不是为了“缓存数据”。
之所以叫”Buffer”而不是”Queue”,是强调其“乱序 + 并发调度能力”。
与普通队列的对比:
对比项 | 普通队列 | Load Buffer |
---|---|---|
数据结构 | FIFO 队列 | 类似 CAM(Content Addressable Memory)或乱序数组 |
访问方式 | 一次 pop 一个 | 任意条目都能被访问、发射、取消 |
顺序 | 严格顺序 | 支持乱序发射、乱序完成、乱序提交 |
功能 | 暂存数据 | 追踪地址、状态、与 Store Buffer 比较、rollback 等 |
Intel 和 ARM 的实际叫法
Intel 官方架构手册:
- 用词是 Load Buffer / Store Buffer
- 有时叫 “Memory Ordering Buffer”
ARM 架构(如 Cortex-A76)
- 常叫:Load-Store Queue(LSQ)
- 还会细分为:
- Load Queue(LQ)
- Store Queue(SQ)
但即使叫“Queue”,也不一定是严格的队列行为,而是调度窗口结构。
缓存行(Cache Line)
- 位置:在 CPU 的缓存(L1/L2/L3)中,缓存是以行(line)为单位存储的。
- 功能:每行通常是 64 字节,它是 CPU 和内存之间最小的同步单位。
- 状态(MESI 协议):
- M(Modified): 修改过,主存未同步
- E(Exclusive): 独占但未修改
- S(Shared): 可共享读取
- I(Invalid): 已失效
总线(Memory Bus / Interconnect)
- 位置:连接多个 CPU 核心、缓存、主存的通信通道。
- 功能:
- 数据在 CPU 和内存之间传输
- 执行原子操作时用于发送
LOCK#
或缓存一致性请求
🔗 示例:Intel 使用 Ring Bus、AMD 使用 Infinity Fabric。
主内存(Main Memory / DRAM)
- 位置:通常是系统主板上的 DRAM 芯片。
- 功能:存储所有不在 cache 中的数据,访问速度比 cache 慢很多。
🕓 延迟:访问主存通常需要 100ns ~ 几百ns,而访问 L1 Cache 只需几个 cycle。
内存控制器通道与总线的区别
维度 | 总线(Bus) | 内存控制器通道(Memory Channel) |
---|---|---|
硬件层级 | CPU、缓存、内存之间的共享通信总线 | 内存控制器内部的通路,直接连接 DRAM 芯片 |
作用范围 | 跨多核、跨缓存,整个系统范围 | 只负责访问主内存的某一部分,细粒度数据路径 |
连接对象 | CPU核心、缓存、内存控制器等 | 内存控制器与物理内存条之间的接口 |
作用 | 传递内存访问请求,实现缓存一致性协议 | 调度内存读写,执行具体内存命令 |
原子操作锁定对象 | 锁定总线,阻止其他核访问内存 | 锁定内存通道,防止同通道冲突访问 |
乱序执行
现代 CPU 采用乱序执行(Out-of-Order Execution, OoO)优化性能:
- 发射顺序有序(程序顺序)
- 执行顺序无序(根据数据/资源依赖动态调度)
- 提交顺序有序(借助 ROB 保证提交的程序语义)
核心机制:
- 指令乱序执行:由调度器和执行单元动态决定执行顺序。
- 寄存器重命名:使用临时物理寄存器消除写后写、写后读冲突。
- Load/Store Buffer:用于暂存尚未提交的读写操作。
- ROB(Reorder Buffer):按程序顺序提交指令(写入寄存器/内存),确保最终结果可预测。
程序顺序一致性(program-order consistency):
- 在本线程中观察到的顺序要“看起来像顺序执行”。
- 注意:多线程中可能出现乱序提交。
1 | 1. Fetch <-- ✅ 顺序取指,从指令缓存中取出下一条指令 |
TODO: 内存控制器
扩展:寄存器重命名(Register Renaming)
现代 CPU 为了支持乱序执行(Out-of-Order Execution)和指令级并行(ILP),会引入比架构寄存器更多的物理寄存器,来存储中间值。
寄存器重命名:CPU 用一张 寄存器映射表(Register Alias Table, RAT) 来将架构寄存器映射到物理寄存器。
- 架构寄存器:由 ISA 定义,固定不变。x86-64 是 16 个通用寄存器。
- 物理寄存器:由具体 CPU 实现,通常远多于架构寄存器。例如:
- Intel 的 Skylake 有 180 个左右物理寄存器(整数+浮点)
- ARM 的 Cortex-A 系列也有几百个物理寄存器(实际数量通常保密)
类型 | 说明 |
---|---|
物理寄存器(Physical Register) | CPU 内部实际使用的、很多个 |
架构寄存器(Architectural Register) | 程序看到的,例如 RAX、RBX 等,一般只有 16~32 个 |
举例:
1 | MOV RAX, 1 |
执行流程:
指令 | 分配的物理寄存器 | 对应动作 |
---|---|---|
1 | RAX → P1 | P1 ← 1 |
2 | RAX → P2 | P2 ← 2(与1可并行执行) |
3 | 用 P2 + 3 | 写入 P3,RAX → P3 |
注意:
- 三条指令都写的是 RAX,但实际上写的是 不同的物理寄存器(P1、P2、P3)
- 只有最后提交阶段才把 RAX 映射为最终结果的寄存器(P3)
内存屏障
屏障类型 | 会阻止 | 作用说明 |
---|---|---|
SFENCE |
Store → Store 重排序 | 前面的写必须完成再执行后面的写 |
LFENCE |
Load → Load 重排序 | 后面的读必须等前面的读完成 |
MFENCE |
所有内存访问 | 保证前面的 load/store 全部完成 |
问题 | 答案 |
---|---|
内存屏障是运行时插入的吗? | ❌ 插入时机是编译期,但运行时会执行它 |
屏障如何阻止后面指令提前? | ✅ 屏障执行时会强制刷新缓冲区、阻止乱序执行 |
缓存一致性协议(MESI)
MESI 协议的核心操作:
- 读(load):如果某核 cache 没有,就从主内存或其他核拉一份。
- 写(store):必须先让其他核的对应 cache line 失效(invalidate),否则就可能出现数据竞争。
atomic(哪怕是 relaxed)为什么会触发 invalidate?
当你写入一个 std::atomic
1 | x.store(123, std::memory_order_relaxed); |
- 这个 store 会编译为一条特殊的汇编指令(如 MOV 带 lock 前缀,或 STLR, STR 等)
- CPU 会发出总线事务(bus transaction)或利用缓存一致性协议 广播这个写入:
- 让其他核心中缓存这个变量所在 cache line 的副本 全部失效(Invalidate)
- 所有核心必须从这个核心或主内存读取最新的值(下一次 load)
内存序
内存序 = 程序中读写内存操作的可见顺序。
在理想世界(顺序一致的架构)中,你写的代码顺序就是 CPU 执行和其他线程观察到的顺序。但现实中:
- CPU 会乱序执行(out-of-order)
- 编译器会重排指令
- 缓存系统和 store/load buffer 会拖延内存访问
- 多核之间访问的是缓存,而不是主内存
👉 所以,需要有一套规则来定义:“内存操作到底是何时、如何被其他线程看见的”,这就是内存序的目标。
load
: 从内存读变量的值到CPU:内存 ➝ CPU。store
: 把一个值从 CPU 的寄存器或处理单元中,写到内存中的某个地址(变量):CPU ➝ 内存。relaxed
: 对其他线程的读写没有同步或顺序约束,仅仅保证本操作的原子性。acquire
: 对load
操作的内存序语义约束。- 当前线程中的任何读写都不能重排序到此
load
之前; - 在其他线程中
release
同一原子变量的所有写入在当前线程中可见。
- 当前线程中的任何读写都不能重排序到此
release
: 对store
操作的内存序语义约束。- 当前线程中的任何读写都不能重排序到此
store
之后; - 当前线程的写入对
acquire
同一原子变量的其他线程可见。
- 当前线程中的任何读写都不能重排序到此
acquire-release
: 对read-modify-write
操作的内存序语义约束。- 当前线程中的任何读写不能重排序到此
load
之前,也不能重排序到此store
之后。 - 其他线程
release
同一原子变量的写操作,对该modification
可见; - 该
modification
对acquire
同一原子变量的其他线程可见。
- 当前线程中的任何读写不能重排序到此
sequentially-consistent
:load
操作遵循acquire
语义;store
操作遵循release
语义;read-modify-write
操作同时遵循acquire
和release
语义;- 存在一个全序,其中所有线程都以相同的顺序观察所有修改。
内存序 | 插入屏障 | 特点 |
---|---|---|
relaxed | ❌ 无 | 只保证原子性,不保证顺序 |
acquire | ✅ 读屏障 | 阻止后续操作提前 |
release | ✅ 写屏障 | 阻止前面操作延后 |
acquire-release | ✅ 两者都有 | 双向有序 |
seq_cst | ✅ 全屏障 | 全局顺序一致性 |
步骤 | 普通变量 | atomic(relaxed) |
---|---|---|
1 | 编译器可优化 | 编译器必须保留这个操作 |
2 | CPU 可能重排 | CPU 仍可能重排(因为是 relaxed) |
3 | 缓存写入 | 可能停在本核 cache 中,不通知他人 |
4 | 不触发 invalidate | ✅ 会触发 cache line invalidate |
5 | 其他线程可见性低 | ✅ 其他线程可读取到最新的值(但时序不保证) |
私注:
acquire
: 当本线程读取后,本线程之后的操作都可以依赖这次读取(“后面的读写不得提前”);release
: 当本线程允许写后,本线程之前的读写都完成了,即之后的操作都可以依赖此前的写入(“前面的读写不得滞后”)。
和普通变量相比,atomic
atomic
保证操作的原子性
所谓“原子性”,是指某个操作(如 load、store、CAS)要么全部完成、要么完全不做,不会出现中间状态。
atomic
x; 1
x.store(42, std::memory_order_relaxed);
这个 store 是原子的:不会被打断、不会被分拆。
普通变量可能被编译器或 CPU 拆成多条指令(比如低 4 字节和高 4 字节分开发),多线程下可能看到“半成品”状态。
触发 CPU 的 cache coherence 协议(MESI等)
原子变量的访问会参与硬件层的缓存一致性协议,以保证多核 CPU 看到一致的值。
- 即使是 relaxed,store 也会触发对应 cache line 的写入通知(invalidate);
- 普通变量(非 volatile、非 atomic)的访问,编译器会缓存到寄存器中,不一定会回写。
禁止编译器优化(只针对原子变量本身)
编译器不能将多个 atomic store 合并,也不能删去它(不会当作“死代码”)。
x.store(42, relaxed) 每次都必须发出一条指令;
对普通变量:
1
2x = 42;
x = 43;可能直接编译为
x = 43;
,前一条被优化掉。
atomic<T, relaxed> 不做的事:
功能 | atomic(relaxed) | 普通变量 |
---|---|---|
保证原子性 | ✅ 是 | ❌ 否 |
保证可见性(跨线程) | ✅ 是(通过 cache 协议) | ❌ 否 |
禁止指令重排(保证顺序) | ❌ 否(只 relaxed) | ❌ 否 |
屏障为什么要设置两种?
性能优化 + 最小化开销 + 精确控制重排序方向。
屏障的本质是“控制指令重排序”
但它并不一刀切地“全部阻止”,而是:
屏障类型 | 阻止的方向(简化理解) |
---|---|
读屏障 | 阻止 读-after-读 重排序(即前面的读取不能晚于后面的读取) |
写屏障 | 阻止 写-after-写 重排序(即前面的写不能晚于后面的写) |
全屏障 | 阻止 读/写 与 所有后续指令的重排序 |
为什么分开这么细?(硬件视角)
现代 CPU 架构(如 x86, ARM)中:
读和写通路是分离的
- 读用 load buffer
- 写用 store buffer
两者乱序的模式、处理机制、性能代价都不同!
所以,硬件支持你只加读屏障(LFENCE)或写屏障(SFENCE),能以更小代价完成你想要的顺序语义。
举例:
操作 | 屏障类型 | 效果 | 例子 |
---|---|---|---|
等 flag==1 后再读取 data |
读屏障 | 禁止 data = ? 提前执行 |
load-acquire fence |
data=42 后,才写 flag=1 |
写屏障 | 禁止 flag=1 提前写出 |
store-release fence |
多线程通信中,强一致顺序 | 全屏障 | 所有读写顺序完全禁止重排 | x86 MFENCE |
- 你可以只阻止读乱序 → 读屏障
- 你可以只阻止写乱序 → 写屏障
- 你可以全部阻止 → 全屏障
原子操作
层次 | 机制 | 说明 |
---|---|---|
指令层 | 原子指令(如 lock cmpxchg 、xadd 、ldrex/strex ) |
提供不可中断的执行 |
缓存层 | MESI 协议 | 保证多个CPU缓存数据一致 |
总线层 | 总线锁定(旧方式) | 强制所有核等待该操作完成 |
内存层 | 内存屏障 | 保证内存操作顺序正确可见 |
CPU的原子指令如何实现的?
CPU 的原子指令是在硬件微架构层面通过微操作(micro-ops)调度、缓存控制、总线协议协调等机制联合实现的。我们可以从以下几个层面来详细剖析:
微架构层面:锁住特定资源
当执行原子指令时,CPU 内部会采取 锁定机制,确保该操作的所有子步骤在执行时不会被打断。
- 对于访问内存的原子操作,CPU 会尝试锁定:
- 缓存行(现代 CPU)
- 或内存总线(旧 CPU)
以 x86 的 LOCK CMPXCHG 为例,它内部其实是:
1 | if (mem == eax) { |
这个流程在硬件中不是三条指令,而是一个专门的微操作指令组合包,称为“fused micro-op”,由 CPU 保证“执行期间中断关闭、禁止抢占”。
缓存一致性协议(MESI)保障原子性
原子指令通常访问共享变量,这就需要解决缓存一致性问题。
举例:两个 CPU 都缓存了某个共享变量的值
当 CPU1 执行原子写入(例如 lock xadd)时:
CPU1 会尝试将该缓存行从 Shared → Modified 状态
MESI 协议要求其他 CPU(如 CPU2)将该缓存行标记为 Invalid
一旦独占,CPU1 就能安全完成读-改-写过程
这种方式实现了 原子访问缓存行。
总线或互联互锁(bus lock / interconnect lock)
对于不在缓存中的内存地址(如非一致内存访问 NUMA、I/O 内存等),现代 CPU 仍可能退回到:
- 总线锁定(Bus Lock)
- 内存控制器互锁(memory interconnect lock)
比如 x86 的 lock 前缀会让 CPU 发出 LOCK# 信号,告诉其他核暂时禁止访问这段地址。
这是一种慢但可靠的 fallback 机制,避免跨节点访问带来的竞态。
禁止乱序与流水线干预
现代 CPU 为了性能采用乱序执行(Out-of-Order Execution)和流水线(Pipelining)机制。但原子操作必须:
- 执行期间不得乱序
- 禁止乱序缓存访问(如 Store Buffer 提前写入)
- 强制所有早于它的读写完成,之后的读写等待
这依赖于:
- 微指令插入内存屏障(memory fence)
- 暂停 load/store buffer 的重排
- Flush pipeline / reorder buffer
这就是为什么原子操作通常比普通操作要慢的原因之一。
例子:lock cmpxchg 的硬件执行流程
假设 CPU1 执行 lock cmpxchg [X], eax,大致流程如下:
- CPU1 发出 LOCK# 信号或锁住缓存行 X(取决于地址是否在 cache)
- 向 MESI 控制器发请求,抢占地址 X 的 cache line 独占权限
- 在其他核心收到 invalidation 后,CPU1 获得修改权限
- CPU1 比较 X 与 eax 的值,如果相等则更新为新值
- 完成后通知 MESI controller,X 的状态设为 Modified
- 操作结束,缓存一致性仍然维护
整个过程是由硬件控制单元(Load/Store Queue、Execution Unit、Bus Interface Unit)协调完成的。
小结:CPU 实现原子操作的关键机制
机制 | 功能 |
---|---|
原子指令(如 lock cmpxchg , ldrex/strex ) |
提供原子读-改-写 |
微操作调度锁定资源 | 保证在流水线中不可拆分 |
MESI 协议 | 多核共享缓存一致性 |
总线锁或互联锁 | 防止跨核心或I/O地址出现竞态 |
内存屏障 & 禁止乱序 | 防止指令与缓存失序访问 |