上的文档volatile
说:
当使用/ volatile:ms编译器选项时(默认情况下,以ARM以外的体系结构为目标时),编译器将生成额外的代码,以维护对易失对象的引用之间的顺序,并保持对其他全局对象的引用的顺序。
哪些确切的代码可以使用/volatile:ms
和进行不同的编译/volatile:iso
?
1> Cody Gray..:
对此有一个完整的了解需要一些历史课程。(谁不喜欢历史?......说谁是历史专业的人。)的/volatile:ms
语义首先加入到与Visual Studio 2005编译器与该版本开始,标志着变量volatile
自动采集征收语义上读取,并在释放语义通过该变量进行写入。
这是什么意思?它与内存模型有关,特别是与允许编译器对内存访问操作进行重新排序的积极程度有关。具有获取语义的操作可防止后续的内存操作挂在其上方;具有释放语义的操作可防止之前的内存操作延迟到之后。顾名思义,获取语义通常在获取资源时使用,而释放语义通常在释放资源时使用。MSDN对获取和释放语义有更完整的描述 ; 它说:
如果其他处理器在任何后续操作生效之前总能看到其效果,则该操作具有语义。如果其他处理器将在操作本身的效果之前看到每个先前操作的效果,则该操作具有
释放语义。考虑以下代码示例:
a++;
b++;
c++;
从另一个处理器的角度来看,前面的操作可能以任何顺序发生。例如,另一个处理器可能会在的增量b
之前看到的增量a
。
例如,InterlockedIncrementAcquire
例程使用获取语义来增加变量。如果您重写了前面的代码示例,如下所示:
InterlockedIncrementAcquire(&a);
b++;
c++;
其他处理器总是看到增量a
的增量之前b
和c
。
同样,InterlockedIncrementRelease
例程使用释放语义来增加变量。如果再次重写代码示例,如下所示:
a++;
b++;
InterlockedIncrementRelease(&c);
其他处理器将始终看到a
和b
的增量c
。
现在,就像MSDN所说的那样,原子操作既具有获取语义又具有释放语义。而且,实际上,在x86上,没有办法只给一条指令获取或释放语义,因此,即使要实现其中之一,也必须使该指令成为原子的(编译器通常会通过发出LOCK CMPXCHG
指令来做到这一点)。
在Visual Studio 2005增强volatile
语义之前,要编写正确的代码的开发人员需要使用Interlocked*
功能家族,如MSDN文章中所述。不幸的是,许多开发人员未能做到这一点,并且得到的代码大多是偶然地起作用(或根本不起作用)。但是,有一个很好的机会,这的确是偶然的工作,考虑到86的相对严格的内存模型。您经常可以免费获得所需的语义,因为在x86上,大多数加载和存储已经具有获取/释放语义。,因此您甚至不需要使任何原子化。(非临时性存储是明显的例外,但是在这种情况下,这些都不重要。)我怀疑在x86上实现这种简便性,再加上程序员通常无法理解并做正确的事情,说服微软加强volatile
VS 2005中的语义。
进行此更改的另一个潜在原因是多线程代码的重要性日益提高。2005年大约是带有HyperThreading的 Pentium 4芯片开始流行的时候,有效地将同步多线程带入了每个用户的桌面。可能并非巧合,VS 2005还删除了链接到C运行时库的单线程版本的选项。只有当您具有多线程代码并可能在多个处理器上执行时,您才真正开始担心正确的内存访问语义。
在VS 2005及更高版本中,您只需将指针参数标记为volatile
,即可获得所需的获取语义。易变性暗示/强加了获取语义,这使得在多处理环境中运行的多线程代码安全。在2011年之前,这非常重要,因为C和C ++语言标准绝对没有关于线程的内容,也没有给您提供编写正确代码的可移植方式。
这使我们有权回答您的问题。如果您的代码采用的这些扩展语义volatile
,那么您需要传递此/volatile:ms
开关以确保编译器继续应用它们。如果您编写了使用现代原语进行原子,线程安全操作的C ++ 11风格代码,则无需volatile
具有这些扩展的语义并且可以安全地传递/volatile:iso
。换句话说,作为manni66打趣说,如果你的代码“误用volatile
为std::atomic
”,然后你会看到在行为和需求的差异/volatile:ms
,以保证volatile
不会有相同的效果std::atomic
。
事实证明/volatile:iso
,与相比,我很难找到实际更改所生成代码的示例/volatile:ms
。实际上,Microsoft的优化程序在重新排序指令方面非常保守,这是获取/发布语义应该避免的类型。
这是一个简单的示例(其中您正在使用volatile
全局变量来保护关键部分,就像您在一个简单的“无锁”实现中可能会发现的那样),该示例应证明两者之间的区别:
volatile bool CriticalSection;
int Data[100];
void FillData(int i)
{
Data[i] = 42; // fill data item at index 'i'
CriticalSection = false; // release critical section
}
如果您使用GCC在编译此-O2
代码,它将生成以下机器代码:
FillData(int):
mov eax, DWORD PTR [esp+4] // retrieve parameter 'i' from stack
mov BYTE PTR [CriticalSection], 0 // store '0' in 'CriticalSection'
mov DWORD PTR [Data+eax*4], 42 // store '42' at index 'i' in 'Data'
ret
即使您不太熟练使用汇编语言,您也应该能够看到优化器已对存储进行了重新排序,从而在(CriticalSection = false
)数据被填充之前释放()关键部分,这Data[i] = 42
恰好与汇编语言相反。语句在原始C代码中出现的顺序。,volatile
它对这种重新排序没有影响,因为GCC遵循ISO语义,就像/volatile:iso
在理论上一样。
顺便说一下,请注意这种排序的方式……嗯……易变。如果我们-O1
在GCC 中进行编译,我们将获得指令,这些指令以与原始C代码相同的顺序执行所有操作:
FillData(int):
mov eax, DWORD PTR [esp+4] // retrieve parameter 'i' from stack
mov DWORD PTR [Data+eax*4], 42 // store '42' at index 'i' in 'Data'
mov BYTE PTR [CriticalSection], 0 // store '0' in 'CriticalSection'
ret
当您开始向其中扔出更多指令以重新编译时,尤其是如果要内联此代码时,您可以想象保留原始顺序的可能性很小。
但是,就像我说的那样,MSVC在重新排序指令方面实际上非常保守。无论我指定/volatile:ms
还是/volatile:iso
,我都会得到完全相同的机器代码:
FillData, COMDAT PROC
mov eax, DWORD PTR [esp+4]
mov DWORD PTR [Data+eax*4], 42
mov BYTE PTR [CriticalSection], 0
ret
FillData ENDP
商店是按原始顺序完成的。我玩过各种不同的排列,引入了其他变量和操作,所有这些都无法找到导致MSVC重新排序商店的神奇序列。因此,很可能当前在实践中,/volatile:iso
在针对x86体系结构时,开关设置不会有很大的不同。至少可以这样说,但这是一个非常宽松的保证。
请注意,这种经验观察与Alexander Gutenev的推测一致,即仅在ARM上观察到语义上的差异,并且引入这些开关的全部原因是为了避免在此新支持的平台上损失性能。同时,在x86方面,由于基本上没有成本,因此生成的代码中的语义没有实际更改。(除了一些极其琐碎的优化可能性之外,但这还要求其优化器具有两个完全独立的调度程序,这可能不是很好地利用开发人员时间。)
关键是,通过/volatile:iso
,允许 MSVC 像GCC一样对商店进行重新排序。使用/volatile:ms
,可以确保不会,因为volatile
暗示该变量的获取/释放语义。
奖金阅读:那么,什么是volatile
应该用于在符合ISO标准严格的代码(即,当/volatile:iso
开关使用)?好吧,volatile
基本上是用于内存映射的I / O。这就是它最初引入时的原始含义,并且仍然是其主要目的。我曾开玩笑说过这volatile
是为了读/写磁带机。基本上,标记指针volatile
是为了防止编译器优化读写操作。例如:
volatile char* pDeviceIOAddr = ...;
void Wait()
{
while (*pDeviceIOAddr)
{ }
}
使用参数限定参数的类型volatile
可防止编译器假定后续读取返回相同的值,从而迫使其每次在循环中都进行新读取。换一种说法:
mov eax, DWORD PTR [pDeviceIoAddr] // get pointer
Wait:
cmp BYTE PTR [eax], 0 // dereference pointer, read 1 byte,
jnz Wait // and compare to 0
如果pDeviceIoAddr
不是volatile
,整个循环可能会被消除。优化程序在实践中肯定会这样做,包括MSVC。或者,您可以获得以下病理代码:
mov eax, DWORD PTR [pDeviceIoAddr] // get pointer
mov al, BYTE PTR [eax] // dereference pointer, read 1 byte
Wait:
cmp al, 0 // compare it to 0
jnz Wait
指针在循环外被解引用一次,将字节缓存在寄存器中。循环顶部的指令仅测试已注册的值,而不创建循环或无限循环。哎呀。
但是请注意,volatile
在ISO标准C ++中使用并不能消除对关键节,互斥锁或其他类型锁的需求。如果另一个线程可能进行修改pDeviceIOAddr
,则即使上述代码的正确版本也无法正常工作,因为该地址/指针的读取没有获取语义。获取语义看起来像这样:
Wait:
mov eax, DWORD PTR [pDeviceIoAddr] // get pointer (acquire semantics)
cmp BYTE PTR [eax], 0 // dereference pointer, read 1 byte,
jnz Wait // and compare to 0
而要得到它,您将需要C ++ 11的std::atomic
。