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

Hoisting of non-atomic loads up through acquiring atomic loads

本文关键字:获取      更新时间:2023-10-16

我的印象是,在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;
}

产品:

_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

这在我看来是正确的。

然而,将数据更改为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;
}

产生这个:

_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

那到底发生了什么?

我已经在gcc bugzilla上发布了这个消息,他们已经确认它是一个bug。

MEM别名集-1(alias_set_MEMORY_BARRIER(被认为是为了防止这种情况,但是PRE不知道这个特殊的属性(它应该"杀死"所有的裁判穿过它(。

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

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

为什么此代码仍然被破坏

根据本文,我的代码仍然不正确,因为它引入了数据竞赛。即使打了补丁的gcc生成了正确的代码,如果不将其封装在std::atomic中,那么访问data仍然是不合适的。原因是数据竞赛是未定义的行为,即使由此产生的计算被丢弃。

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;
}

在这里,编译器可能会优化到跳转表的切换,并且由于上面的if语句,可以避免范围检查。然而,如果数据竞赛不是未定义的行为,则需要进行第二次范围检查。

我认为您的atomic_thread_fence不正确。唯一能与代码一起使用的C++11内存模型是seq_cst。但这对于你所需要的来说是非常昂贵的(你会得到一个完整的内存围栏(。

原始代码是有效的,我认为这是最好的性能折衷。

根据您的更新进行编辑:

如果你正在寻找正则int代码不能按你想要的方式工作的正式原因,我相信你引用的论文(http://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf)给出了答案。看看第2节的末尾。您的代码与图1中的代码有相同的问题。它有数据竞赛。多个线程可以同时对正则int的同一内存执行操作。它被c++11内存模型禁止,此代码在形式上不是有效的c++代码。

gcc希望代码没有数据竞争,即是有效的C++代码。由于没有race,并且代码无条件地加载int,因此可以在函数体的任何位置发出加载。所以gcc是智能的,它只发出一次,因为它不易失。通常与获取障碍并行的条件语句在编译器的操作中起着重要作用

在标准的正式俚语中,原子负载和常规int负载是不排序的。例如,条件的引入将创建一个序列点,并迫使编译器在序列点之后计算正则int(http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx)。然后,c++内存模型将完成剩下的工作(即通过执行指令的cpu确保可见性(

所以你的说法都不是真的。你肯定可以用c++11构建一个锁,只是不能用数据竞赛的锁:-(通常情况下,锁需要在读取之前等待(这显然是你在这里试图避免的(,所以你不会遇到这种问题。

请注意,您原来的seqlock有缺陷,因为您不想只检查seq0!=seq1(您可能正在进行更新(。seqlock纸张具有正确的条件。

我对这些非顺序一致的内存顺序操作和障碍的推理还是个新手,但可能是这种代码生成是正确的(或者说是允许的(。从表面上看,这看起来确实很可疑,但如果一个符合标准的程序无法判断数据中的负载已被提升,我也不会感到惊讶(这意味着根据"假设"规则,该代码是正确的(。

该程序从原子中读取两个后续值,一个在加载之前,一个是在加载之后,并在它们不匹配时重新发出加载。原则上,的两个原子读取不需要看到彼此不同的值。即使刚刚发生了原子写入,这个线程也无法检测到它没有再次读取旧值。然后,线程将返回到循环中,并最终从原子中读取两个一致的值,然后返回,但由于seq0seq1随后被丢弃,因此程序无法判断seq0中的值与从data中读取的值不对应。现在,原则上,这也向我表明,整个循环本可以被消除,只有来自data的负载才是正确性所必需的,但未能消除循环并不一定是正确性问题。

如果reader()返回一个包含seq0(或seq1(的pair<int,unsigned>,并且生成了相同的提升循环,我认为这可能是不正确的代码(但我对这种非顺序一致的操作推理还是新手(。