Visual c++ VS2012 C++;为条件移动(cmov)生成可疑代码

Visual c++ VS2012 C++;为条件移动(cmov)生成可疑代码,visual-c++,compilation,visual-studio-2012,x86-64,Visual C++,Compilation,Visual Studio 2012,X86 64,我有一个环形缓冲区,由一个生产者写入,N个消费者读取。由于它是一个环形缓冲区,所以生产者写入的索引可以小于消费者当前的最小索引。生产者和消费者的位置由他们自己的光标跟踪 class Cursor { public: inline int64_t Get() const { return iValue; } inline void Set(int64_4 aNewValue) { ::InterlockedExchange64(&iValue, aN

我有一个环形缓冲区,由一个生产者写入,N个消费者读取。由于它是一个环形缓冲区,所以生产者写入的索引可以小于消费者当前的最小索引。生产者和消费者的位置由他们自己的
光标跟踪

class Cursor
{
public:
    inline int64_t Get() const { return iValue; }
    inline void Set(int64_4 aNewValue)
    {
        ::InterlockedExchange64(&iValue, aNewValue);
    }

private:
    int64_t iValue;
};

//
// Returns the ringbuffer position of the furthest-behind Consumer
//
int64_t GetMinimum(const std::vector<Cursor*>& aCursors, int64_t aMinimum = INT64_MAX)
{
    for (auto c : aCursors)
    {
        int64_t next = c->Get();
        if (next < aMinimum)
        {
            aMinimum = next;
        } 
    }

    return aMinimum;
}
我看不出编译器如何认为读取
c->Get()
的值是可以的,将其与
aMinimum
进行比较,然后有条件地将
c->Get()
的重读值移动到
aMinimum
。在我看来,这个值可能在
cmp
cmovl
指令之间发生了变化。如果我是正确的,那么以下场景是可能的:

  • aminum
    当前设置为2

  • c->Get()
    返回1

  • 完成
    cmp
    ,并设置
    小于
    标志

  • 另一个线程将当前
    c
    保存的值更新为3

  • cmovl
    最小值设置为3

  • 生产者看到3并覆盖ringbuffer位置2的数据,即使它尚未被处理

我看了太久了吗?不应该是这样的吗

mov rbx, QWORD PTR [r8+56]
cmp rbx, rax 
cmovl rax, rbx 

在访问
iValue
时,您没有使用原子或任何类型的线程间排序操作(在另一个线程上修改
iValue
的内容可能也是如此,但我们会看到这并不重要),因此编译器可以自由地假设它在两个汇编代码行之间保持不变。如果另一个线程修改了
iValue
,则表示行为未定义

如果您的代码是线程安全的,那么您需要使用原子、锁或一些排序操作

C++11标准在第1.10节“多线程执行和数据竞争”中对此进行了形式化描述,这并不是特别轻松的阅读。我认为与此示例相关的部分包括:

第10段:

如果满足以下条件,则评估A在评估B之前排序依赖关系

  • A对原子对象M执行释放操作,而在另一个线程中,B对M执行消耗操作,并读取由A开头的释放序列中的任何副作用写入的值,或
  • 对于某些计算X,依赖项在X之前排序,X将依赖项带到B
如果我们说求值A对应于
Cursor::Get()
函数,而求值B对应于修改
iValue
的一些看不见的代码。求值A(
Cursor::Get()
)不在原子对象上执行任何操作,也不在任何其他操作之前排序依赖项(因此这里不涉及“X”)

如果我们说求值A对应于修改
iValue
的代码,B对应于
Cursor::Get()
,则可以得出相同的结论。因此,在
Cursor::Get()
iValue
的修饰符之间不存在“依赖关系排序在前”的关系

因此,
Cursor::Get()
在任何可能修改的
iValue
之前,都不会对依赖项进行排序

第11段:

如果

  • A与B同步,或
  • A依赖项排序在B之前,或
  • 对于某些评估X
    • A与X同步,X在B之前排序,或
    • A在X之前排序,X内部线程在B之前排序,或
    • 一个内部线程发生在X之前,X个内部线程发生在B之前
同样,这些条件都不满足,所以之前没有线程间的情况发生

第12段

评估A发生在评估B之前,如果:

  • A在B之前排序,或
  • 一个内部线程发生在B之前
我们已经证明了两个操作“线程间发生在”另一个之前。术语“sequenced before”在1.9/13“Program execution”中定义为仅适用于单个线程上发生的求值(“sequenced before”是C++11对旧“sequence point”术语的替换)。因为我们讨论的是单独线程上的操作,所以A不能在B之前排序

因此在这一点上,我们发现
Cursor::Get()
不会在另一个线程上发生的
iValue
修改之前发生(反之亦然)。最后,我们在第21段中得出这一点的底线:

如果一个程序在不同的线程中包含两个冲突的操作,则该程序的执行包含一个数据竞争,其中至少一个操作不是原子的,并且两个操作都不在另一个线程之前发生。任何这样的数据竞争都会导致未定义的行为

因此,如果要在一个线程上使用
Cursor::Get()
,在另一个线程上使用修改
iValue
的内容,则需要使用原子或其他排序操作(互斥或类似操作)来避免未定义的行为

请注意,根据标准,
volatile
不足以提供线程之间的排序。微软的编译器可能会为
volatile
提供一些额外的承诺,以支持定义良好的线程间行为,但这种支持是可配置的,因此我的建议是避免依赖
volatile
来生成新代码。以下是MSDN对此()的一些看法:

符合ISO标准

<>如果您熟悉C++的易失性关键字,或者熟悉VisualC++的早期版本中的易失性行为,请注意C++ 11 ISO标准Valy关键字不同,并且在指定/ISRONT:ISO编译器选项时在VisualStudio中得到支持。(对于ARM,默认情况下会指定它)。易变的钥匙虫
mov rbx, QWORD PTR [r8+56]
cmp rbx, rax 
cmovl rax, rbx