use*_*277 2 assembly multithreading memory-barriers
我有时会在关于内存排序的教程中看到术语"完全内存屏障",我认为这意味着以下内容:
如果我们有以下说明:
instruction 1
full_memory_barrier
instruction 2
Run Code Online (Sandbox Code Playgroud)
然后instruction 1不允许重新排序到下面full_memory_barrier,并且instruction 2不允许重新排序到上面full_memory_barrier.
但是完全内存屏障的反面是什么,我的意思是有什么像"半内存屏障"只能阻止CPU在一个方向上重新排序指令?
如果有这样的记忆障碍,我没有看到它的意思,我的意思是如果我们有以下指示:
instruction 1
memory_barrier_below_to_above
instruction 2
Run Code Online (Sandbox Code Playgroud)
假设这memory_barrier_below_to_above是一个阻止instruction 2重新排序到上面的内存屏障memory_barrier_below_to_above,因此不允许以下内容:
instruction 2
instruction 1
memory_barrier_below_to_above
Run Code Online (Sandbox Code Playgroud)
但是允许以下内容(这使得这种类型的内存屏障毫无意义):
memory_barrier_below_to_above
instruction 2
instruction 1
Run Code Online (Sandbox Code Playgroud)
http://preshing.com/20120710/memory-barriers-are-like-source-control-operations/解释了不同类型的障碍,如LoadLoad或StoreStore.StoreStore屏障仅阻止商店跨屏障重新排序,但负载仍然可以无序执行.
在真正的CPU上,任何包含StoreLoad的障碍都会阻塞其他所有障碍,因而被称为"完全障碍".StoreLoad是最昂贵的类型,因为它意味着在以后的加载可以从L1d缓存读取之前耗尽存储缓冲区.
障碍示例:
strong weak
x86 mfence none needed unless you're using NT stores
ARM dmb sy isb, dmb st, dmb ish, etc.
POWER hwsync lwsync, isync, ...
Run Code Online (Sandbox Code Playgroud)
ARM具有"内部"和"外部可共享域".我真的不知道这意味着什么,没有必要处理它,但是这个页面记录了可用的不同形式的数据存储器障碍. dmb st只等待早期的商店完成,所以我认为它只是一个StoreStore屏障,因此对于C++ 11发布商店来说太弱了,它还需要针对LoadStore重新排序订购早期的负载.另请参阅C/C++ 11与处理器的映射:请注意,seq-cst可以通过围绕每个商店的全屏障来实现,或者在加载之前以及在商店之前使用障碍来实现.不过,降低负荷通常是最好的.
ARM ISB刷新指令缓存.(ARM没有连贯的i-cache,因此在将代码写入内存之后,您需要一个ISB才能可靠地跳转并执行这些字节作为指令.)
POWER提供了多种障碍选择,包括Jeff Preshing在上面链接的文章中提到的轻量级(非全屏障)和重量级同步(全屏障).
单向屏障是您从发布商店或获取负载获得的.关键部分末端的释放存储(例如解锁自旋锁)必须确保临界区内的加载/存储不会在以后出现,但它不必延迟以后加载,直到lock=0全局变为全局可见.
Jeff Preshing也有一篇关于此的文章:获取和释放语义
"完整"与"部分"屏障术语通常不用于释放存储或获取负载的单向重新排序限制.实际的发布栏(在C++ 11中std::atomic_thread_fence(std::memory_order_release))确实阻止了两个方向上的商店重新排序,这与特定对象上的发布存储不同.
这种微妙的区别在过去引起了混乱(甚至在专家中间!).Jeff Preshing还有一篇很好的文章解释它:获取和释放栅栏不会按照你期望的方式工作.
你是对的,没有绑在商店或负载上的单向屏障不是很有用; 这就是为什么这样的东西不存在的原因.:P它可以在一个方向上重新排序无限制的距离,并让所有操作相互重新排序.
到底是atomic_thread_fence(memory_order_release)做什么的?
C11(n1570第7.17.4节)只在创建与获取加载或获取栅栏的同步关系方面定义它,当在原子存储(放松或其他)之前使用释放栅栏到同一对象时负载访问.(C++ 11的定义基本相同,但在评论中与@EOF的讨论提出了C11版本.)
这个定义在净效应方面,而不是实现它的机制,并没有直接告诉我们它的作用或不允许.例如,第3小节说
3)释放栅栏A与对原子对象M执行获取操作的原子操作B同步,如果存在原子操作X,使得A在X之前被排序,X修改M,并且B读取由X写入的值或者如果是释放操作,则假设释放序列X中的任何副作用所写的值将成为头部
所以在写作线程中,它正在谈论这样的代码:
stuff // including any non-atomic loads/stores
atomic_thread_fence(mo_release) // A
M=X // X
// threads that see load(M, acquire) == X also see stuff
Run Code Online (Sandbox Code Playgroud)
同步 - 意味着从M=X(直接或通过释放序列间接)看到值的线程也可以看到stuff没有Data Race UB的所有和读取的非原子变量.
这让我们可以说一下不允许的内容:
这是原子商店的双向障碍.它们无法在任何一个方向上穿过它,因此屏障在此线程的内存顺序中的位置受到前后原子存储的限制.任何早期的商店都可以是stuff某些商店的一部分M,任何后来的商店都可以是M获取 - 加载(或加载+获取 - 栅栏)同步的商店.
它是原子载荷的单向障碍:早期的需要留在障碍物之前,但后来的障碍物可以移动到障碍物之上. M=X只能是商店(或RMW的商店部分).
对于非原子加载/存储来说,这是一个单向障碍:非原子存储可以是其中的一部分stuff,但不能X因为它们不是原子的.可以允许此线程中的后续加载/存储在之前出现在其他线程中M=X.(如果在屏障之前和之后修改了非原子变量,那么即使在与此屏障同步之后也没有任何东西可以安全地读取它,除非还有一种方法让读者停止此线程继续并创建Data Race UB所以编译器可以而且应该重新排序foo=1; fence(release); foo=2;成foo=2; fence(release);,消除死foo=1商店.但下沉foo=1到之后的阻隔只在技术上来说,没有什么可以说没有UB的区别合法的.)
作为一个实现细节,C11版本栅栏可能比这更强(例如,用于更多类型的编译时重新排序的双向屏障),但不是更弱.在某些体系结构(如ARM)上,唯一足够强大的选项可能是完全屏障asm指令.对于编译时重新排序限制,编译器可能不允许这些单向重新排序只是为了保持实现简单.
大多数情况下,这种组合的双向/单向性质仅对编译时重新排序很重要.CPU不区分原子存储和非原子存储.非原子总是与放松原子的m指令相同(对于适合单个寄存器的对象).
使核心等到早期操作全局可见的CPU屏障指令通常是双向障碍; 它们在操作方面被指定,在所有内核共享的内存的连贯视图中变得全局可见,而不是创建与关系同步的C/C++ 11风格.(请注意,在某些其他线程对所有线程全局可见之前,操作可能会变得可见:对于不同线程中的不同位置的两个原子写入是否会被其他线程以相同的顺序看到?但是只有阻止重新排序的障碍?物理核心,顺序一致性可以恢复.)
C++ 11 release-fence需要LoadStore + StoreStore障碍,而不是LoadLoad.一个让你只获得2个但不是全部3个"廉价"障碍的CPU,可以让负载在屏障指令的一个方向上重新排序,同时阻止两个方向的存储.
弱排序的SPARC实际上是这样的,并使用LoadStore等术语(这是Jeff Preshing在他的文章中使用术语的地方). http://blog.forecode.com/2010/01/29/barriers-to-understanding-memory-barriers/显示了它们的使用方式.(更新的SPARC使用TSO(总存储顺序)内存模型.我认为这就像x86,其中硬件给出了程序顺序发生的内存操作错觉,除了StoreLoad重新排序.)