跳至内容
Ch-5 线程级并行与一致性

Ch-5 线程级并行与一致性

1. 线程与架构

1.1 从 ILP 到 TLP

线程级并行 (Thread-Level Parallelism, TLP) 利用的是多个线程之间的并行性。与 ILP 不同,ILP 在一条指令流内部寻找可重叠执行的指令,而 TLP 通常由软件、程序员或运行时系统显式识别:程序被拆成多个线程,每个线程内部仍然包含自己的指令序列。

从 Flynn 分类看,多处理器通常属于 MIMD (Multiple Instruction streams, Multiple Data streams):

  • 每个处理器可以取自己的指令;
  • 每个处理器可以操作自己的数据;
  • 多个处理器之间通过共享地址空间或消息通信协作。

本章讨论的多处理器更关注共享地址空间下的 TLP。多个处理器或核心紧密耦合,通常由同一个操作系统协调,共享同一个地址空间中的数据。

1.2 软件模型

利用 TLP 时,PPT 区分了两种软件模型:

模型含义典型场景
Parallel processing多个紧密协作的线程共同完成同一个任务科学计算、并行循环、矩阵运算
Request-level parallelism多个相对独立的请求或进程同时执行Web 服务、数据库查询、多用户任务

前者强调线程之间协作,常常需要共享数据和同步;后者强调请求之间相互独立,天然更容易扩展。缓存一致性问题主要出现在多个处理器缓存同一份共享数据时,因此和 parallel processing 的关系更密切。

1.3 集中共享

集中式共享内存 (Centralized Shared-Memory) 又常称为 SMP。所有处理器共享一个集中式主存,访问主存的延迟基本相同,因此也称为 UMA (Uniform Memory Access)。

这种结构的特点是简单:

  • 所有处理器看到同一个共享地址空间;
  • 主存位置集中,访问延迟比较均匀;
  • 缓存一致性可以通过共享总线上的 snooping 实现;
  • 常见规模较小,PPT 中给出的范围是约 32 个或更少核心。

集中式结构的问题也很直接:处理器数增加后,集中主存、共享缓存、总线或互连会逐渐成为瓶颈。即使每个核心有私有 cache,共享数据仍然需要通过统一的存储系统协调。

1.4 分布共享

分布式共享内存 (Distributed Shared Memory) 把物理内存分布到多个节点中,每个节点拥有本地内存,同时整个系统仍然给程序员提供共享地址空间。

分布式共享内存的好处是可扩展性更强:

  • 内存带宽随节点数增加;
  • 访问本地内存的延迟更低;
  • 更适合构建较大规模的多处理器。

但它引入了 NUMA (Nonuniform Memory Access):访问时间取决于数据所在位置。访问本地节点的数据较快,访问远程节点的数据较慢。程序性能因此不仅取决于并行度,还取决于数据放在哪里、线程跑在哪里,以及通信是否频繁。

分布式结构的主要代价是:

  • 处理器间通信更复杂;
  • 软件需要更注意数据局部性;
  • 缓存一致性协议通常不能简单依赖单一共享总线。

2. 并行限制

2.1 有限并行

并行处理的第一个障碍是:程序本身的并行部分有限。即使有很多处理器,串行部分仍然只能由少数处理器执行,这会限制总加速比。

这就是 Amdahl 定律 的核心:

\[ \text{Speedup}=\frac{1}{\frac{\text{Fraction}_{\text{enhanced}}}{\text{Speedup}_{\text{enhanced}}}+(1-\text{Fraction}_{\text{enhanced}})} \]

如果并行部分比例为 \(\text{Fraction}_{\text{parallel}}\),并且可以在 100 个处理器上理想加速,那么:

\[ \text{Speedup}=\frac{1}{\frac{\text{Fraction}_{\text{parallel}}}{100}+(1-\text{Fraction}_{\text{parallel}})} \]

PPT 的例题 1 问:若使用 100 个处理器,希望获得 80 倍加速,原计算中最多有多少比例可以是串行的?

令:

\[ 80=\frac{1}{\frac{\text{Fraction}_{\text{parallel}}}{100}+(1-\text{Fraction}_{\text{parallel}})} \]

可得:

\[ \text{Fraction}_{\text{parallel}}=0.9975 \]

因此串行部分为:

\[ 1-0.9975=0.0025=0.25\% \]

这说明 100 个处理器想达到 80 倍加速,程序中串行部分必须低到 0.25%。哪怕只有很小一段不能并行,也会显著限制最终速度。

2.2 分级并行

PPT 的例题 2 更接近真实情况:程序并不总是能用满所有处理器。假设程序:

  • 95% 时间可以使用全部 100 个处理器;
  • 剩下 5% 时间中,有一部分可以使用 50 个处理器;
  • 其余部分只能串行;
  • 目标总加速比仍为 80。

设剩余 5% 中使用 50 个处理器的比例为 \(x\),则:

\[ 80=\frac{1}{\frac{0.95}{100}+\frac{x}{50}+(0.05-x)} \]

求得:

\[ x=0.048 \]

也就是说,除了 95% 的完全并行部分外,剩下 5% 中还必须有 4.8% 能用 50 个处理器执行,只允许约 0.2% 真正串行。

这个例子强调:并行程序不是只有“串行”和“满并行”两种模式。很多程序在不同阶段能使用的处理器数量不同,实际加速比取决于各阶段的加权平均。

2.3 通信代价

第二个障碍是远程通信代价。并行程序即使有足够并行性,也可能因为访问远程内存、同步、传输共享数据而变慢。

PPT 的例题设定为:

  • 程序运行在 32 处理器系统上;
  • 远程内存访问延迟为 100 ns;
  • 时钟频率为 4.0 GHz;
  • 基础 CPI 为 0.5;
  • 有 0.2% 的指令发生远程访问。

时钟周期为:

\[ \text{Cycle time}=\frac{1}{4\ \text{GHz}}=0.25\ \text{ns} \]

一次远程访问折算为:

\[ \text{Remote cost}=\frac{100\ \text{ns}}{0.25\ \text{ns}}=400\ \text{cycles} \]

若 0.2% 指令发生远程访问,则额外 CPI 为:

\[ \text{CPI}_{\text{extra}}=0.2\%\times 400=0.8 \]

总 CPI 变成:

\[ \text{CPI}_{\text{total}}=0.5+0.8=1.3 \]

没有远程通信时 CPI 为 0.5,因此无通信版本相对有通信版本快:

\[ \text{Speedup}_{\text{no comm}}=\frac{1.3}{0.5}=2.6 \]

只占 0.2% 的远程访问就能让程序慢很多。这也是多处理器设计必须缓存共享数据、降低远程通信频率的原因。

3. 一致性基础

3.1 问题来源

多处理器使用私有 cache 后,同一个内存块可能同时出现在多个 cache 中。私有数据没有问题,因为只有一个处理器使用;共享数据则可能被多个处理器读写,导致不同 cache 中的副本不一致。

例如初始时内存中 X=1:

时间事件A 的 cacheB 的 cache内存 X
0初始状态--1
1A 读 X1-1
2B 读 X111
3A 写 X=0010

如果没有额外措施,B 的 cache 仍然保留旧值 1。之后 B 再读 X,可能读到过期数据。这就是 cache coherence problem。

3.2 两个概念

PPT 区分了 coherence 和 consistency:

概念关注问题
Coherence一次读可以返回哪些值
Consistency某次写入什么时候必须被后续读看到

Coherence 关心的是单个地址上的值是否合理。对于同一个位置 X,读操作不应该凭空读出一个从未写过的值,也不应该在明显已经更新后仍长期读到旧值。

Consistency 关心的是写入对其他处理器可见的时间和顺序。比如一个处理器刚写完 X,另一个处理器几乎同时读 X,此时写入数据可能还没有离开第一个处理器,系统未必能保证第二个处理器立刻读到新值。这个“什么时候必须看见”的规则由内存一致性模型定义。

不要混用两者

Coherence 更像是每个地址内部的正确性;consistency 更像是多个内存操作在时间上的可见顺序。本章重点是 cache coherence。

3.3 三条性质

一个 coherent memory system 通常需要满足三条性质:

性质含义
本地读写顺序处理器 P 写 X 后,若中间没有其他处理器写 X,则 P 再读 X 必须读到自己写的值
远程写可见一个处理器写 X 后,若另一个处理器在足够晚的时候读 X,且中间没有其他写 X,则应读到该写入值
写串行化任意两个处理器对同一地址的写,必须被所有处理器看成相同顺序

第一条保证程序顺序中的基本直觉:自己刚写的值,自己再读应该能看到。第二条保证写入最终会传播到其他处理器。第三条最关键:所有处理器必须对同一地址的写入顺序达成一致,否则不同处理器可能看到互相矛盾的历史。

4. 监听协议

4.1 基本做法

实现 cache coherence 的协议大致分为两类:

协议基本思想适用倾向
Snooping每个 cache 监听共享总线上的请求,自行维护块状态集中式共享内存、小规模系统
Directory每个内存块的共享状态集中记录在目录中分布式共享内存、大规模系统

Snooping coherence protocol 的前提是:相关请求会广播到所有 cache,所有 cache 都能观察到这些请求。每个 cache 中为每个块维护状态,并根据本地处理器请求和总线请求改变状态。

写操作有两种常见策略:

策略做法特点
Write invalidate写之前使其他 cache 中的副本失效带宽消耗较低,写者获得独占权
Write update/broadcast写时把新值广播给所有副本读者可继续命中,但写频繁时带宽开销大

现代系统更常见的是 write invalidate。因为同一个共享变量往往会被某个处理器连续写多次,第一次写时让其他副本失效,之后写者可以在本地 cache 中继续写,不必每次都广播新数据。

4.2 写失效

Write invalidate protocol 的基本流程是:

  1. 处理器想写某个共享块;
  2. 它先获得总线或互连访问权;
  3. 在总线上广播要失效的地址;
  4. 其他处理器持续 snoop 总线;
  5. 若发现自己的 cache 中有该块,就把对应块标记为 invalid;
  6. 写者获得该块的独占可写副本。

如果某个处理器发现自己拥有 dirty copy,而其他处理器请求这个块,它需要提供最新数据,并让原本的内存或共享缓存访问中止。因为此时最新值不在内存中,而在拥有 modified 副本的 cache 中。

写失效协议的实现通常在每个核心内加入有限状态控制器:

  • 响应本地处理器的读写请求;
  • 监听总线上的其他处理器请求;
  • 改变对应 cache block 的状态;
  • 必要时发起总线读、写失效、写回等动作。

4.3 MSI 状态

MSI protocol 为每个 cache block 维护三个状态:

状态含义内存是否最新是否可写
I Invalid本 cache 中没有有效副本无意义否
S Shared本 cache 有只读副本,可能被多个 cache 共享是否
M Modified本 cache 有已修改副本,且独占该块否是

M 状态隐含 exclusive:如果一个 cache 拥有某块的 modified 副本,其他 cache 不能同时拥有有效副本。因为内存已经过期,最新值只在这个 cache 中。

从本地处理器角度看:

当前状态本地读本地写
I读 miss,上总线取块,转为 S写 miss,上总线取独占权,转为 M
S读 hit,保持 S写 miss 或 upgrade,使其他副本失效,转为 M
M读 hit,保持 M写 hit,保持 M

从总线请求角度看:

当前状态看到其他读请求看到其他写/失效请求
S保持 S转为 I
M提供或写回最新数据,通常转为 S提供或写回最新数据,转为 I
I无动作无动作

MSI 的关键点是:写之前必须获得独占权;读可以共享;modified 数据被别人请求时,拥有者必须负责把最新值交出来。

4.4 MESI

MSI 的一个低效之处是:当某个块只被一个 cache 持有且未修改时,它在 MSI 中仍然是 S。如果本地处理器随后写它,协议还会发出总线操作去使其他副本失效,但实际上根本没有其他副本。

MESI 在 MSI 基础上加入 E (Exclusive) 状态:

状态含义
M Modified独占且 dirty,内存过期
E Exclusive独占但 clean,内存最新
S Shared可能被多个 cache 共享,只读
I Invalid无有效副本

E 表示该块只存在于当前 cache 中,且尚未被修改。它带来的优化是:

  • E 被其他处理器读时,转为 S;
  • E 被本地写时,直接转为 M;
  • 本地 E->M 不需要在总线上发送 invalidation。

因此 MESI 能减少“独占干净块”的无意义总线流量。

4.5 MOESI

MOESI 在 MESI 基础上加入 O (Owned) 状态,用来表示“共享但脏”的块。

O 状态的含义是:

  • 该 cache 是这个块的 owner;
  • 块内容比内存新,内存副本过期;
  • 其他 cache 可以持有 shared 副本;
  • owner 负责在其他处理器请求时提供最新数据;
  • owner 替换该块时需要写回内存。

MOESI 的典型转换是:当某个 cache 中的块处于 M,其他处理器发生 read miss 时,原拥有者可以从 M 转为 O,并直接把数据提供给请求者,而不必立刻写回内存。这样可以减少内存写回流量。

可以从几个属性理解 MOESI:

维度相关状态说明
validityM/O/E/S 都有效,I 无效是否可被处理器读到
exclusivityM/E 独占其他 cache 没有有效副本
dirtinessM/O 可能 dirty内存副本可能不是最新
ownershipM/O 明确带所有权谁负责提供或写回最新数据;E 有独占性,但不一定被称为 ownership 状态

4.6 MESIF

MESIF 在 MESI 基础上加入 F (Forward) 状态。F 用来指定多个 shared 副本中,哪一个 cache 应该响应其他处理器的读请求。

如果很多 cache 都处于 S,读请求广播后可能有多个 cache 同时尝试响应。F 状态相当于指定一个代表:

  • shared 数据仍然可以被多个 cache 持有;
  • 只有 F 状态的 cache 负责转发数据;
  • 最近请求该 cache line 的处理器常被赋予 F 状态。

MESIF 主要解决的是共享状态下“谁来响应”的问题,而不是改变写失效协议的基本原则。

4.7 缺失类型

Coherence miss 是由于一致性协议动作导致的 cache miss。PPT 中区分两类:

类型原因
True sharing miss另一个处理器真的写了当前处理器后续要读的同一个数据字
False sharing miss另一个处理器写了同一 cache block 中的其他字,导致整个 block 被失效

True sharing 反映真实的数据通信。例如 P1 写 x,P2 随后读 x,P2 必须看到 P1 的新值,因此会发生一致性相关缺失。

False sharing 则是 cache block 粒度带来的副作用。硬件通常以 cache block 为单位维护状态,而不是以单个 word 为单位。若 x1 和 x2 位于同一个 block,P1 只写 x1,P2 只读写 x2,它们逻辑上没有共享同一个变量,但仍会互相使整个 block 失效。

4.8 例题分析

假设 x1 和 x2 在同一个 cache block 中,且该 block 初始在 P1 和 P2 的 cache 中都处于 shared 状态。

时间事件分类原因
1P1 写 x1true sharing missP2 之前也有该块,且 x1 的值确实需要对其他处理器失效
2P2 读 x2false sharing missP2 读的是 x2,但它因为 P1 写 x1 被迫失效
3P1 写 x1false sharing missP2 上一步读 x2 让块重新共享,P1 再写 x1 又要失效整块
4P2 写 x2false sharing missP1/P2 操作不同字,但同块失效造成缺失
5P1 读 x2true sharing missP1 读取的是 P2 刚写过的 x2,确实需要获得新值

这个例子说明,false sharing 不代表硬件协议错了,而是数据布局让无关变量落在同一个 cache block 中。常见优化方法是 padding 或重新排列数据,让不同线程频繁写的变量落在不同 cache block 中。

5. 目录协议

5.1 目录思想

Snooping 协议依赖广播,适合小规模系统;当处理器数量增加时,每次 miss 都广播给所有 cache 会带来很高带宽压力。Directory-based cache coherence protocol 用目录记录每个内存块的共享状态,避免无差别广播。

目录通常放在每个节点中,负责追踪该节点本地内存地址范围内的 cache block:

  • 哪些节点持有该块副本;
  • 该块是否处于 shared、uncached 或 modified;
  • 若处于 modified,哪个节点拥有最新副本。

当某个节点请求一个块时,请求发送到该块的 home node。home node 查询目录后,只向相关 sharers 或 owner 发送消息,而不是广播给所有节点。

5.2 目录状态

目录协议中的常见块状态包括:

目录状态含义
Uncached没有任何节点缓存该块,内存副本最新
Shared一个或多个节点缓存该块,内存副本也是最新
Modified恰好一个节点拥有该块的 dirty 副本,内存副本过期

目录还需要保存 sharer set。例如 Sharers={P1,P3} 表示 P1 和 P3 持有该块的 shared 副本。若状态为 Modified,目录需要记录 owner,而不是一组普通 sharers。

目录协议处理请求时常见消息包括:

消息作用
Read miss请求读取某块
Write miss请求写入某块并获得独占权
Invalidate让其他 sharers 失效
Fetch从 owner 取回最新数据
Data value reply返回数据给请求者
Data write-backowner 替换 dirty 块时写回

5.3 状态转换

目录协议也可以画成状态机。下面这张图描述了单个 cache block 在目录协议中的转换关系。

几个典型过程可以这样理解:

场景目录动作
Uncached 上发生 read miss从内存返回数据,状态变为 Shared,Sharers={P}
Shared 上发生 read miss返回数据,并把请求者加入 sharers
Shared 上发生 write miss向其他 sharers 发送 invalidate,授予请求者独占权
Modified 上发生 read miss向 owner fetch 最新数据,请求者获得副本,状态变为 Shared
Modified 上发生 write miss向 owner fetch/invalidate,新的请求者成为 owner

目录协议的核心优势是减少广播;核心代价是需要额外目录存储和更复杂的消息交互。系统规模越大,目录的优势越明显。

5.4 本章主线

本章从 TLP 进入多处理器系统,核心问题是:当多个处理器共享地址空间并各自拥有 cache 时,如何让它们对共享数据保持一致的看法。

多处理器架构可以是集中式共享内存,也可以是分布式共享内存。集中式结构简单,适合较小规模;分布式结构带来更高带宽和更好扩展性,但引入 NUMA 和更复杂的一致性维护。

并行程序的速度不只取决于处理器数量。Amdahl 定律说明串行部分会限制加速比;远程通信例题说明极少比例的远程访问也可能显著拉高 CPI。

Cache coherence 定义了读操作能返回哪些值,而 consistency 定义写入何时对读可见。为了维持 coherence,系统通常使用 snooping 或 directory 协议。Snooping 适合小规模广播环境,directory 适合大规模分布式系统。

MSI 是最基本的写失效状态协议;MESI 用 E 减少独占干净块的无效总线请求;MOESI 用 O 避免 dirty shared 块立即写回内存;MESIF 用 F 指定 shared 副本中的转发者。协议越复杂,能减少的无效通信越多,但状态机和实现成本也越高。