繁体   English   中英

在这种情况下,volatile限定符是否重要?

[英]Does the volatile qualifier matter in this case?

我在C ++ volatile成员函数的答案中看到了一个代码示例,演示了volatile限定符的用法,引用如下:

volatile int x;

int DoSomething() {
    x = 1;
    DoSomeOtherStuff();
    return x+1; // Don't just return 2 because we stored a 1 in x.  
                // Check to get its current value
}

我不知道volatile限定符是否与上面的代码有任何区别。 x是一个全局变量,在x上写和读之间有一个函数调用,我们只读x一次。 编译器不应该对x执行真正的读取(即使它不是volatile )吗?

我认为这与以下案例不同:

volatile int x;

int DoSomething() {
    x = 1;
    while (x != 1)
        break;
}

在这种情况下,我们反复阅读x到写后立即x ,所以volatile是必要得到的先进的日期值x被其他线程写入。

我对这些代码示例的理解不是很有信心,如果我错了,请纠正我。

编辑(回复评论):对不起,我没有说清楚我的问题。 至于第一个代码片段,我正在质疑代码是否是可能使用volatile的有效示例(不是volatile保证使用)。 我只想知道,没有volatile ,是否保证DoSomeOtherStuff()x任何可能的更改都可以反映在return x+1 ,假设没有多线程或其他非平凡的事情,如内存映射IO。 因为如果它保证在没有volatile情况下工作,那么这个例子是相关的,甚至没有提到volatile的平台相关特性,正如一些评论指出的那样。 但如果不能保证,那么我担心我现有的一些代码可能无法正常工作。
(我可能根本不应该放第二个代码片段。)

volatile限定符从不会对代码本身的含义产生影响。 除非编译器能够证明DoSomeOtherStuff()不修改x ,否则它必须重新读取x无论是volatile还是no。 对于volatile来说, x必须是内存映射IO,这可能会在程序之外发生变化。 如果我们想象它是一个每微秒增加的寄存器,例如:

int
MeasureExecutionTime()
{
    x = 0;
    DoSomeOtherStuff();
    return x;
}

将返回DoSomeOtherStuff使用的时间量; 编译器将被要求重新加载它,即使它内联DoSomeOtherStuff ,并看到它从未修改过x

当然,在典型的台式机上,可能没有任何内存映射IO,如果存在,则它位于受保护的内存中,您无法访问它。 而且许多编译器并没有真正生成使其正常工作所需的代码。 因此,对于这类机器上的通用应用程序,每次使用volatile都没有任何意义。

编辑:

关于你的第二个代码片段:正如通常实现的那样, volatile并不能保证你得到x的最新副本。 可以说,这不符合volatile的意图,但它是g ++,Sun CC和至少某些版本的VC ++的工作方式。 编译器将在循环中发出加载指令以读取x ,但硬件可能会找到管道中已存在的值,而不会将该读取请求传播到存储器总线。 为了保证新的读取,编译器必须插入fence或membar指令。

也许更重要的是(因为迟早会发生某些事情以使值不在管道中),这个循环机制将用于等待,直到在另一个线程中写入的其他值已经稳定。 除了volatile对其他变量的读写时间没有影响。

要理解volatile ,了解引入它的意图非常重要。 当它被引入时,内存管道等是未知的,并且(C)标准忽略了线程(或具有共享内存的多个进程)。 volatile的目的是允许支持内存映射IO,其中写入地址会产生外部后果,并且连续读取并不总是读取相同的内容。 从来没有任何意图将代码中的其他变量同步。 在线程之间进行通信时,通常是读取和写入所有共享变量的顺序 ,这很重要。 如果我这样做:

globalPointer = new Xxx;

和其他线程可以访问globalPointer ,重要的是在globalPointer的值发生变化之前, Xxx构造函数中的所有写入都变得可见。 为实现这一目标,不仅将globalPointer必须是volatile ,也是所有成员Xxx ,以及任何变量成员函数Xxx可能会使用,或通过指针访问的任何数据Xxx 这根本不合理; 你很快就会在程序中的一切最终volatile 即使这样,它也需要编译器正确地实现volatile ,在每次访问时发出fence或membar指令。 (FWIW:fence或membar指令可以将内存访问所需的时间乘以10倍或更多。)

这里的解决方案不是易失性的,而是使用添加到C ++ 11的atomic_loadatomic_store原语来访问指针(并且只访问指针)原子。 这些原语确实会导致使用必要的fence或membar指令; 它们还告诉编译器不要移动任何内存访问。 因此,使用atomic_load来设置上面的指针,将导致所有先前的内存写入在对指针的写入变为可见之前对其他线程可见(在读取线程使用atomic_read的情况下, atomic_read - atomic_write确保所有先前的写入在所有线程的“公共”内存中可用,而atomic_read确保所有后续读取将转到“公共”内存,而不是在管道中已经获取某些值。

编译器可以获得有关DoSomeOtherStuff()函数的一些信息。 例如:

static int DoSomeOtherStuff()
{
    return 42;
}

volatile int x;

int DoSomething() {
  x = 1;
  DoSomeOtherStuff();
  return x+1; // Don't just return 2 because we stored a 1 in x.
  // Check to get its current value
}

-O3选项的GCC完全删除了DoSomeOtherStuff()函数(以及它的主体DoSomeOtherStuff()的调用,但仍然重新加载x以最后返回x+1

这是关于volatile的现有SO问题: volatile关键字有什么用?

只有第一个代码片段是正确的,没有明显的理由让x声明为volatile。 但是,如果您了解该关键字应该用于什么,您可以推断x是易失性的,因为此代码之外存在某些内容,这可能会改变它的值。 例如,存储器可以附加到一些其他硬件或由另一个程序写入。 因此,程序员正在指示编译器无法看到x的值发生变化的所有可能方式。 因此编译器可能无法以某种方式优化代码。

第二个代码片段本身并不需要volatile关键字。 Volatile用作编译器提示,指出内存可能会因当前程序之外的力而改变。 它不应该用于线程通信。 有些新的C ++类型应该用于这些情况。

我建议阅读Herb Sutter 撰写的这篇文章以及本次演讲

这里真正确定的唯一方法是检查为目标平台生成的汇编代码。 如果volatile以你想要的方式执行,那么对变量x的任何读取都将通过内存加载来完成。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM