【问题标题】:volatile variable and atomic operations on Visual C++ x86Visual C++ x86 上的 volatile 变量和原子操作
【发布时间】:2011-06-25 23:46:33
【问题描述】:

普通加载在 x86 上具有获取语义,普通存储具有释放语义,但是编译器仍然可以重新排序指令。虽然栅栏和锁定指令(锁定的 xchg、锁定的 cmpxchg)可以防止硬件和编译器重新排序,但仍然需要纯加载和存储来保护编译器屏障。 Visual C++ 提供了 _ReadWriterBarrier() 函数,它可以防止编译器重新排序,C++ 出于同样的原因也提供了 volatile 关键字。我写下所有这些信息只是为了确保我做对了。所以上面写的都是真的,有什么理由将它们标记为 volatile 变量,这些变量将在受 _ReadWriteBarrier() 保护的函数中使用?

例如:

int load(int& var)
{
    _ReadWriteBarrier();
    T value = var;
    _ReadWriteBarrier();
    return value;
}

使该变量为非易失性是否安全?据我了解,因为函数受到保护,内部编译器无法进行重新排序。另一方面,Visual C++ 为 volatile 变量提供了特殊行为(与标准不同),它使 volatile 读写原子加载和存储,但我的目标是 x86,普通加载和存储在 x86 上应该是原子的无论如何,对吧?

提前致谢。

【问题讨论】:

  • 将自己的原子从 volatile + 障碍中滚出仍然可行,但已被 C++11 std::atomic 淘汰。 When to use volatile with multi threading? 解释说答案基本上是“从不”,并且您可以使用带有 std::memory_order_relaxed atomics 的可移植代码让编译器发出相同的 asm。另请注意,您通常应该在存储之后使用 mfence (RWbarrier),而不是在加载之前和之后。

标签: c++ visual-c++ atomic volatile memory-fences


【解决方案1】:

Volatile 关键字在 C 中也可用。 “易失性”通常用于嵌入式系统,特别是当变量的值可能随时更改时 - 代码不采取任何行动 - 三种常见情况包括从内存映射的外围寄存器或全局变量读取中断服务例程或多线程程序中的那些。

所以这是 volatile 可以被认为类似于 _ReadWriteBarrier 的最后一个场景。

_ReadWriteBarrier 不是函数 - _ReadWriteBarrier 不会插入任何额外的指令,也不会阻止 CPU 重新排列读写操作——它只会阻止编译器重新排列它们。 _ReadWriteBarrier 是为了防止编译器重新排序。

MemoryBarrier 是为了防止 CPU 重新排序!

编译器通常会重新排列指令... C++ 不包含对多线程程序的内置支持,因此编译器在重新排序代码时假定代码是单线程的。使用 MSVC 在代码中使用 _ReadWriteBarrier,这样编译器就不会在其上移动读取和写入。

查看此链接以获取有关这些主题的更详细讨论 http://msdn.microsoft.com/en-us/library/ee418650(v=vs.85).aspx

关于您的代码 sn-p - 您不必使用 ReadWriteBarrier 作为 SYNC 原语 - 不需要第一次调用 _ReadWriteBarrier。

使用 ReadWriteBarrier 时,您不必使用 volatile

您写了“它使 volatile 读取和写入原子加载和存储” - 我不认为这样说是可以的,原子性和易变性是不同的。原子操作被认为是不可分割的 - ... http://www.yoda.arachsys.com/csharp/threads/volatility.shtml

【讨论】:

  • 使用 ReadWriteBarrier 时,您不必使用 volatile - 谢谢。
  • 关于您的代码 sn-p - 您不必将 ReadWriteBarrier 用作 SYNC 原语 - 不需要第一次调用 _ReadWriteBarrier。 是的,您是对的。我打算稍后做优化。
  • 我不认为可以这么说,原子性和易变性是不同的 - 我说的是 Visual C++ 扩展。
  • 关于您的评论“我在谈论 Visual C++ 扩展”——不管它是哪个编译器——易失性和原子性是不同的!
  • 不管是哪个编译器——易失性和原子性是不同的——它们是不同的,但是一些编译器对 volatile 变量的加载/存储访问是原子的。
【解决方案2】:

注意:我不是这个主题的专家,我的一些陈述“我在互联网上听到的”,但我想我仍然可以澄清一些误解。

[编辑]一般来说,我会依赖平台特定的东西,例如 x86 原子读取和缺乏 OOOX 仅在由 #ifdef 检查目标平台的隔离的本地优化中,理想情况下伴随着#else 路径中的便携式解决方案。

注意事项

  • 读/写操作的原子性
  • 由于编译器优化而重新排序(这包括由于简单的寄存器缓存而被另一个线程看到的不同顺序)
  • CPU 中的乱序执行

可能的误解

1. 据我了解,因为函数受到保护,内部编译器无法重新排序。
[编辑] 澄清一下:_ReadWriteBarrier 提供了防止指令重新排序的保护,但是,您必须超越函数的范围。 _ReadWriteBarrier 已在 VS 2010 中修复,早期版本可能会被破坏(取决于它们实际所做的优化)。

优化不仅限于功能。有多种机制(自动内联、链接时间代码生成)跨越函数甚至编译单元(并且可以提供比小范围寄存器缓存更重要的优化)。

2. Visual C++ [...] 使 volatile 读写原子加载和存储,
你是在哪里找到那个东西的。 MSDN 表示超出标准,会在读写周围设置内存屏障,不保证原子读取。

[edit]请注意,C#、Java、Delphi 等具有不同的内存 mdoel,可能会做出不同的保证。

3. 在 x86 上,普通的加载和存储应该是原子的,对吧?
不,他们不是。未对齐的读取不是原子的。如果它们对齐良好,它们恰好是原子的——除非它被隔离且易于交换,否则我不会依赖这一事实。否则,您的“x86 简化”将成为对该目标的锁定。

[edit]发生未对齐的读取:

char * c = new char[sizeof(int)+1];
load(*(int *)c);      // allowed by standard to be unaligned
load(*(int *)(c+1));  // unaligned with most allocators

#pragma pack(push,1)
struct 
{
   char c;
   int  i;
} foo;
load(foo.i);         // caller said so
#pragma pack(pop)

如果您记得参数必须对齐并且您控制所有代码,那么这当然是所有学术性的。我不会再写这样的代码了,因为我经常被过去的懒惰所困扰。

4. 普通加载在 x86 上具有获取语义,普通存储具有释放语义
不,x86 处理器不使用乱序执行(或者更确切地说,没有可见的 OOOX - 我认为),但这并不能阻止优化器重新排序指令。

5. _ReadBarrier / _WriteBarrier / _ReadWriteBarrier 做所有的魔法 他们没有 - 他们只是阻止优化器重新排序。 MSDN 最终将其设为 VS2010 的 big bad warning,但信息显然适用于 previous versions as well


现在,回答您的问题。

我假设 sn-p 的目的是传递任何变量 N,并加载它(原子地?)直接的选择是互锁读取或(在 Visual C++ 2005 及更高版本上)易失性读取。

否则在读取之前您需要为编译器和 CPU 设置一个屏障,在 VC++ 客厅中,这将是:

int load(int& var)
{   
  // force Optimizer to complete all memory writes:
  // (Note that this had issues before VC++ 2010)
   _WriteBarrier();    

  // force CPU to settle all pending read/writes, and not to start new ones:
   MemoryBarrier();

   // now, read.
   int value = var;    
   return value;
}

没有,_WriteBarrier 在 MSDN 中有第二个警告: *在 Visual C++ 编译器的过去版本中,_ReadWriteBarrier 和 _WriteBarrier 函数仅在本地强制执行,不会影响调用树上的函数。这些函数现在在调用树中一直强制执行。*


希望这是正确的。 stackoverflowers,如果我错了,请纠正我。

【讨论】:

  • 1.传递给 Interlocked* 函数的变量不需要是易失的,因为 Interlocked* 函数是完整的内存栅栏。同样的情况也适用于 volatile,我错了吗?如果我关心在我的函数中重新排序人员,则变量本身不需要是易变的。我正在使用 _ReadWriteBarrier 处理编译器重新排序,我不关心 CPU 重新排序,因为我使用的是 x86,并且普通负载本身并没有放松。我不是在争论,我只是解释一下我所知道的以及我从不同文档中理解的内容。
  • 2.也许我错了,但是 C# 和 Java 编译器使对 volatile 变量的读/写访问是原子的,而不是读-修改-写访问(即 volatile int;i++ 不是原子的)。
  • 什么意思?所有变量都应正确对齐。能举个未对齐读取的例子吗?
  • 4.当然,这就是使用 _ReadWriterBarrier() 的原因,而不是硬件屏障。
  • 5.尽一切可能防止编译器重新排序,但是我们在这里不需要硬件屏障,因为 x86 没有宽松的内存模型,这就是我所说的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-05
  • 2021-10-23
  • 1970-01-01
  • 1970-01-01
  • 2012-10-03
相关资源
最近更新 更多