CPU体系(1):内存模型 & CPU Cache一致性 (待整理)

2022-12-30,,,,

C++中的 volatile, atomic, memory barrier 应用场景对比

-- volatile memory barrier atomic
抑制编译器重排 Yes Yes Yes
抑制编译器优化 Yes No Yes
抑制 CPU 乱序 No Yes Yes
保证访问原子性 No No Yes

如果需要原子性的访问支持,只能选择 atomic;
如果仅仅只是需要保证内存访问不会被编译器优化掉,优先考虑 volatile;
如果需要保证 Memory Order,也优先考虑 atomic,只有当不需要保证原子性,而且很明确要在哪插入内存屏障时才考虑手动插入 Memory Barrier。

存储一致性 vs 缓存一致性

存储一致性(memory consistency),不要跟缓存一致性(cache coherence)混淆了。

缓存一致性协议解决的是对单个存储器地址的访问之间如何排序的问题,而对于不同地址的访问并不是缓存一致性协议所要考虑的问题。存储一致性问题在任何具有或不具有高速缓存的系统中都存在,虽然高速缓存的存在有可能进一步加剧存储一致性问题。

存储器模型(memory model)又称为存储一致性模型。用于定义系统中对存储器访问需要遵守的原则,只要软件和硬件都遵循该原则,就能保证多核程序能运行得到确切的结果。Memory model一致性问题来源于:编译期乱序、执行期乱序,以及Cache不同步。

个人理解:内存模型与CPU Cahce一致性:由于CPU core对store buffer/invalidate queue与对Cache的操作是异步的。对于写操作(release语义),需要等待该cpu core的 store buffer 全部写到Cache,并等待MESI操作同步完成;对于读操作(aquire语义),需要等待该清空该CPU Core的invalidate queue,以保证该CPU Core从内存中读取数据。ref

多CPU/Core MESI协议局限性

当一个CPU进行写入时,首先会给其它CPU发送Invalid消息,然后把当前写入的数据写入到Store Buffer中。然后异步在某个时刻真正的写入到Cache中。当前CPU核如果要读Cache中的数据,需要先扫描Store Buffer之后再读取Cache。但是此时其它CPU核是看不到当前核的Store Buffer中的数据的,要等到Store Buffer中的数据被刷到了Cache之后才会触发失效操作。而当一个CPU核收到Invalid消息时,会把消息写入自身的Invalidate Queue中,随后异步将其设为Invalid状态。和Store Buffer不同的是,当前CPU核心使用Cache时并不扫描Invalidate Queue部分,所以可能会有极短时间的脏读问题。MESI协议,可以保证缓存的一致性,但是无法保证实时性。所以我们需要通过内存屏障在执行到某些指令的时候强制刷新缓存来达到一致性。

STL定义的 memory models

value memory model description
memory_order_relaxed Relaxed

没有同步或顺序制约,仅对此操作要求原子性

memory_order_consume Consume

1. 对当前要读取的内存施加 release 语义(store),在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前;

2. 在这个原子变量上施加 release 语义的操作发生之后,consume 可以保证读到所有在 release 前发生的并且与这块内存有关的写入

memory_order_acquire Acquire

1. 向前保证,本线程中所有读写操作都不能重排到memory_order_acquire的load之前;

2. 其他线程中所有memory_order_release的写操作都对当前线程可见

memory_order_release Release

1. 向后保证,本线程中所有读写操作都不能重排到memory_order_acquire的store之后;

2. 本线程中的所有写都对其他对同一atomic变量带有 memory_order_acquire的线程可见;

3. 本线程中的所有写都对其他所有有依赖且consume该变量的线程可见

memory_order_acq_rel Acquire/Release

1. 是release+acquire的结合,前后都保证,本线程中所有读写操作既不能重排到memory_order_acquire的load之前也不能到之后;

2. 其他线程的memory_order_release写在本线程写之前都是可见的;

3. 本线程的的写对其他线程的memory_order_acquire读都是可见的

memory_order_seq_cst Sequentially consistent

1. 顺序一致性;

2. 如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义;

3. 同时会对所有使用此 memory order 的原子操作进行同步,所有线程看到的内存操作的顺序都是一样的,就像单个线程在执行所有线程的指令一样

常使用有三种情形:relaxed order,release/acquire,cst。std::memory_order, memory order再探。C++17开始consume被弃用,自动升级为acquire。

进一步阅读:

std::memory_order
Sequential Consistency,Cache-Coherence及Memory barrier
ARM系列 - - 存储模型(一)
处理器设计 -- 顺序与乱序
Memory Model: 从多处理器到高级语言
内存模型与c++中的memory order
CPU多级缓存
從硬體觀點了解 memory barrier 的實作和效果

ARM fence 指令

数据存储器隔离指令 DMB。指令保证:仅当所有在它前面的存储器访问操作都执行完毕后,才提交(commit)在它后面的存储器访问操作。
数据同步隔离指令 DSB。比DMB严格:仅当所有在它前面的存储器访问操作都执行完毕后,才执行在它后面的指令(亦即任何指令都要等待存储器访问操作——译者注)
指令同步隔离 ISB。最严格:它会清洗流水线,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令。

CPU指令乱序来自流水线中的发射(Issue)/执行(取指、解码、发射/执行、取数、写回)阶段,其他阶段都是顺序DMB、DSB 和 ISB指令的深度解读。

out-of-order的CPU以及有多个pipeline的CPU,都会产生指令执行乱序。更多资料:

A Journey Through the CPU Pipeline

C++11 的单例模式线程安全

class A {
public:
static A* instance() {
static A* ptr = new A();
return ptr;
}
}; int main(int argc, char *argv[]) {
auto ptr = A::instance();
return 0;
}

使用Compiler Explore查看,插入了一个guard variable标记是否初始化;

并且调用guard aquire/release 保证初始化的原子性。

参考

C++ 中的 volatile,atomic 及 memory barrier
atomic实现原理

CPU体系(1):内存模型 & CPU Cache一致性 (待整理)的相关教程结束。

《CPU体系(1):内存模型 & CPU Cache一致性 (待整理).doc》

下载本文的Word格式文档,以方便收藏与打印。