[A Primer Of CC and MC] 1. 对于 Memory Consistency 和 Cache Coherence 及其关系的一点思考

A Primer Of CC and MC - 对于 MC 和 CC 的一点思考

前言

这个专栏是一个全新的专栏,旨在记录我学习书本 A Primer Of CC And MC 的学习过程。

最近在自制 OS 内核,自然而然的搞到了并发,结果很快就被 -O2 优化教做人了。遂不服气,开始研究 CC (Cache Coherence ) 和 MC(Memory Consistency), 势必要搞出点名头。

1 缓存和乱序执行

1.1 缓存的诞生

很久很久以前,CPU想获取或者写入数据,都是直接控制总线读写内存。诶,这多方便,多直接,不是吗?但是,很快架构师就发现,这 DRAM 的速度相对于cpu的寄存器而言,可实在是慢得不敢恭维啊。那怎么办?

于是,我们不得不请出那句计算机界的至理名言(好吧,其实是冷笑话)了: 所有的问题都可以通过加一层抽象层来解决 ,如果加一层解决不掉,那就--再加一层!

(记住这句话,后面讲 MC 和 CC 的关系的时候要考!)

所以,经研究决定,我们决定给 CPU 加一层抽象--缓存。对,加一块容量比寄存器稍微大一点,但是速度又比内存快得多的SRAM。我们将所有常用的数据集成在缓存中,需要的时候快速取就好。这不就解决了速度问题了吗?

但是,如果程序员在写汇编代码的时候还得管缓存的话,那可就麻烦死了。所以,我们必须保证,程序员在写汇编代码的时候,脑子中只有CPU和内存,而没有缓存,这样才不至于让程序员在写代码的时候由于脑子过载而死机。由此,我们可以引出缓存的一个特点,那就是透明性

1.2 CPU 的乱序执行

还是在很久很久以前,那会 CPU 非常的呆,看到内存中的指令只会 FDEMW ,这多符合直觉呀。

但是,很快架构师就发现,诶,我只需要略施小计,对指令进行重排列,就可以加快执行速度呢!

所以,后续的 CPU 就引入了一个新技术 -- 乱序执行 ,它能够对指令进行重排列,在保证最终结果一致的前提下更换指令的顺序。

这个"最终结果一致",说人话,就是重新排列该核心中数条彼此无关的指令的执行顺序。例如

asm 复制代码
mov eax,1 ; Instruction A
mov ebx,2 ; Ins B

这两条指令彼此之间是无关的,所以可以重新排序,先执行 Ins B 和先执行 Ins A 的结果是一样的,所以 CPU 为了速度,可能会先执行 Ins B 再执行 Ins A. 这就是重排列。而若是

asm 复制代码
mov [eax],1
mov ebx,[eax]

这样子的话,那就不能重排列了,因为两条指令是相关的.

由此,我们就可以成功通过让 CPU 对指令重排序,从而加快 CPU 的执行速度。

2 问题: 缓存不一致和内存执行顺序的不一致

2.1 CPU 乱序执行在多核心下的不一致性

乱序执行后,CPU 单核性能确实提上来了。然而,工程师设计之初可没想到有多核 -- 它只保证单核心最终的结果正确。我们来看另外一个情况, 就是书中的 3.2:

此时此刻,C2 核心中的 r1r2 变量,可没有什么关系啊,那我把 L2 移动到 L1 前面可不可以? 在只有 C2 的情况下,这可一点问题都没有啊!

但是问题是,我们将 C1 核心和 C2 核心连起来看?诶,很显然,把 L2 丢上面是违背我们原来想要的逻辑的。所以,L1 和 L2 顺序是不能换的。

而这个情境,其实就是后续会涉及到的 Load-Load 重排序,在此先提前涉及一下。

那如何解决这个问题呢?我们必须建立起一套秩序,尽量减少甚至杜绝这类问题。

2.2 缓存带来的不一致性

既然都提到缓存了,那就展示一下书上所写的缓存的架构吧。

我们可以看到,科学家远比我们想象的要丧心病狂,他们给 CPU 加了很多很多的缓存。我们也可以看到,每个核心都会有一个私有缓存

!IMPORTANT

在接下来的文章中,我们只关注私有缓存 部分,我们将 LLC(L3 Cache) 和内存块看作一个东西,不加以区分。

而前面讲到了,我们实现缓存的目的有两个:

  1. 让缓存相对于程序员透明
  2. 提升CPU访问主存的速度

当然,书中有句老话说得好:

所以我们也必须得保证它的确定性

现在,每个核心都有一个私有缓存,那每个核心之间的私有变量肯定是会出现不同步的。但是不同步就意味着会出错,连正确性都无法保证。

还有,就是如果缓存必须和内存保持强一致性的话,那不就和主存访问一样了吗,那第二个目的就搞不定了,缓存增加了个寂寞。

所以,我们必须建立一套协议,能够实现这两个目的的同时,又可以保证正确,

3 解决: 内存一致性(MC)和缓存一致性(SC)

3.1 抽象: CPU 一致性读写模型

在解决 2 的两个问题之前,我们先尝试对 CPU 一致性做一个抽象建模。

在 1.1 中我们可以知道,CPU 是乱序执行的。那么我们可以尝试建立如下的抽象层,每一层都对上一层暴露了几个接口:

抽象层级 1: 程序员眼中: 哦,CPU 提供了访存指令 mov/ldr/sto

抽象层级 2: CPU 核心译码器眼中: 现在我有一堆指令, 我需要调用指令重排, 加快速度。至于具体怎么访存?交给下一层的控制器吧。

抽象层级 3: CPU 核心缓存控制器眼中: 我现在收到了一堆对于单个内存的读写指令。我需要确定这个内存是在我自己的Private D-Cache 中,还是在别的核间缓存里,或者是在主存中,然后获取数据。

(后续抽象层级省略)

现在我们有了一个模型: CPU的译码器负责重排列访存指令,而访存指令访问缓存控制器用它获取数据。

3.2 抽象层级 3 实现: 缓存一致性协议(CC)

既然我们的目的是对 抽象层级 2 的接口进行设计,那么我们就首先提供两个接口:

这个其实可以继续简化成 readwrite 两个接口。那我们必须得考虑, 如何实现这两个接口, 让它相对于抽象层级 2 透明?

3.2.1 第一次尝试: 增加一个传输层

现在让我们考虑如下情境:

asm 复制代码
Thread 1/CPU 1:
mov [rip+<var1>], rax ; 私有缓存命中

Thread 2/CPU 2:
mov rbx, [rip+<var1>] ; 问题: 这b是在 C1 的私有缓存中啊! 它长啥样我不到啊! 怎么办?

我们尝试加一个层级,这个层级可以让 C2 访问由 C1 修改的,但是目前未同步到主存的内存。对,其实直接把在 C1 存储缓存的值给拿过来。这样,就可以让 C2 获取 C1 的值,这样就可以保证数据的正确性

3.2.2 第二次尝试: 同时进行读和写

首先我们来看一个场景:

asm 复制代码
;CPU 1:
Tick 1: mov [rip+<var1>], rax

;CPU 2:
Tick 2: mov rbx, [rip+<var1>]

;CPU 3:
Tick 2: mov rcx, [rip+<var1>]

我们来看一下,这个,好像,没毛病! 就算有两个 CPU 在 Tick 2 对内存同时进行了读取,但是没有任何数据是读错的!

而且,这个还能很明显的加快 CPU 读写内存的速度,毕竟缓存的速度比内存快多了 (至于具体的速度,这个涉及到后续两个折磨死人的东西,一个叫做 MESI,另一个叫做 Store Buffer)。

所以,我们可以得出一个结论: 在一个 Tick 中, 很多个 CPU 可以同时读取多个数据.

那再来看一个场景:

asm 复制代码
;CPU 1:
Tick 1: mov [rip+<var1>], rax ;L1

;CPU 2:
Tick 1: mov [rip+<var1>], rbx ;L2

诶,这下出问题了! 我变量 var1 的值到底是 C1 的 rax 还是 C2 的 rbx 啊? 我不到啊! 由此可见, 多个 CPU 不能在同一 Tick 写同一块内存.

所以,我们必须加一个硬件仲裁器,强制对 L1 和 L2 的写入顺序进行排序,确保 L1 和 L2 有先后顺序,这样 CPU 就可以正常的写入数据了。

而至于怎么定义先后顺序来保证数据的正确性... 这个说来话长, 后续讲吧,反正现阶段只需要知道这样可以保证正确性就好了。

3.2.3 最终结论: SWMR 和缓存一致性

总之,现在所有的问题都解决了,数据的正确性,透明性,还有速度(多个 CPU 可以一起读同一片缓存,而缓存的速度比主存快得多。同时)。

所以,我们可以得出几个结论:

  1. 在一个 Tick 中, 很多个 CPU 可以同时读取多个数据
  2. 多个 CPU 不能在同一 Tick 写同一块内存

这就是 SWMR(Single Writer, Multi Reader) 的总结, 它可以很好的保证缓存的一致性(即正确性),同时兼顾速度和透明性。

3.3 对抽象层级 2 的接口实现: 内存一致性协议(MC)

这方面的话,要是在此详细讲开的话,太重了,所以我就一笔带过,详细讲的话就需要在后续的好几篇 Blog 讲了。

解决了缓存一致性的问题后,这个层级的目的其实和上个层级的一样: 如何设计一套方案,让 CPU 在加快速度的同时,又尽力减少对程序员的影响?

前面讲到了 CPU 的乱序执行,我们把它摆出来。但是正如 2.1 所说,它数据乱了!!!

由此,我们有两个方案:

  1. 既然解决不了问题,那就建立一套规范,让提出问题的人闭嘴,自己遵守去。
  2. 我们可以通过改 CPU 的乱序执行的步骤,尽量在 CPU 内部就能遵守 1 的规范,来搞定这些问题。

而这些规范就是内存一致性协议 ,至于如何平衡 CPU 和程序员之间的工作,那就又回到我们前几期讨论的经典命题了-- 权衡(Trade Off)

这个的话,在此就不细讲了,因为不同 CPU 采取的方案不同(例如 ARM 等 CPU 就倾向于方案 1,哈哈,心疼 ARM 嵌入式程序员三秒钟),后续讲内存模型的时候再详细扯。

总而言之,内存一致性协议其实目的就是加快 CPU 乱序执行速度的同时,如何保证 CPU 最终数据的正确性。

The End

内存一致性协议(MC) 的目的是加快 CPU 乱序执行速度的同时,如何保证 CPU 最终执行结果的正确性 ,它是宏观层面下的,是针对整个程序而言,换句话说,它关注的是一堆指令的顺序。

缓存一致性协议(CC) 的目的是,通过追加缓存,在加快 CPU 单条指令的执行速度(敲黑板啦,单条指令,不是整个程序!)的同时,如何保证最终的缓存最终结果的正确性。

本期文章写到这, 感谢大家的观看哦~萌新初涉系统编程, 有错误也请多多指正~

版权声明: 本文采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

作者: Sudo-su-Bash (Alien-Bash)

发布时间: 2026-04-05

原文链接: https://www.cnblogs.com/SudosuBash/p/19823256

相关推荐
消失的旧时光-194312 小时前
C++ 网络服务端主线:从线程池到 Reactor 的完整路线图
开发语言·网络·c++·线程池·并发
Arthas2175 天前
Java大厂面试:从Spring到微服务的全面技术考察
java·jvm·spring·微服务·面试·并发
CappuccinoRose7 天前
并行处理技术和并行处理机 - 软考备战(八)
并发·并行·并行处理机·并行处理技术
发际线还在16 天前
互联网大厂Java三轮面试全流程实战问答与解析
java·数据库·分布式·面试·并发·系统设计·大厂
SudosuBash1 个月前
[CS:APP 3e] 关于对 第 12 章 读/写者的一点思考和题解 (作业 12.19,12.20,12.21)
linux·并发·操作系统(os)
callJJ1 个月前
深入浅出 MVCC —— 从零理解 MySQL 并发控制
数据库·mysql·面试·并发·mvcc
西门吹雪分身1 个月前
JUC之公平锁与非公平锁
java·并发·juc·
消失的旧时光-19431 个月前
C++ 多线程与并发系统取向(二)—— 资源保护:std::mutex 与 RAII(类比 Java synchronized)
java·开发语言·c++·并发
消失的旧时光-19431 个月前
C++ 多线程与并发系统取向(七)—— 并发排障与工程纪律(从“会写”到“能控场”)
开发语言·c++·并发