【问题标题】:Is there any compiler barrier which is equal to asm("" ::: "memory") in C++11?在 C++11 中是否存在等于 asm("" ::: "memory") 的编译器障碍?
【发布时间】:2021-10-07 17:13:26
【问题描述】:

我的测试代码如下,发现只有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")”级别太低了。

还有更好的选择吗?

【问题讨论】:

  • 满足什么? atomic_thread_fence 在编译时和运行时会做任何必要的事情来停止重新排序。 atomic_signal_fence 仅在编译时停止重新排序,因此其他线程可以观察重新排序,但在该线程内异步运行的信号处理程序不会。 (因为乱序执行和内存重新排序总是保留单个线程的行为。)
  • 请澄清您的问题,但我认为答案是:atomic_signal_fence 是您正在寻找的编译器障碍。即使使用 mo_seq_cst(相当于 GNU C 中的 asm volatile("" ::: "memory");),它也不会编译为任何指令。这很令人困惑,因为您说“memory_order_acq_rel 根本没有产生任何编译器障碍!”,但是您没有显示任何检查方式的证据。当然,它不会编译为 x86 上的任何额外指令,因为 x86 是强排序的,并且在每次加载/存储之前/之后都有这样的免费屏障。

标签: c++ c++11 atomic lock-free memory-barriers


【解决方案1】:

回复:您的编辑:

但我不想使用原子变量。

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

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

请参阅 LWN 上的Who's afraid of a big bad optimizing compiler? 了解有关如果您只使用编译器障碍来强制重新加载非atomic 变量而不是使用带有 read-exactly- 的东西时可能遇到的错误的一些详细信息(如发明的加载)一次语义。 (在那篇文章中,他们谈论的是 Linux 内核代码,因此他们使用 volatile 进行手动加载/存储原子操作。但一般不要这样做:When to use volatile with multi threading? - 几乎从不)


够用吗?

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

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

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


...atomic_thread_fence(memory_order_acq_rel) 根本没有产生任何编译器障碍!

在几个方面完全错误。

atomic_thread_fence 一个编译器屏障加上任何运行时屏障都是必要的,以限制我们的加载/存储对其他线程可见的顺序重新排序。 p>

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

在像 ARM 这样的弱排序 ISA 上,thread_fence(mo_acq_rel) 不是免费的,并且会编译为指令。 gcc5.4 使用dmb ish。 (在Godbolt compiler explorer 上查看)。

编译器屏障只是防止在编译时重新排序,而不必阻止运行时重新排序。所以即使在 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,屏障没有任何作用,并且在 asm 输出中只存在对 A 的最终存储。

    # 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) 具有我们预期的屏障效应,但任何比 seq_cst 弱的atomic_thread_fence 根本不会充当编译器屏障。

这里的一个理论是 gcc 知道多个线程写入非atomic&lt;&gt; 变量是正式的未定义行为。这没有多少水,因为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 值是一种可能的排序,因此编译器可以决定这是总是发生的)。不过,当前的编译器通常不会进行这种优化。请参阅我在Can num++ be atomic for 'int num'? 上的回答末尾的讨论。

【讨论】:

  • 对此进行更新:atomic_thread_fence 不会停止对非atomic 对象的操作重新排序。在 x86 上使用 gcc,atomic_signal_fence 可以。我不确定这是标准要求还是实现工件。所以atomic_signal_fence 不是atomic_thread_fence 的严格子集。
  • 更新 2:IIRC,最后一条评论是一个 GCC 错误,现已修复。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-11-10
  • 1970-01-01
  • 2014-12-30
  • 2016-07-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多