c++ - VC++,x86上的/volatile:ms

标签 c++ visual-c++ x86 volatile

documentation on volatile 说:

When the /volatile:ms compiler option is used—by default when architectures other than ARM are targeted—the compiler generates extra code to maintain ordering among references to volatile objects in addition to maintaining ordering to references to other global objects.



哪些确切的代码可以使用/volatile:ms/volatile:iso进行不同的编译?

最佳答案

对此有一个完整的了解需要一些历史类(class)。 (还有谁不喜欢历史?……专攻历史的人说。)/volatile:ms语义首先使用Visual Studio 2005添加到编译器中。Starting with that version,标记为volatile的变量自动在读取时强加语义,并在写入时释放语义,通过该变量。

这是什么意思?它与内存模型有关,特别是与允许编译器对内存访问操作进行重新排序的积极程度有关。具有获取语义的操作可以防止后续的内存操作卡在其上方;具有释放语义的操作可防止之前的内存操作延迟到之后。顾名思义,获取语义通常在获取资源时使用,而释放语义通常在释放资源时使用。 MSDN has a more complete description of acquire and release semantics;它说:

An operation has acquire semantics if other processors will always see its effect before any subsequent operation's effect. An operation has release semantics if other processors will see every preceding operation's effect before the effect of the operation itself. Consider the following code example:

a++;
b++;
c++;

From another processor's point of view, the preceding operations can appear to occur in any order. For example, the other processor might see the increment of b before the increment of a.

For example, the InterlockedIncrementAcquire routine uses acquire semantics to increment a variable. If you rewrote the preceding code example as follows:

InterlockedIncrementAcquire(&a);
b++;
c++;

other processors would always see the increment of a before the increments of b and c.

Likewise, the InterlockedIncrementRelease routine uses release semantics to increment a variable. If you rewrote the code example once again, as follows:

a++;
b++;
InterlockedIncrementRelease(&c);

other processors would always see the increments of a and b before the increment of c.



现在,就像MSDN所说的那样,原子操作既具有获取语义又具有释放语义。而且,实际上,在x86上,没有办法只给一条指令获取或释放语义,因此,即使要实现其中之一,也必须使该指令成为原子的(编译器通常会通过发出LOCK CMPXCHG指令来做到这一点)。

在Visual Studio 2005增强volatile语义之前,想要编写正确代码的开发人员需要使用Interlocked*系列功能,如MSDN文章中所述。不幸的是,许多开发人员未能做到这一点,并且得到的代码大多是偶然地起作用(或根本不起作用)。但是,鉴于x86的内存模型相对严格,它很有可能偶然发生了工作。由于on x86, most loads and stores already have acquire/release semantics,您通常可以免费获得想要的语义,因此甚至不需要使任何原子化。 (非临时性存储是明显的异常(exception),但是在这种情况下,这些都没关系。)我怀疑在x86上实现这种简便性,再加上程序员通常无法理解并做正确的事情,说服Microsoft在VS 2005中增强了volatile的语义。

进行此更改的另一个潜在原因是多线程代码的重要性日益提高。 2005年左右,带有HyperThreading的奔腾4芯片开始流行,有效地将同步多线程带入了每个用户的桌面。可能并非巧合,VS 2005也是removed the option to link to single-threaded version of the C run-time libraries。只有当您具有多线程代码(可以在多个处理器上执行)时,您才真正开始担心正确的内存访问语义。

在VS 2005及更高版本中,您可以将指针参数标记为volatile并获得所需的获取语义。易变性暗示/强加了获取语义,这使得在多处理环境中运行的多线程代码安全。在2011年之前,这非常重要,因为C和C++语言标准绝对没有关于线程的任何内容,也没有为您提供编写正确代码的可移植方式。

这使我们有权回答您的问题。如果您的代码采用volatile的这些扩展语义,那么您需要传递/volatile:ms开关以确保编译器继续应用它们。如果您已经编写了使用现代原语进行原子,线程安全操作的C++ 11样式代码,则不需要volatile来具有这些扩展的语义,并且可以安全地传递/volatile:iso。换句话说,as manni66 quipped,如果您的代码“将volatilestd::atomic误用”,那么您将看到行为上的差异,并且需要/volatile:ms来确保volatilestd::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会(理论上)一样。

顺便说一下,请注意这种排序的方式……嗯……易变。如果我们在GCC中的-O1上进行编译,我们将获得指令,它们以与原始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重新排序存储的魔术序列。因此,很可能当前在实践中,在针对x86体系结构时,/volatile:iso开关设置不会有很大的不同。至少可以这样说,但这是一个非常宽松的保证。

请注意,此经验观察与Alexander Gutenev's speculation一致,即仅在ARM上观察到语义上的差异,并且引入这些开关的全部原因是为了避免在此新支持的平台上付出性能损失。同时,在x86方面,生成的代码中的语义没有实际更改,因为基本上没有成本。 (除了一些极其琐碎的优化可能性之外,但这还需要其优化器具有两个完全独立的调度程序,这可能不是很好地利用开发人员的时间。)

关键是,通过/volatile:iso,允许MSVC像GCC一样对商店进行重新排序。使用/volatile:ms可以确保不会,因为volatile隐含了该变量的获取/释放语义。

奖励阅读:那么,在严格符合ISO要求的代码中(即,当使用volatile开关时)应使用什么/volatile:iso?好吧,volatile基本上是用于内存映射的I / O的。这就是它最初引入时的原始含义,并且仍然是其主要目的。我曾开 Jest 说过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

指针在循环外被取消引用一次,将字节缓存在寄存器中。循环顶部的指令仅测试已注册的值,而不创建循环或无限循环。哎呀。

但是请注意,在ISO标准C++中使用volatile并不会消除对关键节,互斥锁或其他类型锁的需求。如果另一个线程可能修改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

关于c++ - VC++,x86上的/volatile:ms,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44374614/

相关文章:

c++ - SSE:reinterpret_cast<__m128*> 而不是 _mm_load_ps

assembly - MASM 程序集将 8 位寄存器移动到 16 位寄存器(即 mov cx, ch)

c++ - 尝试制作 AMF3 数据包

c++ - std::integral_constant 背后的原因是什么?

.net - SHCreateStreamOnFileEx 链接器错误

assembly - 有没有一种简单的方法可以在 AT&T 程序集 : %eax * %ebx = %ecx 中将两个寄存器相乘

c++ - 仅在 boost::asio basic_stream_socket::async_read_some 处理程序中使用部分数据

c++ - 互斥锁示例/教程?

c++ - 在头文件中搜索#ifdef

visual-c++ - 如何使用Visual C++查看代码背后的程序集?