Roxy's Library

Back

本文内容基于 2025 秋季《计算机体系结构》双语班课程讲述,如有差错,欢迎指正

Introduction:#

为什么要进行并行计算,一个很简单的理由:为了提高性能

传统上,人写的代码一般都是顺序的,天然适合串行执行:

  • CPU: 拥有单个中央处理器
  • 程序执行: 程序被分解成一系列离散的指令 ,指令一条接一条地执行
  • 时序: 在任何时刻,只有一条指令可以执行

这是在模仿人脑的习惯,于每个时间点执行一条指令。

而并行计算则比较反直觉,同时使用多个计算资源

  • CPU: 使用多个处理器(或多个计算核心)
  • 程序分解: 程序被分解成可以并发解决的离散部分(子程序)
  • 指令执行: 每个部分被进一步分解成一系列指令 ,来自每个部分的指令在不同的计算资源上同时执行

Flynn’s Classification#

根据数据流和指令流的数目,我们对并行进行了大致的分类

Flynn's Classification

  • SISD:处理器每次执行一条指令,这条指令一般处理一条数据
    • SuperScaler处理器虽然有多个FU,可以同时执行多条指令,但仍然算是SISD,因为指令来自于同一条指令流
  • SIMD:单条指令处理多个数据,如AVX512指令集进行向量化处理,ARM中NEON指令集
    • 适合做矩阵运算
    • GPU其实也算一种SIMD,自称SIMT(多个线程执行一条指令)
  • MISD:多条指令,同时处理一条数据,在极端不可靠场景(如太空)为了避免结果不稳定可能会使用
  • MIMD:多条指令流处理多条数据
    • 目前最常见的并行计算机类型(大多数现代超级计算机)

那现代CPU到底算是哪种?

  • 一个核自然是SISD,如果支持像AVX512这样的指令集那可以是SIMD,多个核组合起来是MIMD

本文重点关注 MIMD 概念,这在如今的超级计算机和多核处理器中非常常见

Mutiprocessor 中的存储结构#

随着核数呈幂律增长,进入多处理器时代 。多处理器中的内存层次结构有不同的访问模型 :

  • UMA:最早期的架构,Memeory Controller在CPU外,CPU Core 和 Memory Controller, Memory Controller 和 Memory 之间通过总线相连,访存的延迟相同

UMA

  • NUMA:每个 Chip 有自己的 Memory Controller 和自己的内存,但如果访问的数据不在自己的内存中需要进行Chip间通讯访问,导致访存的延迟不同

NUMA

  • 分布式内存系统:内存位于本地,通过外侧的网络互联,这种互联会导致延迟差距更大

DMS

对于Multiprocessor的影响#

多处理器环境下的主要挑战来自于数据块的“本地副本”(Local copy of data block),即缓存 。这导致了两个核心问题:内存一致性(Memory Consistency)和缓存一致性(Cache Coherence)问题

看下面的例子

  • Memory Consistency的问题 : 不同CPU不同内存地址的内存操作的顺序的问题

ChallengeCode1

不考虑Cache,对于#Code1 来说,AFlag的写入内存的顺序不可知,故A = 1可能在Flag = 1之后写入,Processor2可能打印0或1;对于#Code2 来说,两个处理器的执行顺序和写入顺序均不可知,故输出结果有四种可能

  • Cache Coherence的问题 : 同一份数据不同Core的Cache副本问题

ChallengeCode2

某个Processor更改数据后,其他Processor的Cache上副本可能不一致,其他Processor访问该数据可能会出错

Memory Consistency#

Memory Consistency 主要解决不同CPU不同内存地址的内存操作的顺序的问题

Deffusion 问题#

数据传播的速度未知,导致不同Processor看到的内存状态不一致,操作并不是原子的

Deffusion

图中所有Processor和内存通过一条Ring Bus相连, BA先传递到Processor 3,导致 R1的值不是我们想要的

对于这个问题,目前的方法是定义一个规范,硬件按照规范给与支持,软件根据规范写代码

Sequential Consistency (SC)#

按照程序顺序依次发起内存操作(Program Order 和 Memory Order一样)

等效于随机在不同Processor之间进行切换,切到哪个Processor就只执行它的访存指令,其他Processor的访存操作全都Stall

SCHardwire

基于这个规则,为保证一致性,处理器的Cache只能是Write through

SCexample

SC保证访存顺序和程序执行顺序一致,但不保证全局程序执行顺序

Total Store Order (TSO)#

只保证Store(写)的顺序和程序顺序一样

对于写操作,不需要直接写到memory上,而是写到一个FIFO的Write Buffer上面(这是之前说过的对Cache的一个优化)

TSOHardwire

  • 与SC的区别:每个Core有一个FIFO的Write Buffer
  • TSO的规则:
    • 只有写:访存顺序符合程序顺序,Write Buffer FIFO
    • 只有读:访存顺序符合程序顺序,直接从Memory读
    • 先写后读:不能保证,因为写到Write Buffer中,不能确定什么时候写回Memory

TSOexample

对于上面的例子:r1 == r3 == NEW, 因为会从Write Buffer里面读数据, r2, r4都可能是 0 或NEW

其他更relaxed的规则#

Partial Store Order (PSO)

  • 和TSO相似,不过允许对于不同地址的Store顺序改变

  • 对应于Write back的Cache

Relaxed Memory Order (RMO)

  • 允许 Load->Store、Store->Store、Load->Load、Store->Load 顺序改变

  • 需要程序员用一些指令(比如fence)来规范顺序

Weak Consistency (WC)

  • 内存仅在同步操作时保持一致,在 barrier 之间操作可以自由重排

Summary#

这种规范是硬件设计者和程序员之间的trade off,规则越Relaxed,硬件性能越好(一些放松的规则可以应用一些硬件的优化),但编程越困难

Cache Coherence#

Cache Coherence 主要解决同一份数据不同Core的Cache副本问题

关键思想:追踪每个Cache上数据块的状态,以保持全局更新同步

新增硬件:

  • “bus snooper”,用于监控总线上的数据传输
  • cache line 状态寄存器,记录每个cache line的状态

Write through 的规则#

Write through: 当处理器更新本地副本时,同时更新主内存

在这个条件下,处理Cache Coherence问题相对简单

snooper的做法:如果其他处理器更新的数据块存在于本地缓存中,则将其变为invalid

  • 原则上也可以将该数据更新到本地,但有可能该数据不会被使用

cache line的状态只有两种:valid 和 invalid

当处理器读取数据时,snooper会检查数据块的状态:

  • 如果数据块是valid,则直接从本地缓存读取
  • 如果数据块是invalid,则从主内存读取,并将其更新到本地缓存

Write back 的规则#

Write back: 当处理器更新本地副本时,不立即更新主内存,而是将其标记为dirty

snooper需要让其他处理器知道该数据块已被更新

MESI#

MESI Protocol 是最经典的缓存一致性协议,它定义了每个cache line有四种状态:

  • Modified(M):数据块已被修改,内容与内存不同
  • Exclusive(E):独有的副本,内容与内存相同
  • Shared(S):非独有副本,与其他处理器共享
  • Invalid(I):数据已被其他处理器更新

MESI 中定义了一些操作,这些操作会触发状态转换:

  • 处理器自身发起的操作: PrRd / PrWr (读/写)
  • 三种通过bus感知到的操作:
    • BusRd: 其他processor进行读操作,发生cache miss
    • BusRdX: 其他processor进行写操作,需要先从其他地方把数据读过来,再进行写(其他processor需要invalid自己的副本)
    • BusWB: 其他processor将数据写回内存
  • 两种snooper操作:
    • Flush: 把数据放到总线上,供其他processor使用
    • Flush*: 把数据放到总线上,同时告知有相同副本的processor不需要再Flush

下面我们逐步拆解,看状态转换图:

下述情景假设有两个processor P1,P2,我们只关注P1

情景1:Cold Miss(not-S)

设P1和P2的Cache Line都为I状态

  • P1和P2的Cache都没有数据块X,P1发起PrRd请求,发生Cold Miss
  • P1的snooper发送BusRd请求,从内存读取数据Data0,数据块状态从I变为E

如何判断应该设为E状态?

  • 因为P1的snooper没有接受到其他processor的Cache对数据Data0的响应,只接受到Memory的响应
  • Cache的响应应该比Memory更快

MESI_ColdMiss

情景2: Reads Local Copy(E)

设P1的Cache Line状态为E

  • P1发起PrRd请求,从本地Cache读取数据Data0
  • 数据块状态不变,仍为E

MESI_ReadLocal

情景3:Updates Local Copy(E)

设P1的Cache Line状态为E

  • P1发起PrWr请求,修改本地Cache上的数据Data0
  • 数据块状态从E变为M

MESI_ModifyLocal

情景4:Local Copy (M)

设P1的Cache Line状态为M

  • P1发起PrWr/PrRd请求,修改/读本地Cache上的数据Data1
  • 数据块状态不变,仍为M

MESI_LocalAccess_M

情景5:Write Miss

设P2有状态为E的本地副本,P1的Cache Line状态为I

  • P1发起PrWr请求,发生Write Miss,但P2有数据
  • P1的snooper发送BusRdX请求,P2的snooper接收到请求
  • P2的snooper将数据Data0通过Flush放到总线上,同时将自己的Cache行状态从E变为I
  • P1从总线读取数据Data0,更新本地Cache,修改数据为Data1,并将状态设为M

MESI_WriteMiss

情景6:Cold Miss(S)

设P2有状态为E的本地副本,P1的Cache Line状态为I

  • P1发起PrRd请求,发生Cold Miss,但P2有数据
  • P1的snooper发送BusRd请求,P2的snooper接收到请求
  • P2的snooper将数据Data0通过Flush放到总线上,同时将自己的Cache行状态从E变为S
  • P1从总线读取数据Data0,更新本地Cache,并将状态设为S

MESI_ColdMiss_S

情景7:Reads Shared Copy

设P1和P2的Cache Line状态均为S

  • P1发起PrRd请求,从本地Cache读取数据Data0
  • 数据块状态不变,仍为S

MESI_ReadShared

情景8:Updates Shared Copy

设P1和P2的Cache Line状态均为S

  • P1发起PrWr请求,修改本地Cache上的数据Data0
  • P1的snooper发送BusRdX请求,P2的snooper接收到请求后,将自己的数据块状态从S变为I
  • P1将数据块的状态设为M

MESI_UpdateShared

情景9:Capture BusRd (S)

设P1的Cache Line状态为S,P2的Cache Line 状态为I

  • P2尝试读数据,但发生Cache Miss,P2的snooper发起BusRd请求
  • P1的snooper接收到请求后,将数据Data0通过Flush放到总线上
  • P1数据块状态不变,仍为S,P2从总线读取数据Data0,更新本地Cache,并将状态设为S

MESI_CaptureBusRd_S

情景10:Capture BusRd (E)

设P1的Cache Line状态为E,P2的Cache Line 状态为I

  • P2尝试读数据,但发生Cache Miss,P2的snooper发起BusRd请求
  • P1的snooper接收到请求后,将数据Data0通过Flush放到总线上
  • P1数据块状态从E变为S,P2从总线读取数据Data0,更新本地Cache,并将状态设为S

MESI_CaptureBusRd_E

情景11: Capture BusRd (M)

设 P1的Cache Line状态为M,P2的Cache Line 状态为I

  • P2尝试读数据,但发生Cache Miss,P2的snooper发起BusRd请求
  • P1的snooper接收到请求后,将数据Data1通过Flush放到总线上
  • P1数据块状态从M变为S,P2从总线读取数据Data1,更新本地Cache,并将状态设为S
  • 内存更新为Data1

MESI_CaptureBusRd_M

情景12:Capture BusRdX (E)

设P1的Cache Line状态为E,P2的Cache Line 状态为I

  • P2尝试写数据,但发生Cache Miss,P2的snooper发起BusRdX请求
  • P1的snooper接收到请求后,将数据Data0通过Flush放到总线上,同时将自己的Cache行状态从E变为I
  • P2从总线/内存读取数据Data0,更新本地Cache,修改数据为Data1,并将状态设为M

MESI_CaptureBusRdX_E

情景13:Capture BusRdX (M)

设P1的Cache Line状态为M,P2的Cache Line 状态为I

  • P2尝试写数据,但发生Cache Miss,P2的snooper发起BusRdX请求
  • P1的snooper接收到请求后,将数据Data1通过Flush放到总线上,同时将自己的Cache行状态从M变为I
  • P2从总线读取数据Data1,更新本地Cache,修改数据为Data2,并将状态设为M
  • 内存更新为Data1

MESI_CaptureBusRdX_M

情景14:Capture BusRdX (S)

设P1的Cache Line状态为S,P2的Cache Line状态为I

  • P2尝试写数据,但发生Cache Miss,P2的snooper发起BusRdX请求
  • P1的snooper接收到请求后,将数据Data0通过Flush*放到总线上,同时将自己的Cache行状态从S变为I
  • P2从总线读取数据Data0,更新本地Cache,修改数据为Data1,并将状态设为M

MESI_CaptureBusRdX_S

最终的MESI状态转换图如下:

MESI_StateDiagram

Other Protocols#

除了MESI,还有其他一些缓存一致性协议:

  • MEI:没有S状态,当数据被多个处理器共享时,直接invalidate 其他处理器上的副本
    • Cache上只能有一份副本,容易导致乒乓现象
  • MSI:RISCV使用的一致性协议
    • 如果只有一个副本,MSI位于S状态,发生写时,需要发起BusRdX请求,额外占用带宽
    • MESI只需要把E改为M即可
  • MOESI:AMD的处理器使用该协议
    • 增加了O(Owned)状态,O表示该数据块是脏的,与内存不一致,且共享的
    • 当P1上数据是M状态,P2想要读数据时,MESI需要把数据写回内存
    • 而MOESI不更新内存,直接将数据提供给P2,并把自身M状态改为O,P2状态改为为S
    • 这样可以减少内存访问次数,提高性能(因为AMD的内存做的不好,笑)
  • MESIF:Intel的处理器使用该协议
    • 增加了F(Forward)状态,F表示该数据块是共享的,但负责将数据提供给其他处理器
    • 当P1上数据是S状态,P2想要读数据时,MESI需要广播BusRd请求,所有拥有该数据块的处理器都需要响应
    • 而MESIF只需要让F状态的处理器响应请求,获得数据的处理器改为F状态,发送数据的处理器(F)改为S状态
    • 减少了总线上的响应数量,提高性能

多层 Cache Coherence#

Naive 的方法:每一层 Cache 都实现 snooper

Intel 的方法:snoop filter

  • 因为高级的Cache(L1 Cache)是低级Cache(L2 Cache)的子集,把L2 作为snoop filter
  • 有snoop请求到来时,L2替L1检查是否要响应。
  • L1必须是Write through

一些Q&A#

Q:Flush*和MESIF有啥区别?

A:MESIF明确了F状态的Cache行负责响应请求,(猜测)而Flush*可能需要某个S告诉其他Cache它不需要响应

Q:如何确定是cache还是memory服务请求?

A:传统的MSI是memory服务请求,MESI优先尝试让cache服务请求

Q:E state的processor为何还要flush data to memory?

A:(没找到具体解释,个人猜测)MESI继承了MSI的设计,同时M和E states比较接近,所以沿用了flush。Memory controller在侦听到flush之后,会进一步判断是否需要进行memory update操作。

Directory Based Coherence#

snooper的缺点:

  • 对于大型系统,snooper需要监视来自每个处理器的总线流量,导致总线带宽瓶颈
  • 在总线上广播会有信号衰减,限制了系统的可扩展性

一种解决思路:

  • 把Cache line的状态信息存储在一个集中式的Directory中
  • Directory跟踪每个Cache上数据块的状态,以保持全局更新同步
  • 通过查询Directory来确定数据块的位置

DirectoryBasedCoherence

但是,集中式的Directory需要被所有Core访问,可能会成为瓶颈

一种改进思路:分布式Directory(这不就转化成了Directory Coherence了吗啊喂)

分布式Directory#

Directory只负责记录本地memory副本的状态

  • 访问数据时一定要知道数据在哪个Home Node(memory&processor)上,可能是通过某种哈希或其他方式

DistributedDirectory

图中的Interconnect可以是总线,也可以是Mesh网络

下面分析几种情形

情景1:Read Miss

Case A:设数据 Z 是共享的(clean但不是modified)

  • P0想要访问数据Z,访问HomeNode
  • HomeNode 返回数据Z
  • 更改数据Z的Directory状态,记录P0有数据Z的副本

Case B:设数据Z是共享的(但是是dirty的)

  • P0想要访问数据Z,访问HomeNode
  • HomeNode 发现数据Z是dirty的,把数据Z的Owner Node(假设是P1)找出来发给P0
  • P0找到P1,P1把数据Z发给P0和HomeNode
  • 更新 Directory 状态,no-dirty,P0有副本

情景2:Write Miss

设数据 Z 是共享的

  • P0想要写数据Z,访问HomeNode
  • HomeNode 发现数据Z是shared的,发送共享者的ID给P0
  • HomeNode 更新 Directory 状态,dirty,只有P0有副本
  • HomeNode 把数据Z发给P0
  • P0给共享者发送 Invalidate 请求
  • 共享者Invalidate 自己的副本,并向P0发送ack
  • P0 Invalidate HomeNode的副本
  • HomeNode 返回 ack 给P0
  • P0 修改数据Z

太复杂了图就省略了

Summary#

Coherence 的复杂行取决于 cache的类型

  • write back 比 write through 复杂
  • 多级 cache 比 单级复杂
  • cache是否有包含关系也会影响复杂度

Coherence 两种实现

  • snooper-based 适合小规模系统
  • directory-based 适合大规模分布式系统

Coherence 协议,重点MESI

Mutiprocessor Environment:Memory Consistency&Cache Coherence
https://astro-pure.js.org/blog/computerarch_11_13
Author GreyRat
Published at November 28, 2025
Comment seems to stuck. Try to refresh?✨