Visual C++ x86 上的易失性变量和原子操作
-
12-11-2019 - |
题
普通加载在 x86 上具有获取语义,普通存储具有释放语义,但是编译器仍然可以对指令重新排序。虽然栅栏和锁定指令(锁定的 xchg、锁定的 cmpxchg)会阻止硬件和编译器重新排序,但仍然需要使用编译器屏障来保护普通加载和存储。Visual C++ 提供了 _ReadWriterBarrier() 函数,可以防止编译器重新排序,出于同样的原因,C++ 也提供了 volatile 关键字。我写下所有这些信息只是为了确保一切正确无误。所以上面写的都是正确的,是否有任何理由将其标记为将在受 _ReadWriteBarrier() 保护的函数中使用的易失性变量?
例如:
int load(int& var)
{
_ReadWriteBarrier();
T value = var;
_ReadWriteBarrier();
return value;
}
使该变量成为非易失性是否安全?据我了解,因为函数是受保护的,编译器内部无法进行重新排序。另一方面,Visual C++ 为易失性变量提供了特殊行为(与标准所做的不同),它使易失性读取和写入原子加载和存储,但我的目标是 x86,普通加载和存储在 x86 上应该是原子的无论如何,对吧?
提前致谢。
解决方案
volatile关键字也可用。 “易失性”通常用于嵌入式系统,尤其是当变量的值可以随时改变时 - 没有代码所拍摄的任何动作 - 三种常见场景包括从内存映射的外围寄存器或全局变量读取的中断服务程序或多线程中的程序。
所以它是最后一个场景,其中volatile可以被认为与_readwritebarrier类似。
_readwritebarrier不是函数 - _readwritebarrier没有插入任何其他说明,它不会阻止CPU重新排列读写 - 它只阻止编译器重新排列它们。 _readWritebarrier是防止编译器重新排序。
MemoryBarrier是防止CPU重新排序!
编译器通常重新排列指令... C ++不包含对多线程程序的内置支持,因此编译器在重新排序代码时,编译器是单线程的单线程。使用代码中的MSVC使用_ReadWriteBarrier,因此编译器不会移动读取和写入。
检查此链接有关这些主题的更详细讨论 http://msdn.microsoft.com/zh- US / LIBLUCE / EE418650(v= Vs.85).aspx
关于您的代码段 - 您不必使用READWRITEBARRIER作为同步原语 - 不需要对_ReadWRITEBARRIER的第一个调用。
使用ReadWritebarrier时,您不必使用volatile
你写道“它使波动读写并写出原子负荷和商店” - 我不认为可以说,原子性和波动性不同。原子操作被认为是不可分割的 - ... http://www.yoda。 arachsys.com/csharp/threads/volatility.shtml
其他提示
笔记:我不是这个话题的专家,我的一些说法 是 “我在网上听到的”,但我想我还是澄清了一些误解。
[编辑] 一般来说,我会依赖特定于平台的特性,例如 x86 原子读取,并且仅在由隔离的本地优化保护的情况下缺乏 OOOX。 #ifdef
检查目标平台,最好附带一个便携式解决方案 #else
小路。
需要注意的事项
- 读/写操作的原子性
- 由于编译器优化而重新排序(这包括由于简单的寄存器缓存而被另一个线程看到的不同顺序)
- CPU中的乱序执行
可能的误解
1. 据我了解,因为函数是受保护的,编译器内部无法进行重新排序。
[编辑] 澄清:这 _ReadWriteBarrier
提供针对指令重新排序的保护,但是,您必须超越该函数的范围。 _ReadWriteBarrier
已在 VS 2010 中修复此问题,早期版本可能会被破坏(取决于它们实际执行的优化)。
优化不仅限于功能。有多种机制(自动内联、链接时代码生成)跨越函数甚至编译单元(并且可以提供比小范围寄存器缓存更重要的优化)。
2. Visual C++ [...] 使易失性读取和写入原子加载和存储,
你是在哪里找到那个东西的。 微软软件定义网络 说超出标准,将在读取和写入周围设置内存屏障,不保证原子读取。
[编辑] 请注意,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;
}
不,那个 _WriteBarrier
MSDN 中有第二个警告:*在过去版本的 Visual C++ 编译器中,_ReadWriteBarrier 和 _WriteBarrier 函数仅在本地强制执行,不会影响调用树上的函数。这些函数现在在调用树中一直强制执行。*
我 希望 那是对的。stackoverflowers,如果我错了,请纠正我。