13

我的印象是内存负载无法提升到 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

发生什么了?

4

3 回答 3

4

为什么负载被提升到收购上方

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

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

看起来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;
}

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

于 2013-05-29T20:25:59.110 回答
1

我认为您的 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++ 代码。由于没有竞争并且代码无条件地加载 int,因此可以在函数体的任何位置发出负载。所以 gcc 很聪明,它只发出一次,因为它不是易失的。通常与获取屏障齐头并进的条件语句在编译器将执行的操作中起着重要作用。

在标准的正式俚语中,原子加载和常规 int 加载是无序的。例如,条件的引入将创建一个序列点,并强制编译器在序列点 ( http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx ) 之后评估常规 int。然后 c++ 内存模型将完成其余的工作(即确保 CPU 执行指令的可见性)

所以你的说法都不是真的。您绝对可以使用 c++11 构建锁,而不是使用数据竞争 :-) 通常锁会涉及在读取之前等待(这显然是您在这里试图避免的)所以您没有这种问题。

请注意,您原来的 seqlock 有问题,因为您不想只检查 seq0 != seq1 (您可能正在更新中)。seqlock 文件具有正确的条件。

于 2013-05-23T21:04:29.333 回答
0

我在推理这些非顺序一致的内存顺序操作和障碍方面仍然很陌生,但可能是这种代码生成是正确的(或者说是允许的)。从表面上看,它确实看起来很可疑,但如果符合标准的程序无法告诉数据负载被提升,我不会感到惊讶(这意味着这段代码在“好像“ 规则)。

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

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

于 2013-05-23T18:47:02.567 回答