什么样的优化在C ++中“易失”?

时间:2010-08-30 22:02:18

标签: c++ optimization volatile

我正在查找关键字volatile以及它的用途,我得到的答案非常多:

  

它用于防止编译器优化代码。

有一些示例,例如轮询内存映射硬件时:如果没有volatile,则会删除轮询循环,因为编译器可能会识别出条件值永远不会更改。但由于只有一个例子或两个例子,它让我思考:在避免不必要的优化方面,我们是否还需要使用volatile?条件变量是唯一需要volatile的地方吗?

我认为优化是特定于编译器的,因此未在C ++规范中指定。这是否意味着我们必须通过直觉,说嗯,我怀疑如果我不将该变量声明为volatile ,或者是否有任何明确的规则,我的编译器将废除此过去了?

8 个答案:

答案 0 :(得分:24)

基本上,volatile宣布一个值可能会在你的程序背后发生变化。这可以防止编译器缓存该值(在CPU寄存器中),并防止在程序的POV中看起来不必要时访问该值。

应该触发volatile的使用是指一个值发生变化,尽管你的程序没有写入它,并且没有其他内存障碍(如用于多线程程序的互斥锁) 。

答案 1 :(得分:21)

C ++程序的可观察行为是通过读取和写入volatile变量以及对输入/输出函数的任何调用来确定的。

这需要的是对volatile变量的所有读写必须按照它们在代码中出现的顺序发生,并且必须发生。 (如果编译器违反了其中一条规则,则会破坏as-if规则。)

这就是全部。当您需要指示读取或写入变量被视为可观察的效果时,可以使用它。 (注意,"C++ and the Perils of Double-Checked Locking" article触及了这一点。)


因此,为了回答标题问题,它会阻止任何可能重新排序相对于其他易变变量的volatile变量评估的优化。

这意味着更改的编译器:

int x = 2;
volatile int y = 5;
x = 5;
y = 7;

int x = 5;
volatile int y = 5;
y = 7;

很好,因为x的值不是可观察行为的一部分(它不易变)。什么不合适的是将赋值从5改为赋值为7,因为写5是一个可观察的效果。

答案 2 :(得分:10)

条件变量,需要volatile;严格来说,它只在设备驱动程序中需要。

volatile保证对对象的读取和写入不会被优化掉,或者相对于另一个volatile重新排序。如果您正在忙于循环另一个线程修改的变量,则应将其声明为volatile。但是,你不应该忙着循环。由于该语言并非真正为多线程设计,因此不太受支持。例如,编译器可能会在循环之前到循环之前将写入移动到 - 易失性变量,从而违反锁定。 (对于无限旋转,这可能只发生在C ++ 0x下。)

当你调用一个线程库函数时,它充当一个内存栅栏,编译器会认为所有的值都已经改变了 - 基本上所有的东西都是volatile。这可以通过任何线程库指定或默认实现,以保持车轮平稳转动。

C ++ 0x可能没有这个缺点,因为它引入了正式的多线程语义。我并不熟悉这些更改,但为了向后兼容,它不需要声明之前不存在的任何易变。

答案 3 :(得分:4)

Volatile不会尝试将数据保存到cpu寄存器(比内存快100倍)。它必须在每次使用时从内存中读取它。

答案 4 :(得分:4)

请记住,“似乎规则”意味着编译器可以而且应该做任何想做的事情,只要从程序外部看到的行为是相同的。特别是,虽然变量在概念上命名了内存中的某个区域,但实际上它没有理由存在于内存中。

可以在寄存器中:

它的值可以计算掉,例如在:

int x = 2;
int y = x + 7;
return y + 1;

根本不需要xy,但可以替换为:

return 10;

另一个例子是,任何不影响外部状态的代码都可以完全删除。例如。如果你将敏感数据归零,编译器可以将其视为浪费的练习(“为什么要写入不会被读取的内容?”)并将其删除。 volatile可用于阻止这种情况发生。

volatile可以被认为是“这个变量的状态必须被视为外部可见状态的一部分,而不是被搞乱”。除了字面上遵循源代码之外,不允许使用它的优化。

(注释C#。我在volatile上看到的很多内容表明人们正在阅读C ++ volatile并将其应用于C#,并在C#中阅读并将其应用于C ++。实际上,volatile在两者之间表现得如此不同,以至于认为它们无关。)

答案 5 :(得分:1)

除非您使用的是嵌入式系统,或者您正在编写使用内存映射作为通信方式的硬件驱动程序,否则永远不会使用volatile

考虑:

int main()
{
    volatile int SomeHardwareMemory; //This is a platform specific INT location. 
    for(int idx=0; idx < 56; ++idx)
    {
        printf("%d", SomeHardwareMemory);
    }
}

必须生成如下代码:

loadIntoRegister3 56
loadIntoRegister2 "%d"
loopTop:
loadIntoRegister1 <<SOMEHARDWAREMEMORY>
pushRegister2
pushRegister1
call printf
decrementRegister3
ifRegister3LessThan 56 goto loopTop

而没有volatile则可能是:

loadIntoRegister3 56
loadIntoRegister2 "%d"
loadIntoRegister1 <<SOMEHARDWAREMEMORY>
loopTop:
pushRegister2
pushRegister1
call printf
decrementRegister3
ifRegister3LessThan 56 goto loopTop

关于volatile的假设是可以改变变量的存储位置。每次使用变量时,您都强制编译器从内存加载实际值;并告诉编译器不允许在寄存器中重用该值。

答案 6 :(得分:1)

考虑volatile变量的一种方法是想象它是一个虚拟属性;写入甚至读取可能会做编译器无法知道的事情。用于写入/读取volatile变量的实际生成代码只是内存写入或读取(*),但编译器必须将代码视为不透明;它不能做出任何可能多余的假设。问题不仅在于确保编译的代码注意到某些内容导致变量发生变化。在某些系统中,即使是内存读取也可以“做”事情。

(*)在某些编译器中,可以将volatile变量作为不同的操作添加,减去,递增,递减等。它可能对编译器有用:

  volatilevar++;

作为

  inc [_volatilevar]

因为后一种形式在许多微处理器上可能是原子的(尽管不是在现代多核PC上)。但是,重要的是要注意,如果声明是:

  volatilevar2 = (volatilevar1++);

正确的代码不会是:

  mov ax,[_volatilevar1] ; Reads it once
  inc [_volatilevar]     ; Reads it again (oops)
  mov [_volatilevar2],ax

,也不

  mov ax,[_volatilevar1]
  mov [_volatilevar2],ax ; Writes in wrong sequence
  inc ax
  mov [_volatilevar1],ax

而是

  mov ax,[_volatilevar1]
  mov bx,ax
  inc ax
  mov [_volatilevar1],ax
  mov [_volatilevar2],bx

以不同方式编写源代码可以生成更高效(也可能更安全)的代码。如果'volatilevar1'不介意被读两次并且'volatilevar2'不介意在volatilevar1之前写入,那么将语句分成

  volatilevar2 = volatilevar1;
  volatilevar1++;

将允许更快,更安全的代码。

答案 7 :(得分:0)

通常编译器假定程序是单线程的,因此它完全了解变量值发生了什么。然后,智能编译器可以证明该程序可以转换为具有等效语义但性能更好的另一个程序。例如

x = y+y+y+y+y;

可以转换为

x = y*5;

但是,如果可以在线程外部更改变量,则编译器不会通过简单地检查这段代码就完全了解正在发生的事情。它不能再像上面那样进行优化。 (编辑:它可能在这种情况下;我们需要更复杂的例子

默认情况下,对于性能优化,假定单线程访问。这种假设通常是正确的。除非程序员明确指示volatile关键字。