深入理解宽松内存一致性模型与内存顺序的投机性违规
1. 宽松内存一致性模型概述
在多线程编程和多核系统中,内存一致性模型是确保程序正确执行的关键。传统的顺序一致性模型虽然简单直观,但会对硬件性能产生较大限制。因此,研究人员提出了多种宽松内存一致性模型,以提高硬件效率。
2. 添加 MEMBAR 指令以实现 TSO
在一些代码中,为了实现总存储顺序(TSO),需要添加 MEMBAR 指令。例如,对于以下代码:
T1
T2
T3
A=1
R1=A
R2=B
B=1
R3=A
在 T2 中需要强制执行全局加载 - 存储顺序,在 T3 中需要强制执行全局加载 - 加载顺序。添加 MEMBAR 指令后的新代码如下:
T1
T2
T3
A=1
R1=A
R2=B
MEMBAR 0100
MEMBAR 1000
B=1
R3=A
即使存储操作将值转发给加载操作,即使加载操作可以乱序执行,并且即使缓存以下的内存不是原子的,这段代码也只会产生符合 TSO 的结果,并且也符合顺序一致性。T2 中的 MEMBAR 指令强制前面的加载操作在存储操作可以发送到缓存之前全局执行。T3 中的 MEMBAR 指令强制第一个加载操作在第二个加载操作可以执行之前全局执行。
需要注意的是,MEMBAR 是一种本地的、线程内的栅栏,用于对给定线程发出的访问进行排序,它与屏障同步有很大不同,屏障同步是一种全局的、线程间的机制,用于在多个线程的执行之间强制执行顺序。
3. 依赖同步的宽松模型
目前所有的宽松内存模型都受到硬件效率和简单性的启发(主要是通过放宽存储 - 加载顺序),并且独立于程序员的直觉。然而,在硬件层面上,还基于除顺序一致性之外的其他程序员直觉构思了其他模型,并且对硬件施加的限制比顺序一致性更少。这些模型依赖于同步操作的语义。
同步在多线程编程中是必不可少的,无论是否存在多个缓存副本,也无论采用何种内存模型。当多个线程读写共享变量时,应该使用临界区来保护它们,临界区可以通过锁或屏障来实现。同步是线程交换控制信息的时间点,通常使用原子 RMW 指令来实现。如果使用共享数据的加载和存储操作进行同步(如在 Dekker 算法中),程序员应该将它们声明为同步变量,硬件可以对这些同步变量与其他共享变量进行不同的处理,只需要强制执行同步访问的正确交错,而不是所有共享内存访问的交错。
例如,对于通过 FLAG 传递值的简单程序:
INIT: A=FLAG=0
declare SYNC FLAG
T1
T2
...
...
A=1
FLAG=1
/release
while (FLAG==0)
/acquire
print A
在这段代码中,A 是普通的共享变量,而 FLAG 是用于同步的特殊共享变量,并被声明为同步变量。大多数共享变量是非同步变量。软件可以将此信息传达给硬件,以便硬件将对同步变量的访问视为栅栏(即像顺序一致性中的所有内存访问,或像 Sun 的 RMO 中的 MEMBAR)。在 RMW 指令(如 T&S 和 swap)中访问的共享内存位置也会自动被视为同步变量。
再考虑以下依赖顺序一致性来保证正确性的程序:
INIT: A=B=0
T1
T2
A=1
R1=B
B=2
R2=A
R3=R1+R2
在顺序一致性下,R3 唯一可能的值是 0、1 或 3。在不强制执行访问之间任何全局顺序的 RMO 系统中,硬件可能无法在将 T1 中的 A 存储操作传播到 T2 之前传播 B 的存储操作,从而导致 R3 的值为 2。为了避免这种情况,可以将代码包含在临界区中:
INIT A=B=0
declare lock L
T1
T2
Lock(L)
Lock(L)
A=1
R1=B
B=2
R2=A
Unlock(L)
Unlock(L)
R3=R1+R2
由于临界区的存在,两个线程不能同时执行其代码。因此,T1 和 T2 对 A 和 B 的访问永远不会并发执行,所有可能的执行都将保持顺序一致。实际上,即使存储操作不是原子的,临界区中的语句在每个线程中也会看起来像是原子执行的。唯一可能的结果是 R3 = 0 或 3,这两个结果都符合顺序一致性。
4. 弱排序模型
弱排序模型将对同步变量的访问视为栅栏,而对所有其他变量不施加顺序。一个线程包含关键和非关键代码部分。在非关键代码部分,只访问私有变量或只读共享变量。在关键代码部分,所有共享位置以互斥方式访问。因此,对同一内存位置的存储操作自然地通过由锁和解锁标记的临界区进行排序。
例如,对于图 7.31 所示的多线程程序:
lock(Lx)
X=X+2;
unlock(Lx)
lock(Lx)
X=A+X
unlock(Lx)
lock(Lb)
B=B+1
unlock(Lb)
lock(Lb)
B=B+1
unlock(Lb)
lock(Lb)
B=B+1
unlock(Lb)
INIT: A=2; B=0; X=0; Lb=Lx=0
3
T
2
T
1
T
C=D*E
C=C+K
Y=2*Y
Z=D+2
.....
.......
while(B<3);
while(B<3);
while(B<3);
在弱排序一致性模型中,锁中的 RMW 指令和解锁中的交换(或标记存储)在每个线程的代码执行中充当栅栏。解锁不能再作为常规存储实现,以便硬件能够识别它们。一些共享变量也可以被声明为同步变量,对这些同步变量的访问通常称为同步访问。
对于 RMW 访问,其全局执行的定义为:当 RMW 访问中的加载和存储操作都全局执行时,对内存位置的 RMW 访问才被视为全局执行。RMW 访问还必须是原子的,即没有其他线程可以在加载和存储之间更新内存位置。基于无效化的协议更适合强制执行 RMW 原子性。
在弱排序模型中,规则如下表所示:
| 访问类型 | 规则 |
| ---- | ---- |
| 常规加载和存储 | 不施加顺序 |
| 同步访问 | 必须在所有先前的访问全局执行后才能执行,并且后续访问必须在同步访问全局执行后才能执行 |
5. 释放一致性模型
释放一致性是弱排序的细化。同步访问进一步分为获取锁的同步访问(获取)和释放锁的同步访问(释放)。当一个线程成功获取锁时,它可以自由访问其临界区中的共享变量,因为它知道自己对这些变量具有独占访问权。因此,在获取操作全局执行之前,临界区中的加载和存储等访问不能绕过它。此外,当一个线程释放当前临界区的锁时,它隐式地将临界区中更新的共享变量的访问权让给其他线程,并允许其他线程修改它们。因此,临界区中的所有加载和存储操作必须在释放操作执行之前全局执行,以避免与临界区的其他执行产生干扰。
释放一致性模型的规则与弱排序模型的对比如下:
| 模型 | 规则 |
| ---- | ---- |
| 弱排序 | 同步变量的栅栏是双向的,同步过去和未来的访问 |
| 释放一致性 | 栅栏是单向的,获取操作同步未来的访问,释放操作同步过去的访问 |
释放一致性模型的执行顺序可以用以下 mermaid 流程图表示:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A(开始):::process --> B(获取操作):::process
B --> C{是否全局执行}:::process
C -- 是 --> D(执行临界区代码):::process
C -- 否 --> B
D --> E(释放操作):::process
E --> F{是否全局执行}:::process
F -- 是 --> G(结束):::process
F -- 否 --> E
在释放一致性中,获取操作必须在内存阶段全局执行后,后续访问才能执行。此外,获取操作必须等待存储缓冲区中的所有释放操作全局执行,但不等待常规存储操作。释放一致性在硬件层面比弱排序更宽松,因此更高效,但编程时需要将对同步变量的访问标记为获取或释放,而弱排序只需要将所有用于同步的变量声明为同步变量。
6. 内存顺序的投机性违规
在现代机器中,加载操作通常会进行投机性执行,即乱序执行。在投机性乱序(OoO)处理器中,大量指令可以同时处于执行状态。指令按线程顺序获取、解码和分发,然后在输入操作数可用时按数据流顺序开始执行。执行完成后,它们按线程顺序在历史或重排序缓冲区(ROB)中等待,直到更新寄存器或内存的时间到来,然后退役。在分发和退役之间,指令执行仍然是投机性的,如果前面的分支预测错误或前面的指令引发异常,执行可以回滚。
6.1 保守的内存模型执行
在投机性 OoO 处理器中,加载和存储操作在准备好提交和退役之前不能全局执行。一种非常保守的执行内存模型的方法是等待加载操作到达 ROB 的顶部后再返回其值,但这种方法会严重影响处理器性能,因为会暴露处理器的大内存访问延迟,并限制并行指令执行。
为了提高性能,处理器可以在知道加载或存储操作的地址后立即预取 L1 缓存中的内存块。这些非绑定预取受一致性协议的约束,如果发生异常则会被丢弃,因此不会影响内存模型的正确性,并且可以帮助隐藏加载或存储缺失的延迟。
另一种更激进的方法是利用投机性硬件基础设施提前执行投机性加载操作,使用其值,并在发生内存一致性模型违规时回滚执行。由于内存一致性模型违规非常罕见,这种方法非常有效。
6.2 内存顺序违规的检测
检测内存顺序违规有两种方法:
-
两次访问缓存
:处理器可以在前面的访问发送到内存之前投机性地执行加载操作,并使用其返回的投机值。当加载操作准备退役且所有先前的内存访问都已退役时,通过重新访问缓存将投机值与同一内存位置的当前值进行比较。如果值不同,则必须将执行回滚到加载操作;如果值相同,则加载操作在缓存副本上全局执行。但这种方法的问题是每次加载都需要访问两次缓存。
-
利用缓存和缓存一致性协议
:加载操作尽快执行并返回其投机值,同时将包含内存位置的块带入处理器节点的缓存层次结构。如果远程处理器尝试修改加载的数据,会向处理器节点发送通知(更新或无效化)。通过监听加载/存储队列,可以检查是否对同一数据进行了投机性加载操作。如果是,则必须回滚加载操作及其后续的所有投机指令,因为内存模型可能已被违反,加载操作返回的值可能已损坏后续指令。当加载操作进行了投机性执行并在未被召回的情况下到达 ROB 的顶部时,它可以直接全局执行而无需访问缓存,然后退役。
6.3 不同内存模型的投机性违规
- 顺序一致性的投机性违规 :在投机性顺序一致性中,所有顺序都必须全局强制执行。加载 - 存储顺序自动强制执行,因为存储操作必须到达 ROB 的顶部才能在缓存中执行;加载 - 加载顺序通过加载值召回机制强制执行;存储 - 存储顺序通过将存储操作按线程顺序插入 FIFO 存储缓冲区并依次全局执行来强制执行;存储 - 加载顺序也通过加载值召回机制强制执行,并且加载操作在 ROB 的顶部必须等待存储缓冲区中的所有先前存储操作全局执行。
- TSO 的投机性违规 :在 TSO 中,加载操作可以在本地对线程存储管道中的非全局一致(non-GP)值进行投机性执行。只要值仍在存储缓冲区中,投机性加载就不会被召回,因为该位置不一定被缓存。如果在投机性加载退役时,返回其值的存储操作仍在存储缓冲区中,则模型得到满足;如果存储操作在加载操作退役之前全局执行,则会加载一个块副本,加载操作将开始受到无效化或更新的影响。
- RMW 访问的投机性执行 :RMW 访问是复杂的指令,包括加载、寄存器操作和存储。以测试和设置指令为例,它可以作为加载操作进行投机性执行,但需要应用加载值召回机制。当测试和设置指令到达 ROB 的顶部时,必须作为存储操作全局执行。如果在退役之前收到更新或无效化通知,测试和设置指令将被召回,并且其后续的整个投机执行将被回滚。
- 弱排序的投机性违规 :在弱排序中,对同步变量的所有访问(同步加载、同步存储或 RMW 指令)必须视为栅栏。任何对同步变量的访问必须在 ROB 的顶部全局执行,并且在存储缓冲区中的所有存储操作全局执行之后,同时所有先前的加载操作也必须全局执行。加载同步变量可以进行投机性执行,但需要应用加载值召回机制。常规加载到非同步变量可以进行投机性执行,但不应用加载值召回机制,如果先前的同步或 RMW 访问被召回,其执行将被回滚。
- 释放一致性的投机性违规 :在释放一致性中,释放操作必须在所有先前的内存访问全局执行之后才能执行,获取操作之后的访问必须在获取操作全局执行之后才能执行。常规非同步加载可以在到达 ROB 的顶部之前进行投机性执行,不应用加载值召回机制。作为获取操作一部分的加载可以进行投机性执行,但必须应用加载值召回机制。获取操作可以绕过先前的存储操作,但不能绕过先前的释放操作,并且必须等待存储缓冲区中的所有释放操作全局执行。
不同内存模型的投机性违规规则总结如下表:
| 内存模型 | 加载 - 存储顺序 | 加载 - 加载顺序 | 存储 - 存储顺序 | 存储 - 加载顺序 | 同步访问规则 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| 顺序一致性 | 自动强制执行 | 加载值召回 | 按线程顺序全局执行 | 加载值召回,等待先前存储 | 所有同步访问按顺序执行 |
| TSO | 允许非 GP 值投机加载 | 无特殊规则 | 按线程顺序全局执行 | 允许非 GP 值投机加载 | 无特殊规则 |
| 弱排序 | 无特殊规则 | 无特殊规则 | 无特殊规则 | 无特殊规则 | 同步访问视为栅栏,所有先前访问全局执行后执行 |
| 释放一致性 | 无特殊规则 | 无特殊规则 | 无特殊规则 | 无特殊规则 | 获取同步未来访问,释放同步过去访问 |
7. 练习题解析
7.1 程序执行特性分析
对于不同的程序,需要分析其执行结果在不同内存一致性模型下的特性,以下是具体分析:
-
第一组程序
P1
P2
P3
A=1
R1=A
R2=B
B=1
R3=A
- **不连贯的执行**:若出现 R1 = 0 且 A 已被赋值为 1 的情况,说明数据在不同线程间的传播出现问题,不满足连贯性。
- **非顺序一致的执行**:若执行顺序导致结果不符合所有线程按顺序执行的预期,例如出现不符合逻辑的 R1、R2、R3 组合值,就不满足顺序一致性。
- **非 TSO 的执行**:当加载操作不遵循 TSO 中加载 - 存储顺序等规则时,如加载操作提前获取到未全局传播的值,就不满足 TSO。
- **非弱排序的执行**:若同步变量的访问顺序不符合弱排序模型中同步访问视为栅栏的规则,就不满足弱排序。
- 其他程序组 :按照同样的逻辑,分析不同程序在不同内存一致性模型下的执行结果特性。
7.2 简单屏障同步代码实现
在实现简单屏障同步代码时,涉及到多个方面的问题:
-
读取 BAR 无需临界区保护的原因
:因为 BAR 是单调递增的,每个线程读取 BAR 时,只要其值小于 N,就会继续等待,不会出现数据竞争问题,所以不需要用临界区保护。
-
使用 test 和 test&set 实现代码
:
# 假设 T&S R1, X 指令
# 每个线程的代码
Begin: T&S R1, Lock
BEQ R1, #0, Begin
LW R2, BAR
ADDI R2, R2, #1
SW R2, BAR
SW #0, Lock
Loop: LW R2, BAR
BLT R2, #N, Loop
- 使用 F&A 指令实现 :
# F&A R1, X 指令
F&A R1, BAR
Loop: LW R2, BAR
BLT R2, #N, Loop
-
代码死锁问题
:原屏障代码在结束时 BAR 值为 N,重置为 0 有风险。新代码中,若多个线程同时执行
if(BAR==N)BAR=0,可能会导致 BAR 值混乱,从而死锁。 - 交替增减屏障计数的实现 :
# BARRIER(BAR,N) 过程
# 假设 F&A R1, X 指令
# 二进制标志 FLAG 表示奇偶次执行
F&A R1, FLAG
ANDI R2, R1, #1
BEQ R2, #0, Increment
Decrement: F&A R1, BAR
ADDI R1, R1, # - 1
SW R1, BAR
Loop: LW R2, BAR
BEQ R2, #0, End
J Loop
Increment: F&A R1, BAR
ADDI R1, R1, #1
SW R1, BAR
Loop2: LW R2, BAR
BEQ R2, #N, End
J Loop2
End: RET
7.3 同步原语的实现
使用 LL(load - linked)和 SC(store conditional)指令实现不同的同步原语:
-
F&ADD X, Rx, a
LL Rx, X
ADDI Ry, Rx, a
SC Ry, X
BEQ Ry, #0, Begin
- F&ADD X, Rx, Ry
LL Rx, X
ADD Rz, Rx, Ry
SC Rz, X
BEQ Rz, #0, Begin
- F&ADD X, Rx, Y
LL Rx, X
LL Rz, Y
ADD Rw, Rx, Rz
SC Rw, X
BEQ Rw, #0, Begin
- SWAP(Rx,X)
LL Ry, X
SC Rx, X
BEQ Rx, #0, Begin
MOV Rx, Ry
- CAS Rx, Ry, X
LL Rz, X
BEQ Rz, Rx, Swap
J End
Swap: SC Ry, X
BEQ Ry, #0, Begin
End:
- 实现临界区原子操作 :可以使用 LL 和 SC 实现临界区的原子操作,例如:
LL R1, A
ADD R1, R1, R1
BEQ R1, #8, AddB
MOV R2, #0
SW R2, B
J End
AddB: LL R2, B
ADDI R2, R2, #1
SC R2, B
BEQ R2, #0, Begin
End:
7.4 多处理器系统中存储缓冲区的管理
对于不同的存储缓冲区管理策略,分析其在不同内存一致性模型下的特性:
| 策略 | 描述 | 内存一致性特性 |
| ---- | ---- | ---- |
| a | 加载可从存储缓冲区获取值,存储按 FIFO 顺序全局执行 | 可能满足连贯性、TSO 等,具体取决于情况 |
| b | 同一地址只允许一个存储在缓冲区,新存储替换旧存储 | 与 a 类似,但存储管理不同 |
| c | 加载从内存获取值,即使存储在缓冲区 | 可能影响连贯性和其他一致性模型 |
| d | 加载等待存储在内存执行后再获取值 | 增强了顺序性,可能更符合顺序一致性 |
| e | 加载等待存储缓冲区为空再执行 | 严格的顺序控制,更接近顺序一致性 |
| f | 存储缓冲区中的存储无序执行 | 对内存一致性模型影响较大,可能不满足多种模型 |
7.5 竞争协议的状态图推导
竞争协议是一种混合写 - 更新/无效化协议,其状态图推导如下 mermaid 流程图所示:
graph LR
classDef state fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
S0(共享状态 S0, UP - bit = 0):::state -->|PrRd| S0
S0 -->|PrWr| D(修改且唯一状态 D):::state
S0 -->|BusRd| S0
S0 -->|BusUpd| S1(共享状态 S1, UP - bit = 1):::state
S1 -->|PrRd| S0
S1 -->|PrWr| D
S1 -->|BusRd| S1
S1 -->|BusUpd| I(无效状态 I):::state
D -->|PrRd| D
D -->|PrWr| D
D -->|BusRd| S0
D -->|BusUpd| I
对于每个状态转换,需要根据输入(PrRd、PrWr、BusRd、BusUpd)执行相应的操作,类似于 MSI 协议。
7.6 缓存缺失对内存一致性模型的敏感性
使用 Jacobi 算法分析不同内存一致性模型和缓存协议下的缓存缺失情况:
-
顺序一致性,轮询访问
:对于 MSI 和 MESI 无效化协议,计算每个处理器在一次迭代中的一致性缺失次数;对于 MSI - 更新协议,计算更新次数;对于竞争协议,计算更新和无效化缺失次数。
-
顺序一致性,不同时间访问
:改变访问的全局交错顺序,重新计算上述各项指标。
-
释放一致性
:考虑大存储缓冲区,存储仅在需要时传播,计算不同协议下的更新/无效化缺失次数。
7.7 不同缓存协议的成本比较
对于给定的参考流
r1 w1 r1 w1 r2 w2 r2 w2 r3 w3 r3 w3 r2 w2 w2 r2
,比较四种缓存协议(MSI - 无效化、MSI - 更新、MESI、竞争协议)的成本:
| 协议 | 成本计算 | 原因分析 |
| ---- | ---- | ---- |
| MSI - 无效化 | 根据缓存命中、简单事务和缺失的成本模型计算 | 无效化操作可能导致更多的缓存缺失 |
| MSI - 更新 | 考虑更新操作的成本 | 更新操作在某些情况下可能更频繁 |
| MESI | 综合考虑各种状态转换的成本 | 更复杂的状态管理可能影响成本 |
| 竞争协议 | 结合更新和无效化的情况计算成本 | 特殊的更新/无效化策略影响成本 |
7.8 任务调度策略分析
对于 13 个任务在四个处理器上的调度,分析四种调度策略(静态、半静态、动态、最优)在不同任务执行时间情况下的总执行时间和加速比:
| 调度策略 | 任务执行时间情况 1(T(i) = 1 + i) | 任务执行时间情况 2(T(i) = 13 - i) |
| ---- | ---- | ---- |
| 静态 | 计算每个处理器分配的任务执行时间总和 | 同样计算总执行时间和加速比 |
| 半静态 | 考虑空闲处理器偷取任务的情况 | 分析不同任务时间下的调度效果 |
| 动态 | 从 FIFO 任务队列中获取任务执行 | 评估执行效率和加速比 |
| 最优 | 根据已知任务执行时间进行最优调度 | 理论上达到最小总执行时间 |
7.9 CAS 指令的应用与分析
使用 CAS 指令实现循环队列的入队和出队操作:
-
入队操作
Enqueue (TOP,BOTTOM,R1){
Begin: LW R2,TOP
LW R3,BOTTOM
SUBI R4,R3,#4
ANDI R4,R4,0xxFF
BEQ R2,R4,Begin
CAS R3,R4,BOTTOM
BNE R3,R4,Begin
SW R1,0(R3)
}
- 出队操作
Dequeue (TOP,BOTTOM,R1){
Begin
LW R2,TOP
LW R3,BOTTOM
BEQ R2,R3,Begin
SUBI R4,R2,#4
ANDI r4,r4,0xx0FF
CAS R2,R4,TOP
BNE R2,R4,Begin
LW R1,0(R2)
}
在弱排序系统和投机性 OoO 处理器中,分析不同场景下的操作情况:
-
一个生产者和两个消费者,队列为空
:消费者尝试出队会不断循环,直到生产者入队。在 MSI - 无效化和 MSI - 写 - 更新协议下,需要确保 CAS 指令的正确执行,避免数据不一致。
-
生产者入队后
:一个消费者出队,另一个继续等待。需要保证入队和出队操作的并发正确性,CAS 指令的投机性执行可以提高并发度,但需要处理好加载值召回等问题。
-
CAS 非投机性执行的影响
:会降低并发度,因为每次操作都需要等待更多的条件满足,但可以减少执行回滚的可能性,对性能有一定的影响。
通过对这些练习题的分析和解答,可以更深入地理解宽松内存一致性模型和内存顺序的投机性违规相关知识,掌握不同模型和协议在实际应用中的特点和影响。
超级会员免费看
1693

被折叠的 条评论
为什么被折叠?



