是否有任何编译器屏障等于 C++11 中的 asm( " " ::: "memory" )?



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

有更好的选择吗?

re: your edit:

但是我不想使用原子变量。

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

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

看谁害怕一个大坏的优化编译器?如果您只使用编译器屏障来强制重新加载非atomic变量,而不是使用具有只读一次语义的东西,则可以在LWN上了解有关可能遇到的问题(例如虚构的加载)的一些详细信息。(在那篇文章中,他们讨论的是Linux内核代码,所以他们使用volatile进行手动加载/存储原子。但一般不要这样做:何时使用多线程volatile ?-几乎没有)


足够做什么?

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

这也将与要求"编译器障碍"一致,仅在编译时防止重排序,因为乱序执行和内存重排序总是保留单个线程的行为。所以你不需要额外的屏障指令来确保你的操作是按程序顺序进行的,你只需要阻止编译器在编译时重新排序。参见Jeff Preshing的文章:编译时的内存排序

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


atomic_thread_fence(memory_order_acq_rel)根本没有生成任何编译器屏障!

在很多方面都是完全错误的。

atomic_thread_fence编译器屏障加上任何必要的运行时屏障来限制重新排序,以便我们的load/store对其他线程可见。

我猜你的意思是当你查看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编译器资源管理器中查看)。

编译器屏障只是防止在编译时重新排序,而不一定阻止运行时重新排序。因此,即使在ARM上,atomic_signal_fence(mo_seq_cst)编译成没有指令。

一个足够弱的屏障允许编译器在存储到A之前存储到B,但是即使使用thread_fence(mo_acquire)(它不应该与其他store排序),gcc仍然决定按照源代码顺序执行它们。

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


与编译器屏障不同的gcc编译器的奇怪行为:

请参阅Godbolt的源代码+asm。

#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)具有我们期望的屏障效应,但是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值是一种可能的顺序,所以编译器可以决定这是经常发生的事情)。但是,当前的编译器通常不会进行这种优化。

<#39;int num'?

最新更新