"full memory barrier" 的反义词是什么?

What is the opposite of a "full memory barrier"?

我有时会在有关内存排序的教程中看到术语 "full memory barrier",我认为它的含义如下:

如果我们有如下指令:

instruction 1
full_memory_barrier
instruction 2

instruction 1不允许重排到full_memory_barrier以下,instruction 2不允许重排到full_memory_barrier以上。


但是完全内存屏障的反面是什么,我的意思是有没有像 "semi 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

http://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ 解释了不同类型的障碍,例如 LoadLoad 或 StoreStore。 StoreStore barrier 只能防止 stores 跨过 barrier 重新排序,但加载仍然可以乱序执行。

在真实的 CPU 上,任何包含 StoreLoad 的屏障也会阻止其他所有内容,因此称为 "full barriers"。 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 有 "inner" 和 "outer shareable domains"。我真的不知道那是什么意思,没必要处理它,但是 this page documents the different forms of Data Memory Barrier available. dmb st only waits for earlier stores to complete, so I think it's only a StoreStore barrier, and thus too weak for a C++11 release-store which also needs to order earlier loads against LoadStore reordering. See also C/C++11 mappings to processors:请注意,seq-cst 可以通过每个商店周围的全障碍或加载前的障碍以及在商店之前。不过,让负载便宜通常是最好的。

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

POWER 有大量可供选择的障碍,包括上面链接的 Jeff Preshing 文章中提到的轻量级(非完全障碍)和重量级同步(完全障碍)。


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

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


"full" 与 "partial" 屏障术语通常不用于发布存储或获取加载的单向重新排序限制。实际版本 fence(在 C++11 中,std::atomic_thread_fence(std::memory_order_release)does 阻止存储在两个方向上的重新排序,这与 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 release fence A synchronizes with an atomic operation B that performs an acquire operation on an atomic object M if there exists an atomic operation X such that A is sequenced before X, X modifies M, and B reads the value written by X or a value written by any side effect in the hypothetical release sequence X would head if it were a release operation

所以在写作线程中,它正在谈论这样的代码:

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 并读取非原子变量而没有数据竞争UB.

这让我们可以说明什么是/不允许的:

这是原子存储的双向屏障。它们不能在任何一个方向上穿过它,所以屏障在这个线程的内存顺序中的位置受前后原子存储的限制。对于某些 M,任何较早的存储都可以是 stuff 的一部分,任何较晚的存储都可以是获取负载(或负载 + 获取围栏)与之同步的 M

这是原子负载的单向屏障:前面的需要留在屏障之前,但后面的可以移动到屏障上方。 M=X 只能是商店(或 RMW 的商店部分)。

It's a one-way barrier for non-atomic loads/stores: 非原子存储可以是 stuff 的一部分,但不能是 X 因为它们不是原子的。可以允许此线程中的稍后加载/存储在 M=X 之前显示给其他线程。 (如果非原子变量在屏障之前和之后被修改,那么即使在同步之后也没有任何东西可以安全地读取它 - 与这个屏障,除非还有一种方法可以让 reader 停止这个线程继续和创建 Data Race UB。因此编译器可以而且应该将 foo=1; fence(release); foo=2; 重新排序为 foo=2; fence(release);,消除死的 foo=1 存储。但是将 foo=1 下沉到屏障之后仅在技术上没有 UB 什么也说不出来。)

作为一个实现细节,C11 发布围栏可能比这个 更强(例如,用于更多类型的编译时重新排序的双向屏障),但并不弱。在某些架构(如 ARM)上,唯一足够强大的选项可能是完全屏障 asm 指令。对于编译时重新排序限制,编译器可能不允许这些单向重新排序只是为了保持实现简单。

大多数情况下,这种组合的 2-way / 1-way 性质只对 compile-time reordering 有意义。 CPUs 不区分原子存储与非原子存储。非原子总是与松散原子相同的 asm 指令(对于适合单个寄存器的对象)。

CPU 使核心等待直到较早的操作全局可见的屏障指令通常是双向屏障;它们是根据在所有内核共享的一致内存视图中变得全局可见的操作来指定的,而不是创建同步关系的 C/C++11 样式。 (请注意,在操作变得全局对所有线程可见之前,它们可能对一些其他线程可见:。 但是只要在物理内核中设置防止重新排序的障碍,就可以恢复顺序一致性。)

C++11 release-fence 需要 LoadStore + StoreStore 屏障,但不需要 LoadLoad。 CPU 让您只获得 "cheap" 障碍中的那 2 个而不是所有 3 个障碍,这将使加载在一个方向上跨障碍指令重新排序,同时在两个方向上阻塞存储。

弱序SPARC其实就是这样的,并且使用了LoadStore等术语(Jeff Preshing在他的文章中使用了这些术语)。 http://blog.forecode.com/2010/01/29/barriers-to-understanding-memory-barriers/ 展示了它们的使用方式。 (最近的 SPARC 使用 TSO(总存储顺序)内存模型。我认为这就像 x86,其中硬件给人一种内存操作按程序顺序发生的错觉,StoreLoad 重新排序除外。)