在 C++11 中是否存在等于 asm("" ::: "memory") 的编译器障碍?

Is there any compiler barrier which is equal to asm("" ::: "memory") in C++11?

我的测试代码如下,发现只有memory_order_seq_cst禁止编译器重新排序

#include <atomic>

using namespace std;

int A, B = 1;

void func(void) {
    A = B + 1;
    atomic_thread_fence(memory_order_seq_cst);
    B = 0;
}

而其他选择如memory_order_releasememory_order_acq_rel根本没有产生任何编译障碍。

我认为他们必须像下面那样使用原子变量。

#include <atomic>

using namespace std;

atomic<int> A(0);
int B = 1;

void func(void) {
    A.store(B+1, memory_order_release);
    B = 0;
}

但是我不想使用原子变量。同时,我觉得"asm("":::"memory")"的水平太低了。

还有更好的选择吗?

回复:您的编辑:

But I do not want to use atomic variable.

为什么不呢?如果是出于性能原因,请将它们与 memory_order_relaxedatomic_signal_fence(mo_whatever) 一起使用以阻止编译器重新排序,而不会有任何 运行 时间开销,除了编译器障碍可能会阻止某些编译时优化,具体取决于周围环境代码。

如果是出于其他原因,那么 atomic_signal_fence 可能会为您提供恰好在您的目标平台上运行的代码。我怀疑它的大多数实现在实践中确实订购了非 atomic<> 加载和存储,至少作为一个实现细节,并且如果可以访问 atomic<> 变量,则可能是有效需要的。因此,在实践中可能有助于避免任何仍然存在的数据争用未定义行为的实际后果。 (例如,作为 SeqLock 实现的一部分,为了提高效率,您希望使用共享数据的非原子读/写,以便编译器可以使用 SIMD 向量副本,例如。)

参见 Who's afraid of a big bad optimizing compiler? on LWN for some details about the badness you can run into (like invented loads) if you only use compiler barriers to force reloads of non-atomic variables, instead of using something with read-exactly-once semantics. (In that article, they're talking about Linux kernel code so they're using volatile for hand-rolled load/store atomics. But in general don't do that: When to use volatile with multi threading? - 几乎没有)


够用什么?

不管有什么障碍,如果两个线程运行同时执行这个函数,你的程序就会因为并发访问非atomic<>变量而出现未定义行为。因此,此代码唯一有用的方法是,如果您正在谈论与 运行 在同一线程中的信号处理程序同步。

这也与要求“编译器屏障”一致,仅防止在编译时重新排序,因为乱序执行和内存重新排序始终保留单个线程的行为。所以你永远不需要额外的屏障指令来确保你在程序顺序中看到你自己的操作,你只需要停止编译器在编译时重新排序的东西。参见 Jeff Preshing 的 post:Memory Ordering at Compile Time

这就是 atomic_signal_fence 的作用。您可以将它与任何 std::memory_order 一起使用,就像 thread_fence 一样,以获得不同强度的屏障,并且仅阻止您需要阻止的优化。


... atomic_thread_fence(memory_order_acq_rel) did not generate any compiler barrier at all!

在几个方面完全错误。

atomic_thread_fence 编译器障碍任何运行-时间障碍是限制重新排序所必需的我们的 loads/stores 对其他线程可见的顺序。

我猜你的意思是当你查看 x86 的 asm 输出时它没有发出任何屏障 指令。像 x86 的 MFENCE 这样的指令不是“编译器障碍”,它们是 运行 时间的内存障碍,甚至可以防止 StoreLoad 在 运行 时间重新排序。 (这是 x86 允许的唯一重新排序。只有在使用弱排序 (NT) 存储时才需要 SFENCE 和 LFENCE,例如 MOVNTPS (_mm_stream_ps)。)

在像 ARM 这样的弱顺序 ISA 上,thread_fence(mo_acq_rel) 不是免费的,而是编译成一条指令。 gcc5.4 使用 dmb ish。 (见Godbolt compiler explorer)。

编译器屏障只是防止在编译时重新排序,不一定防止 运行-time 重新排序。因此,即使在 ARM 上,atomic_signal_fence(mo_seq_cst) 也不会编译成任何指令。

一个足够弱的屏障允许编译器在存储到 A 之前先存储到 B,但 gcc 碰巧决定仍然按源代码顺序执行它们,即使thread_fence(mo_acquire)(不应该与其他商店一起订购商店)。

所以这个例子并没有真正测试某些东西是否是编译器障碍。


来自 gcc 的奇怪编译器行为与编译器障碍不同的示例:

See this source+asm on Godbolt.

#include <atomic>
using namespace std;
int A,B;

void foo() {
  A = 0;
  atomic_thread_fence(memory_order_release);
  B = 1;
  //asm volatile(""::: "memory");
  //atomic_signal_fence(memory_order_release);
  atomic_thread_fence(memory_order_release);
  A = 2;
}

这会按照您期望的方式使用 clang 进行编译:thread_fence 是 StoreStore 屏障,因此 A=0 必须在 B=1 之前发生,并且不能与 A= 合并2.

    # clang3.9 -O3
    mov     dword ptr [rip + A], 0
    mov     dword ptr [rip + B], 1
    mov     dword ptr [rip + A], 2
    ret

但是对于 gcc,屏障没有作用,并且只有 A 的最终存储出现在 asm 输出中。

    # gcc6.2 -O3
    mov     DWORD PTR B[rip], 1
    mov     DWORD PTR A[rip], 2
    ret

但是对于 atomic_signal_fence(memory_order_release),gcc 的输出与 clang 匹配。 所以 atomic_signal_fence(mo_release) 具有我们预期的屏障效果,但是 atomic_thread_fence 任何弱于 seq_cst 的东西都不会充当编译器屏障。

这里的一个理论是 gcc 知道它是正式的多线程写入非 atomic<> 变量的未定义行为。这并没有多大意义,因为如果用于与信号处理程序同步,atomic_thread_fence 应该仍然有效,只是比必要的强。

顺便说一句,使用 atomic_thread_fence(memory_order_seq_cst),我们得到预期的

    # gcc6.2 -O3, with a mo_seq_cst barrier
    mov     DWORD PTR A[rip], 0
    mov     DWORD PTR B[rip], 1
    mfence
    mov     DWORD PTR A[rip], 2
    ret

即使只有一个障碍,我们也能做到这一点,这仍然允许 A=0 和 A=2 存储一个接一个地发生,因此允许编译器跨障碍合并它们。 (观察者看不到单独的 A=0 和 A=2 值是一种可能的顺序,因此编译器可以决定总是发生这种情况)。不过,当前的编译器通常不进行这种优化。请参阅我在 .

的回答末尾的讨论