“全内存障碍”的对立面是什么?

时间:2018-06-24 20:52:38

标签: multithreading assembly memory-barriers

我有时会在有关内存排序的教程中看到“完全内存屏障”一词,我认为这意味着:

如果我们有以下说明:

instruction 1
full_memory_barrier
instruction 2

然后不允许将instruction 1重新排序到full_memory_barrier以下,也不允许将instruction 2重新排序到full_memory_barrier以上。


但是,全内存屏障的反义词是什么,我的意思是说有像“半内存屏障”之类的东西只能阻止CPU在一个方向上对指令重新排序吗?

如果存在这样的内存障碍,我看不出它的意思,我的意思是如果我们有以下说明:

instruction 1
memory_barrier_below_to_above
instruction 2

假设memory_barrier_below_to_above是一个内存屏障,可防止将instruction 2重新排序到memory_barrier_below_to_above以上,因此将不允许以下操作:

instruction 2
instruction 1
memory_barrier_below_to_above

但是以下内容将被允许(这将使这种类型的内存屏障毫无意义):

memory_barrier_below_to_above
instruction 2
instruction 1

1 个答案:

答案 0 :(得分:6)

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, ...

ARM具有“内部”域和“外部可共享域”。我真的不知道这意味着什么,不必处理,但是this page介绍了可用的不同形式的数据内存屏障。 dmb st仅等待较早的存储完成,因此我认为这只是一个StoreStore障碍,因此对于C ++ 11发行存储来说太弱了,C ++ 11发行存储也需要对较早的加载进行排序以防止LoadStore重新排序。另请参见C/C++11 mappings to processors:请注意,可以在每个商店周围使用完全屏障,或者在加载之前和商店之前使用屏障来实现seq-cst。通常,使负载便宜通常是最好的。

ARM ISB刷新指令缓存。 (ARM没有相干的i缓存,因此在将代码写入内存后,需要一个ISB才能可靠地跳转到那里并将这些字节作为指令执行。)

POWER有很多可用的障碍,包括上面链接的Jeff Preshing文章中提到的轻型(非全障碍)和重型同步(全障碍)。


单向障碍是您从发行商店或获取负载获得的东西。关键部分末尾的发布存储(例如,解锁自旋锁)必须确保关键部分内部的加载/存储不会在以后出现,但不是必须延迟稍后加载,直到lock=0全局可见为止。

Jeff Preshing也有一篇有关此的文章:Acquire and Release semantics


“完全”与“部分”屏障术语通常不用于发布存储或获取负载的单向重新排序限制。与特定对象上的发布库不同,实际的发布 fence (在C ++ 11,std::atomic_thread_fence(std::memory_order_release)中) 阻止双向存储的重新排序。

这种微妙的区别在过去(甚至在专家之间)引起了混乱。 Jeff Preshing还有另一篇出色的文章对此进行了解释:Acquire and Release Fences Don't Work the Way You'd Expect

您是对的,与商店或货物无关的单向障碍不是很有用;这就是为什么这种事不存在的原因。 :P可以在一个方向上重新排序无限制的距离,并使所有操作彼此重新排序。


atomic_thread_fence(memory_order_release)到底做什么?

C11(n1570 Section 7.17.4 Fences)仅在与原子存储(松弛或其他方式)使用释放栅栏之前使用与获取负载或获取围栏创建同步关系时定义它。负载访问的对象相同。 (C ++ 11的定义基本相同,但是在注释中与@EOF的讨论提出了C11版本。)

这个关于净效果的定义,不是实现它的机制,不能直接 告诉我们它允许或不允许做的事情。例如,第3小节说

  

3)释放栅栏A与执行获取的原子操作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

同步方式表示,从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发行版围栏可能比此要强(例如,用于更多种编译时重新排序的2向障碍),但并不弱。在某些体系结构(如ARM)上,唯一足够强大的选择可能是完全障碍的asm指令。对于编译时重新排序的限制,编译器可能不允许这些1向重新排序只是为了使实现简单。

大多数情况下,这种组合的2向/ 1向性质仅对compile-time reordering 重要。 CPU不会区分原子存储和非原子存储。非原子的总是与宽松原子的asm指令相同(对于适合单个寄存器的对象)。

使核心等待直到全局可见的较早操作的

CPU屏障指令通常是2向屏障;它们是根据在所有内核共享的内存的一致视图中全局可见的操作而不是C / C ++ 11创建同步关系的样式来指定的。 (请注意,在所有线程全局对所有线程可见之前,操作可能会在一些其他线程中变为可见:Will two atomic writes to different locations in different threads always be seen in the same order by other threads?。  但是,只要在物理核心内避免重新排序的障碍,就可以恢复顺序一致性。)

C ++ 11发行版围栏需要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重新排序。)