5

普通加载在 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 上应该是原子的无论如何,对吧?

提前致谢。

4

2 回答 2

2

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

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

_ReadWriteBarrier 不是函数 - _ReadWriteBarrier 不插入任何附加指令,也不会阻止 CPU 重新排列读取和写入 - 它只会阻止编译器重新排列它们。_ReadWriteBarrier 是为了防止编译器重新排序。

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

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

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

关于您的代码片段 - 您不必将 ReadWriteBarrier 用作 SYNC 原语 - 不需要第一次调用 _ReadWriteBarrier。

使用 ReadWriteBarrier 时,您不必使用 volatile

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

于 2011-02-12T10:12:53.030 回答
1

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

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

需要注意的事项

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

可能的误解

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

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

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

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

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

[编辑]未对齐的读取发生:

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 提出了一个严重的错误警告,但该信息显然也适用于以前的版本


现在,回答你的问题。

我假设该片段的目的是传递任何变量 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;
}

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


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

于 2011-02-12T08:56:43.720 回答