这一部分的内容对应于 lec 35 的相关内容, (b 大上的参考课程为 [Summer 20])这一部分承接了前三讲中关于线程级并行的内容,是并行系列讲座的收官之作。它从前两讲的软件和编程模型,深入到了支撑这些模型的底层硬件机制,解决了并行计算中最根本的两个问题:如何实现正确的同步和如何维护多核数据一致性。
从整体思路而言, lec 35 的逻辑脉络是非常清晰的,它的主体包括两大部分:
- 硬件对同步的支持工作:包括原子操作、RISC – V 下的相关指令、无竞态条件锁的实现以及 OpenMP 中是如何通过这些硬件原语构建高级同步指令
- 缓存一致性:包括多核缓存面临的新问题,由此引发的性能陷阱以及解决相关问题所必需的相关协议
原子操作
在前一讲的内容中我们提到,在软件层面上实现原子操作是有缺陷的,因为软件在执行时不能保证当前操作不被中断。因而在检查和设置之间可能存在间隙,导致竞态。
相应的解决方案是由 CPU 提供原子指令,在一个不可中断且中立的总线周期内完成读改写操作。外部观察者只能看到操作前或操作后的状态,看不到中间态。
在 RISC – V 中,相应的指令为 AMO
amoop.w rd, rs2, (rs1)
该指令相当于原子地执行
tmp = Mem[rs1];
rd = tmp;
Mem[rs1] = tmp op rs2;
其中op
可以是 add
, and
, or
, swap
, xor
等命令符
那么无竞态锁的正确实现其实为:
li t0, 1 # 要写入锁的值 (1)
Try:
amoswap.w.aq t1, t0, (a0) # 原子交换: t1 = old_lock; lock = 1;
bnez t1, Try # 如果 old_lock != 0(锁已被占),循环重试
# ----------- 临界区开始 -----------
# ... 访问共享资源 ...
# ----------- 临界区结束 -----------
amoswap.w.rl x0, x0, (a0) # 原子写入: lock = 0; (x0是只读的0寄存器)
其中.aq
(acquire) 和 .rl
(release) 用于保证内存操作的顺序性,防止指令重排破坏同步逻辑。
实现的逻辑其实很简单,由 amoswap
指令原子地完成了“读取锁值”和“写入 1 ”两个操作,接着获取寄存器 t1 的值,如果获取失败(t1 != 0
),线程在循环中不断尝试(Try
标签处),直到成功。并在自己的临界区执行结束后 使用 amoswap
将锁值原子地写回 0 。
- 这种锁称为自旋锁(Spinlock)。它在等待时会持续消耗CPU周期,适用于临界区执行时间非常短的场景。对于长临界区,使用会让出CPU的锁(如互斥锁)效率更高,不过讲座没提。
缓存一致性
这是并行处理器下新面对的另一个问题。多核处理器每个核心都有私有缓存。如果核心0修改了其缓存中的变量X,核心1的缓存中可能还是旧的X值,这违反了共享内存的语义。
问题其实类似于 DBS 的并发,但解决方法与 DBS 不尽相同。我们在这设计了一种协议,使其对任一个内存地址的写操作对处理器均可见,且要求所有处理器看见的读写顺序是一致的,也就是所谓的缓存一致性协议
缓存一致性协议
在诸多一致性协议中,侦听协议是一种常见的实现方式。所有缓存都“侦听”连接它们和内存的总线(或互联网络)上的事务。当一个缓存要写数据时,它通过总线广播一个消息,其他缓存收到消息后采取行动(如作废其副本)来保证数据一致。
此外还有 MOESI ,这同样是一个景点的缓存行状态协议,每个缓存行会处于以下状态之一:
- M (Modified): 脏数据,只在当前缓存中,与内存不一致。可写。
- O (Owned): 脏数据,但其他缓存可能有只读副本(S状态)。当前缓存负责在需要时提供数据或写回内存。是MESI协议的优化扩展。
- E (Exclusive): 干净数据,只在本缓存中,与内存一致。可写(写入后变为M)。
- S (Shared): 干净数据,可能存在于多个缓存中,与内存一致。只读。
- I (Invalid): 数据无效(是旧副本)。
缓存通过侦听总线上的读写请求,并根据规则在上述状态间转换,来保证所有缓存看到的数据视图是一致的。
但缓存一致性同样可能引发潜在问题,我们考虑这样一个场景:
“ 两个无关的变量 X
和 Y
恰好位于同一个缓存行中。两个不同的核心分别频繁地读写 X
和 Y
”
这会发生什么?显然会出现频繁的缓存缺失,导致大量的不必要的一致性流量,严重损害性能。这也被称为“一致性缺失(Coherence Miss)”
当然解决方案也是存在的,我们可以通过编译器指令进行内存对齐,从而保证每个变量独占一个缓存行,或者填充冗余字节作为 Padding,将不同线程的字段分隔到不同缓存行。
写在最后
CS61C 中关于并行的部分至此告一段落,相应的 lab 10 的内容还是比较容易上手的,不过似乎没有考虑到架构的可移植性(对,说的就是你 x86 – msvc (╬☉д⊙)什么库都缺)写起来比较不顺手,但总之这一讲将硬件、体系结构和软件深刻地联系了起来,当我们在 OpenMP 里写下一句 #pragma omp critical
时,底层是成千上万个晶体管在通过复杂的协议和原子操作,为我们确保程序的正确性。
发表回复