允许编译器优化掉局部volatile变量吗?

时间:2018-07-23 06:01:45

标签: c++ language-lawyer c++17 volatile

是否允许编译器对此进行优化(根据C ++ 17标准):

int fn() {
    volatile int x = 0;
    return x;
}

对此吗?

int fn() {
    return 0;
}

如果是,为什么?如果没有,为什么不呢?


这是关于此主题的一些思考:当前的编译器将fn()编译为放置在堆栈中的局部变量,然后将其返回。例如,在x86-64上,gcc创建以下代码:

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

现在,据我所知,标准并没有说应该将本地volatile变量放在堆栈中。因此,此版本同样不错:

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

在这里,edx存储着x。但是现在,为什么在这里停下来?由于edxeax均为零,我们可以这样说:

xor    eax,eax // eax is the return, and x as well
ret    

然后我们将fn()转换为优化版本。此转换有效吗?如果不是,哪一步无效?

6 个答案:

答案 0 :(得分:59)

不。对volatile对象的访问被视为可观察到的行为,与I / O完全一样,在本地变量和全局变量之间没有特殊区别。

  

符合标准的实现的最低要求是:

     
      
  • 严格按照抽象机的规则评估对volatile对象的访问。
  •   
     

[...]

     

这些统称为程序的可观察行为。

N3690,[简介],¶8

的确切观察方式超出了标准的范围,并且直接属于特定于实现的领域,与I / O和对全局volatile对象的访问完全相同。 volatile的意思是“您认为您知道这里发生的所有事情,但事实并非如此;请相信我,并且不要太聪明地做这些事情,因为我正在您的程序中使用您的字节来做我的秘密工作”。这实际上在[dcl.type.cv]¶7中进行了解释:

  

[注意:volatile是对实现的提示,以避免涉及对象的积极优化   因为对象的值可能通过实现无法检测的方式进行更改。此外,   对于某些实现,volatile可能指示需要特殊的硬件指令才能访问   物体。有关详细的语义,请参见1.9。通常,volatile的语义在C ++中应与在C中相同。—结束说明]

答案 1 :(得分:11)

此循环可以通过按规则进行优化,因为它没有可观察到的行为:

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

该人不能:

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

第二个循环在每次迭代中执行某些操作,这意味着该循环需要O(n)时间。我不知道常数是什么,但是我可以测量它,然后我有一种忙循环(或多或少)已知时间的方法。

之所以可以这样做,是因为该标准规定必须依次访问挥发物。如果编译器决定在这种情况下该标准不适用,我认为我有权提交一个错误报告。

如果编译器选择将looped放入寄存器,我想我对此没有很好的论据。但是它仍然必须为每次循环迭代将该寄存器的值设置为1。

答案 2 :(得分:9)

尽管完全了解volatile表示可观察的I / O,但我还是不同意多数意见。

如果您有此代码:

{
    volatile int x;
    x = 0;
}

我相信编译器可以根据假设规则对其进行优化,假设

  1. 通过其他方式,volatile变量不会在外部可见指针(这里显然不是问题,因为在给定范围内没有这样的东西)

  2. 编译器没有为您提供从外部访问该volatile

  3. 的机制

理由很简单,由于准则#2,您仍然无法观察到差异。

但是,在您的编译器中,准则2可能不满足!编译器可以尝试为您提供从外部“观察” volatile变量的额外保证,例如通过分析堆栈。在这种情况下,行为实际上是 可见的,因此无法对其进行优化。

现在的问题是,以下代码与上面的代码有何不同?

{
    volatile int x = 0;
}

我相信我在Visual C ++中观察到了关于优化的不同行为,但是我不确定是基于什么理由。可能初始化不算作“访问”?我不确定。如果您有兴趣,这可能是一个单独的问题,但是否则,我相信答案就是我上面所解释的。

答案 3 :(得分:6)

从理论上讲,中断处理程序可以

  • 检查返回地址是否在fn()函数内。它可能会通过检测或附加的调试信息来访问符号表或源行号。
  • 然后更改x的值,该值将存储在距堆栈指针可预测的偏移量处。

…因此使fn()返回非零值。

答案 4 :(得分:6)

我将为as-if规则和volatile关键字添加详细的参考。 (在这些页面的底部,遵循“另请参见”和“参考”以追溯到原始规格,但我发现cppreference.com更易于阅读/理解。)

我特别希望您阅读本节

  

volatile对象-类型为volatile限定的对象,或volatile对象的子对象,或const-volatile对象的可变子对象。出于优化目的(即在单个执行线程中,通过volatile限定类型的glvalue表达式进行的每次访问(读或写操作,成员函数调用等)都被视为可见的副作用)无法优化访问,也不会在volatile访问之前或之后的另一个可见副作用进行优化或重新排序,这使得volatile对象适合与信号处理程序进行通信,但不适合与其他执行线程进行通信,请参见std :: memory_order )。任何尝试通过非易失性glvalue(例如通过对非易失性类型的引用或指针)引用易失性对象的行为都会导致不确定的行为。

因此,易失性关键字具体是关于在glvalues上禁用编译器优化。 volatile关键字在这里可能会影响的唯一事情可能是return x,编译器可以使用该函数的其余部分执行任何操作。

编译器可以优化返回值的多少取决于在这种情况下允许编译器优化x的访问量(因为它不会重新排序任何东西,严格来说,是不删除返回表达式。是访问,但它是在对堆栈进行读写,应该可以简化。)因此,在我阅读该文章时,这是允许编译器优化的灰色区域,并且很容易就两者兼而有之方式。

侧面说明:在这些情况下,始终假设编译器将执行与您想要/需要的相反的操作。您应该禁用优化(至少对于该模块而言),或者尝试为所需的内容找到更定义的行为。 (这也是为什么单元测试如此重要的原因)。如果您认为它是一个缺陷,则应与C ++开发人员联系起来。


这一切仍然很难阅读,因此尝试包含我认为相关的内容,以便您可以自己阅读。

  

glvalue glvalue表达式是lvalue或xvalue。

     

属性:

     

可以将glvalue隐式转换为prvalue   左值到右值,数组到指针或函数到指针隐式   转换。 glvalue可能是多态的:   它标识的对象不一定是该对象的静态类型   表达。在值允许的情况下,glvalue可以具有不完整的类型   表达。


  

xvalue以下表达式是xvalue表达式:

     

函数调用或重载的运算符表达式,其返回值   类型是对对象的右值引用,例如std :: move(x); a [n],   内置下标表达式,其中一个操作数是数组rvalue;   a.m,对象表达式的成员,其中a为右值,m为a   非引用类型的非静态数据成员; a。* mp,指向的指针   对象表达式的成员,其中a是一个右值,mp是一个指针   数据成员;一种 ? b:c,某些条件的三元条件表达式   b和c(请参阅定义以了解详细信息);强制转换为右值   引用对象类型,例如static_cast(x);任何   在临时之后指定临时对象的表达式   物化。 (自C ++ 17起)属性:

     

与右值相同(如下)。与glvalue相同(如下)。特别喜欢   所有右值,x值绑定到右值引用,就像所有glvalue一样,   xvalue可能是多态的,非类xvalue可能是cv限定的。


  

lvalue以下表达式是lvalue表达式:

     

变量,函数或数据成员的名称,无论   类型,例如std :: cin或std :: endl。即使变量的类型是   右值引用,其名称组成的表达式为左值   表达;函数调用或重载的运算符表达式,   其返回类型是左值引用,例如std :: getline(std :: cin,   str),std :: cout <<,str1 = str2或++ it; a = b,a + = b,a == b和   所有其他内置赋值和复合赋值表达式;   ++ a和--a,内置的预递增和递减表达式;   * p,内置的间接表达式; a [n]和p [n],内置的下标表达式,除非a是数组右值(因为   C ++ 11); a.m,对象表达式的成员,除非m是a   成员枚举器或非静态成员函数,或者其中a是   rvalue和m是非引用类型的非静态数据成员; p-> m,   指针表达式的内置成员,除了m是成员   枚举器或非静态成员函数; a。* mp,指向的指针   对象表达式的成员,其中a是一个左值,mp是一个指针   数据成员; p-> * mp,指向指针成员的内置指针   表达式,其中mp是指向数据成员的指针; a,b,内置   逗号表达式,其中b是左值;一种 ? b:c,三元   一些b和c的条件表达式(例如,当两个都是左值时   类型相同,但请参阅定义以了解详细信息);字符串文字,   例如“你好,世界!”;转换为左值引用类型的表达式,   例如static_cast(x);函数调用或重载   运算符表达式,其返回类型为右值引用   功能;将值转换为对函数类型的引用的强制转换表达式,例如   作为static_cast(x)。 (自C ++ 11起)属性:

     

与glvalue相同(下)。左值的地址可以采用:&++ i 1   和&std :: endl是有效的表达式。可以使用可修改的左值   作为内置赋值和复合的左操作数   赋值运算符。左值可用于初始化左值   参考;这会将新名称与由标识的对象相关联   表达式。


作为规则

  

只要满足以下条件,就允许C ++编译器对程序进行任何更改:

     

1)在每个序列点,所有易失对象的值都是稳定的(先前的评估已完成,新的评估未开始)   (直到C ++ 11)   1)严格根据易失性对象所在的表达式的语义来进行访问(读取和写入)。特别是,它们不会相对于同一线程上的其他易失性访问而重新排序。   (自C ++ 11起)   2)在程序终止时,写入文件的数据与在执行程序时完全一样。   3)在程序等待输入之前,将显示发送到交互式设备的提示文本。   4)如果支持ISO C编译指示#pragma STDC FENV_ACCESS并将其设置为ON,则可以保证浮点算术运算符和函数可以观察到浮点环境的变化(浮点异常和舍入模式)。调用就像执行一样,除了   除强制转换和赋值以外的任何浮点表达式的结果可能具有与该表达式的类型不同的浮点类型的范围和精度(请参见FLT_EVAL_METHOD)   尽管如此,可以将任何浮点表达式的中间结果计算为无穷大的范围和精度(除非#pragma STDC FP_CONTRACT为OFF)


如果您想阅读规格,我相信这些是您需要阅读的。

  

参考

     

C11标准(ISO / IEC 9899:2011):   6.7.3类型限定符(p:121-123)

     

C99标准(ISO / IEC 9899:1999):   6.7.3类型限定符(p:108-110)

     

C89 / C90标准(ISO / IEC 9899:1990):   3.5.3类型限定符

答案 5 :(得分:-1)

我认为我从未见过使用volatile的局部变量,该变量不是指向volatile的指针。如:

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

我所知道的volatile的其他唯一情况是使用在信号处理程序中编写的全局变量。那里没有指针。或访问链接描述文件中定义的符号,这些符号位于与硬件相关的特定地址。

在这里推理为什么优化会改变可观察的效果要容易得多。但是,相同规则适用于本地volatile变量。编译器必须表现得好像可以访问x并且无法对其进行优化。