通过获取原子载荷来提升非原子载荷

jle*_*ahy 13 c++ gcc atomic c++11

我的印象是内存加载无法在C++ 11内存模型中的获取负载之上提升.然而,看看gcc 4.8产生的代码,对于其他原子载荷而言似乎只是真实的,而不是所有的内存.如果这是真的并且获取负载不同步所有内存(只是std::atomics)那么我不确定如何用std :: atomic实现通用互斥锁.

以下代码:

extern std::atomic<unsigned> seq;
extern std::atomic<int> data;

int reader() {
    int data_copy;
    unsigned seq0;
    unsigned seq1;
    do {
        seq0 = seq.load(std::memory_order_acquire);
        data_copy = data.load(std::memory_order_relaxed);
        std::atomic_thread_fence(std::memory_order_acquire);
        seq1 = seq.load(std::memory_order_relaxed);
    } while (seq0 != seq1);
    return data_copy;
}
Run Code Online (Sandbox Code Playgroud)

生产:

_Z6readerv:
.L3:
    mov ecx, DWORD PTR seq[rip]
    mov eax, DWORD PTR data[rip]
    mov edx, DWORD PTR seq[rip]
    cmp ecx, edx
    jne .L3
    rep ret
Run Code Online (Sandbox Code Playgroud)

这对我来说是正确的.

但是,将数据更改为int而不是std::atomic:

extern std::atomic<unsigned> seq;
extern int data;

int reader() {
    int data_copy;
    unsigned seq0;
    unsigned seq1;
    do {
        seq0 = seq.load(std::memory_order_acquire);
        data_copy = data;
        std::atomic_thread_fence(std::memory_order_acquire);
        seq1 = seq.load(std::memory_order_relaxed);
    } while (seq0 != seq1);
    return data_copy;
}
Run Code Online (Sandbox Code Playgroud)

产生这个:

_Z6readerv:
    mov eax, DWORD PTR data[rip]
.L3:
    mov ecx, DWORD PTR seq[rip]
    mov edx, DWORD PTR seq[rip]
    cmp ecx, edx
    jne .L3
    rep ret
Run Code Online (Sandbox Code Playgroud)

发生什么了?

jle*_*ahy 4

为什么负载被提升到获取上方

我已将其发布在gcc bugzilla上,他们已确认它是一个错误。

MEM 别名集 -1 (ALIAS_SET_MEMORY_BARRIER) 应该可以防止这种情况,但 PRE 不知道这个特殊属性(它应该“杀死”所有穿过它的引用)。

看起来gcc wiki有一个关于此的很好的页面。

一般来说,释放是下沉代码的障碍,获取是提升代码的障碍。

为什么这段代码仍然被破坏

根据本文,我的代码仍然不正确,因为它引入了数据竞争。即使修补后的 gcc 生成了正确的代码,但data如果不将其包装起来,仍然无法正确访问std::atomic. 原因是数据竞争是未定义的行为,即使由此产生的计算被丢弃。

AdamH.Peterson 提供的示例:

int foo(unsigned x) {
    if (x < 10) {
        /* some calculations that spill all the 
           registers so x has to be reloaded below */
        switch (x) {
        case 0:
            return 5;
        case 1:
            return 10;
        // ...
        case 9:
            return 43;
        }
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这里,编译器可能会将开关优化为跳转表,并且由于上面的 if 语句将能够避免范围检查。但是,如果数据争用不是未定义的行为,则需要进行第二次范围检查。

  • 2者并非不兼容。您的代码存在数据竞争,并且 C++ 标准明确表示 (1.10 21) 您的代码依赖于未定义的行为。该代码不正确(或者至少缺少正确的同步来证明您的观点)。hp论文也再次明确了这一点(作者是c++11内存模型的架构师之一)1.10 13说不允许gcc进行代码提升。如果这种情况发生在有效的 C++ 代码上,则这是一个错误。重点是,*如果*没有数据竞争,生成的代码将是正确的(至少我不明白为什么不) (4认同)
  • @GuillaumeMorin:我很想同意。由存储和加载形成的发布序列只是发生之前链中的一个组成部分。如果第一个线程说了类似“if (seq_copy == 2) { data_copy = data; }`。在这种情况下,“data = 2”*发生在*原子存储之前,原子存储*与*原子加载*发生在“data_copy = data”之前。使用发布的代码,对“数据”的访问会导致竞争。(更正后的代码也为我生成正确的输出。) (4认同)