C++ memory order循序渐进(五)—— C++ memory order在编译器和cpu层面的具体实现


前面四篇文章主要介绍了c++ memory order的概念和使用,在实际的编码中,知道这些基本就够了,代码指定好了memory order剩下的事情编译器和cpu会给我们保证,但了解底层实现有助于加深理解,本篇文章就来介绍一下各种memory order在编译器和cpu层面的具体实现。前面说过,C++的memory order更多的是一个抽象概念,给程序员提供了这么一种手段,合理使用memory order能够实现不同的同步效果,而不同的cpu平台差异性比较大,指令集也不一样,要实现各种order的方法也不相同,这部分对上层程序员是透明的,但是对于编译器来说就不一样了,而且相同的memory_order有可能可以用不同的指令来实现,不同编译器乃至相同的编译器的不同版本都可能会有不同的实现,下面以x86-64和ARMv8两个典型平台为例,这也分别是最常见的两个strong memory model和weak memory model。

1. x86-64 C++ memory_order和指令对照

以下是x86-64平台对于各种memory order的c++代码所对应的典型的汇编指令,包括原子操作的memory order和单独的fence,

C++11 操作对应x86-64指令
Load RelaxedMOV (from memory)
Load ConsumeMOV (from memory)
Load AcquireMOV (from memory)
Load Seq_CstMOV (from memory)
Store RelaxedMOV (into memory)
Store ReleaseMOV (into memory)
Store Seq Cst(LOCK) XCHG // alternative: MOV (into memory),MFENCE
Consume Fenceignore
Acquire Fenceignore
Release Fenceignore
Acq_Rel Fenceignore
Seq_Cst FenceMFENCE

2. x86-64 C++ memory_order_seq_cst底层实现

根据上表可以看到,因为x86-64本身就不会进行storestore loadstore loadload重排,除了Seq_Cst相关的操作,均不需要添加额外的cpu指令,实际上,对于x86-64平台,实现Sequential Consistency可以有四种方式,如下:

  1. 纯LOAD 和STORE + MFENCE
  2. 纯LOAD 和LOCK XCHG
  3. MFENCE + LOAD 和纯STORE
  4. LOCK XADD ( 0 ) 和纯STORE

XCHG和XADD和MFENCE效果类似,这里不单独讨论了,只看使用MFENCE的方法的话其实就是以下两种:

  1. LOAD不增加额外指令,STORE后面加MFENCE指令。
  2. STORE不增加额外指令,LOAD前面加MFENCE指令。
    由于MFENCE开销大,而且一般的程序LOAD比STORE多,因此像GCC等编译器一般都是采用的前者。

首先我们来看看MFENCE指令的作用,在intel手册里是这么描述MFENCE的:

Performs a serializing operation on all load-from-memory and store-to-memory instructions that were issued prior the MFENCE instruction. This serializing operation guarantees that every load and store instruction that precedes the MFENCE instruction in program order becomes globally visible before any load or store instruction that follows the MFENCE instruction.

关键点就在于,MFENCE前的所有读写指令将会在MFENCE后的读写指令执行之前全局可见,这实际上也就是full memory fence,阻止了包括storeload在内的所有重排,当然对于x86本身默认就不会进行其他三种重排,因此这里也就是额外扮演了一个store load 屏障的角色。单纯从store load 屏障的角度来看也就是阻止store load 重排,但是在实际实现上,store load 屏障往往还会有附加的作用,比如上述的x86的mfence就能保证前面指令的全局可见性。通常,StoreLoad屏障会保证屏障前的所有store对其他cpu核心可见屏障后的所有load能读到截止到屏障为止的最新的可见值

那么MFENCE主要进行了什么操作呢?了解过MESI和缓存一致性的相关知识的话就知道,现在的多核cpu,为了性能会向其他cpu核心发出Invalidate消息后,在没有收到ack的情况下先把值写入store buffer随后收到ack后再写入L1 cache全局生效。而MFENCE指令会清空store buffer,也就是让前面的store全局生效后才往下走,而怎么样才算全局生效呢?就是收到了所有的ack,意味着其他所有核心都知道要来拿这个新值了。

假设有以下这么个需要顺序一致的场景:
有线程A和线程B,以及两个变量a和b,初值均为0,A执行a.store(1),B执行b.store(1),那么如果后续线程有读取操作,如果没有MFENCE指令,由于storebuffer和store forwarding技术的存在,当前线程的写入对于当前线程立即生效,而对于其他线程有延迟,那么完全可能A的读取看到的变化顺序是a变为1再b变为1,而B的读取看到的是b变为1再a变为1,从而违背了顺序一致。

所以MFENCE加入的的核心原因就是需要立即排空storebuffer,解决的是store forwarding带来的顺序不一致问题,本质上就是要阻止写线程本身提前获得别的线程还取不到的值,而一旦堵上了这个漏洞,Invalidate消息的顺序处理保证了各个线程观察到各内存修改的顺序一致性。那么很自然的,无论是在store后加MFENCE还是在LOAD前加MFENCE,都能达到效果,我们来具体分析下两种方案:

  1. 纯LOAD 和STORE + MFENCE
    每次写内存后都会调用MFENCE,MFENCE会排空store buffer,也就是等待全局可见后再继续往下走,过后包括当前核心在内的所有的核心自然都能读到这个最新的值,只进行普通的LOAD即可。
  2. MFENCE + LOAD 和纯STORE
    每次读之前都调用一次MFENCE,注意,这个方案里,某个核纯store的写入并不能保证立即可见,写入之后其它核即便是通过MFENCE + LOAD的方式读取仍然可能读到旧值,因为MFENCE是针对当前核的限制,对于纯store写入的值首先进到storebuffer里,如果当前核自己后续没有seq读,其实是等待自然全局生效,全局生效了其余的核都能读到,但是自己如果要读,因为会执行MFENCE,也就使得自己读之前仍然会等待全局生效。

所以无论哪种方案,都能确保这些使用了memory_order_seq_cst的操作读的都是全局生效的值,排除了store forwarding的影响,从而达到顺序一致性。

注意full memory fence的开销较大,因此在实际使用中,只有确实必须要Sequential Consistency的时候才使用memory_order_seq_cst。

3. ARMv8(AArch64部分) C++ memory_order和指令对照

C++ 操作对应AArch64 指令
Load RelaxedLDR
Load ConsumeLDR + preserve dependencies until next kill_dependency OR LDAR
Load AcquireLDAR
Load Seq CstLDAR
Store RelaxedSTR
Store ReleaseSTLR
Store Seq CstSTLR
Cmpxchng Relaxed_loop: ldxr roldval, [rptr]; cmp roldval, rold; b.ne _exit; stxr rres, rnewval, [rptr]; cbnz rres, _loop; _exit
Cmpxchng Acquire_loop: ldaxr roldval, [rptr]; cmp roldval, rold; beq _exit; stxr rres, rnewval, [rptr]; cbnz rres, _loop; _exit
Cmpxchng Release_loop: ldxr roldval, [rptr]; cmp roldval, rold; b.ne _exit; stlxr rres, rnewval, [rptr]; cbnz rres, _loop; _exit
Cmpxchng AcqRel_loop: ldaxr roldval, [rptr]; cmp roldval, rold; b.ne _exit; stlxr rres, rnewval, [rptr]; cbnz rres, _loop; _exit
Cmpxchng SeqCst_loop: ldaxr roldval, [rptr]; cmp roldval, rold; b.ne _exit; stlxr rres, rnewval, [rptr]; cbnz rres, _loop; _exit
Acquire FenceDMB ISH LD
Release FenceDMB ISH
AcqRel FenceDMB ISH
SeqCst FenceDMB ISH

可以看到,因为ARM架构是weak memory model,本身就允许各类重排,因此 Acquire、Release等用的指令和relaxed不相同,需要进行额外的限制,整体原理和x86类似,这里不再赘述,有兴趣的可以查阅cpu手册了解各指令的功能。

3. 总结

c++ memory order属于软件层面的抽象,在实际实现上需要根据不同平台的实际情况插入合适的cpu指令来保证,总的来说就是编译器翻译成不同平台的cpu指令,而cpu在碰到这些指令时会执行附加操作保证内存序,在strong memory model的平台因为本身就有相对强的顺序保证,总体来说需要增加的指令较少,而weak memory order的平台则需要更多的指令来进行限制。

参考:
https://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
https://preshing.com
https://stackoverflow.com/questions/48316830/why-does-this-stdatomic-thread-fence-work
C++ Concurrency in Action 2nd

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值