CPU实现原子操作的原理

586之前的CPU, 会通过LOCK锁总线的形式来实现原子操作. 686开始则提供了存储一致性(Cache coherence),  这是多处理的基础, 也是原子操作的基础.

 

1. 存储的粒度

存储的组织形式(粒度)是以CacheLine为单位的, 通常为64字节甚至更高(早期也有32字节的). 然后几组CacheLine组成一个小的LRU(或者其他替换规则).

 

2. 协议

存储一致性(CC)一般是通过MESI协议, 以及后续的变种协议, 例如Intel的MESIF协议和AMD的MOESI协议, 来实现的. 

以MESI协议为例:

Modified: 独占CacheLine, 已经修改了, 但是还未同步到主存

Exclusive: 独占, 并且和主存一致

Shared : 共享的, 其他core也拥有该CacheLine, 并且与主存中一致

Invalid: 表示该CacheLine不可用

 

3. 通讯

有了协议, 那么就需要通讯来实现协议(存储的状态). 通讯有两种, 一种是广播/侦听, 一种是目录式.

广播/侦听顾名思义就是存储状态的变更, 会被广播到其他core上面去, 进而去维护CacheLine的状态. 很明显这种方式会浪费大量的流量, 而且难以扩展, CPU核数多了, 总线就是明显的瓶颈.

目录式, 就是把改变通知给具体的core, 从而避免广播. 但是考虑一种极端情况, 如果很多个core都在访问同一个CacheLine, 那还是不能避免(事实上的)广播. 所以, 多线程编程时, 共享同一个CacheLine不是一个好的选择.

 

 有了上面的东西, 现在我们来考虑原子操作的实现:

 

1. 原子的Load/Store

由于CPU对缓存的管理是以CacheLine为单位的, 所以在一个CacheLine内load/store实际上都是原子的. Load和Store一个8字节对象, 不可能高4位和低4位是分开操作的(从而搞成俩值). 

但是光有这个实际上还不够, CPU对CacheLine的修改不是立即写到主存里面去, 所以其他Core看到的值就有可能是老的值, 所以这时候还需要fence来读到最新的值; 至于写, 那一定需要写权限, 即M或者E状态, 而这两个权限里面都有最新的值(只是你刚才读到的不一定是最新的, 所以有可能用老值覆盖了新值).

2. FetchAndAdd

这是比load和store稍微复杂的操作, 实际上是一个复合操作. 但是有了M和E状态, 就很好理解了:

lock(CacheLine)
v := load(obj)
v += add
store(obj, v)
release(CacheLine)

x86里面是xadd指令.

3. CompareAndSwap

那么CAS, 也就可以猜出来:

lock(CacheLine)
v := load(obj)
if v != expected {
  store(obj, new_value)
}
release(CacheLine)

x86里面是xchg

这里说的lock和release均表示对该CacheLine独占和解出独占的意思.

 

关于原子操作的原理, 鲜有资料表表示其具体怎么做的, 很有可能是过于偏向于硬件. 但是对MESI等协议的思考, 实际上还是能猜到CPU内部的实现(至少七八不离十).  好在找到两个资料, 一个是<<并行多核体系结构基础>>和<<从鲲鹏920了解现代服务器实现和引用>>. 其中鲲鹏920内存模型章节这么写到:

原子指令在软件上看来逻辑并不复杂,但在微架构上看,成本是很高的。如果我们把CPU 和内存都看做是总线上的一个个独立的实体,有一个CPU要做CAS指令,这个CPU需要先从 内存中读一个值,同时要在内存控制器上设置一个标志,保证其他CPU写不进去,等它比 较完了,然后再决定写一个值回去,才会让其他CPU写入。

不同微架构实现有不同方法对行为进行优化,在鲲鹏920上,原子指令的请求需要在 L3Cache上进行排队,保证在原子操作的多个动作之间能维持原子指令要求的语义。这个 排队本身也有成本。所以没有原子需要就不要轻易用原子变量,这其实是有成本的。

 并行多核体系结构这么写到:

幸运的是, 缓存一致性协议提供了原子性被保障的基础. 举例来说, 当遇到一个原子指令时, 这个协议知道需要保证原子性. 他首先获得对存储单元M的"独家所有权" (通过将其他包含M的缓存块中的拷贝都置为无效). 当获得独家所有权之后, 这个协议会确保只有一个处理器能够访问这个块, 而如果其他处理器在此时想要访问的话就会经历缓存缺失, 接下来原子指令就可以执行. 在原子指令持续期间, 其他处理器不允许"偷走"这个块. 距离来说, 如通另一个处理器要求读或者写这个块, 这个块就被"偷"了(如块被清理, 块的状态被降级为无效). 在原子指令完成之前暴露块会破坏指令的原子性, ......

参考:

1) 并行多核体系结构基础

2) 从鲲鹏920了解现代服务器实现和应用

/**********************************************************************
 * 机械教条主义
 *
 * From:          http://www.cnblogs.com/egmkang/
 * Email:          egmkang [at] outlook.com
 * Weibo:        http://weibo.com/egmkang
 * Github:       http://github.com/egmkang
 *
 **********************************************************************/

posted @ 2020-12-04 10:10  egmkang  阅读(1206)  评论(0编辑  收藏  举报