基于《计算机体系结构:量化研究方法》第5章
线程级并行(TLP)需要多处理器,以及软件或操作系统提供的并行。
并行处理的挑战
- 程序本身并行有限,无法充分利用多处理器
- 多处理器通信成本较高
架构
按照多处理器访问存储器的方式分成两种架构
-
SMP (Symmetric multiprocessing,对称多处理器),也叫UMA(一致存储器访问),所有处理器访问存储器的延时是一样的。
-
DSM(Distributed shared memory,分布式共享存储器),也叫NUMA(非一致存储器访问),处理器访问远程存储器需要经过拥有该存储器的处理器,访问本地存储器和远程存储器的延迟不一样。
SMP和DSM中,多处理器之间的通信是通过共享地址空间完成的。
缓存一致性
每个处理器都会使用独立缓存(如L1 cache),当多处理器共享数据时,一份数据可能在每个处理器的独立缓存中都存在副本,这引入了缓存一致性问题。
如果存储器系统满足一下条件,则认为它是一致的
- 一个处理器读取的数据总是其之前写入的数据
- 一个处理器在某个位置写入数据X后,在一段时间后,另一处理器在该位置读取的数据就是X。这两者之间没有其他在该位置的写入操作
- 对同一位置的写入操作串行化:对所有处理器来说,任意两个处理器对同一位置的两次写入操作是相同的顺序。
缓存一致性的实现方式
- 目录式
- 监听式
监听一致性协议
所有处理器监听总线事件,当特定事件发生时,修改缓存块的状态。当对更新某块缓存后,其他处理器的缓存可以置为失效(写失效),也可以更新为最新的值(写更新)。因为写更新占用更多的总线带宽,所以一般都采用写失效方式。
以下都是写失效协议
- MSI:Modified,Share,Invalid
- MESI:基于MSI增加Exclusive状态,读写独占Exclusive状态的缓存块,可以不发送广播
- MOESI:基于MESI增加Owned状态,Owned状态表示缓存已过期
一个缓存块只有一个有效位,当更新一个字节时,会导致整个块失效。如果两个处理器交替更新同一缓存块中的完全无关的两个位置的数据,会不断触发总线广播以维持缓存块一致,进而导致处理性能下降,这就是伪共享问题。
监听式的缺点就是总线带宽有限,当处理器数量增多时,总线带宽就无法满足了。
目录一致性协议
目录一致性协议是监听一致性协议的替代方案,两者对缓存状态的定义都是差不多的。目录就是保存缓存状态的一个结构,处理器通过这个目录查询或修改缓存的状态。
同步
缓存一致性是针对缓存块的,且其保证一致性的操作并非是原子操作,对于真正需要保证多核一致的,还需要使用同步原语。应用层的多线程同步机制的底层都是基于这些硬件同步原语来实现的,包括如锁和屏障之类的。
使用一致性实现自旋锁(Spin lock),自旋锁的使用场景
- 程序员希望短暂持有该锁
- 程序员希望在锁可用时,锁定的延迟较低。因为未释放CPU使用权,所以一旦锁可用,其锁定延迟较低。
存储器连贯性
通过缓存一致性协议保证了处理器最终都能看到其他处理器的修改,但是何时能看到其他处理器的修改也是一个问题。
书里的例子,A和B的初值都是0
P1执行如下代码
A=1
if( B == 0 ){ ... }
P2执行如下代码
B=1
if( A == 0 ){ ... }
P1和P2同时执行各自的代码,这段代码的执行结果其实是不确定的
- 当P1在通知其他处理器
A=1
的更新时,P2已经执行到if( A==0 )
,此时P2看到的A仍等于0,于是进入if
分支。 - 当P1的
A=1
已经通知到P2,那么就不会进入if
分支。
为了保证执行结果是确定的,就需要保证对于一个存储器位置的读写是具有连贯性的。最简单的连贯性模型就是顺序连贯性。对于顺序连贯性,我的理解是当P1执行A=1
时,P2不能执行A==0
,P2必须等到P1执行完A=1
,执行完指的是处理器之间的对A这块缓存达到一致。这看起来就像是顺序执行了A=1; if( A == 0 )
,程序结果是确定的。
因为P2需要等待P1对A的变更结束,所以顺序连贯性必然会影响性能,这就引出了宽松连贯性。
这一节的内容,让我想到的是C++的memory order。编程中明确指定memory order的目的是规定了编译器的优化范围,避免编译器优化后程序不符合预期语义。
结语
这一章的部分还需要继续深入,结合C++做一些实验,比如伪共享、连贯性