是否允许编译器优化掉局部易失性变量



是否允许编译器对此进行优化(根据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()进行了优化。此转换有效吗?如果没有,哪个步骤无效?

否。对volatile对象的访问被认为是可观察的行为,就像I/O一样,局部和全局之间没有特别的区别。

对一致性实施的最低要求是:

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

[…]

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

N3690,[介绍执行],¶8

如何观察到这一点超出了标准的范围,直接属于实现特定的领域,就像I/O和对全局volatile对象的访问一样。volatile的意思是"你认为你知道这里发生的一切,但事实并非如此;相信我,做这些事情不要太聪明,因为我在你的程序中用你的字节做我的秘密"。这实际上在[dcl.type.cv]¶7:中进行了解释

[注意:volatile是实现的一个提示,以避免涉及对象的激进优化因为对象的值可能会通过实现无法检测到的方式进行更改。此外对于某些实现,volatile可能表示需要特殊的硬件指令才能访问对象。有关详细语义,请参见1.9。一般来说,volatile的语义在C++中与在C中相同

这个循环可以通过假设规则进行优化,因为它没有可观察的行为:

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。

尽管我完全理解volatile意味着可观察的I/O,但我不同意大多数人的意见。

如果你有这个代码:

{
volatile int x;
x = 0;
}

我相信编译器可以下优化它,就好像规则假设

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

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

基本原理是,由于标准#2,您无论如何都无法观察到差异。

但是,在编译器中,标准#2可能不满足!编译器可能会尝试为您提供从"外部"观察volatile变量的额外保证,例如通过分析堆栈。在这种情况下,行为真的可观察的,所以它不能被优化掉。

现在的问题是,下面的代码与上面的代码有什么不同吗?

{
volatile int x = 0;
}

我相信我在Visual C++中观察到了优化方面的不同行为,但我不完全确定是基于什么原因。初始化可能不算作"访问"?我不确定。如果你感兴趣的话,这可能值得单独提问,但除此之外,我相信答案如上所述。

我只想添加一个关于as-if规则和volatile关键字的详细引用。(在这些页面的底部,按照"另请参阅"one_answers"参考资料"追溯到原始规范,但我发现cppreference.com更容易阅读/理解。)

特别是,我想让你阅读这一部分

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

因此volatile关键字特别是是关于禁用glvalues上的编译器优化。volatile关键字在这里唯一能影响的可能是return x,编译器可以对函数的其余部分为所欲为。

编译器能在多大程度上优化返回取决于在这种情况下允许编译器优化x的访问量(因为它没有对任何东西进行重新排序,严格地说,也没有删除返回表达式。有访问,但它是对堆栈的读写,这应该能够简化。),这是允许编译器优化多少的灰色地带,可以很容易地进行双向论证。

附带说明:在这种情况下,总是假设编译器会做与您想要/需要的相反的事情。您应该禁用优化(至少针对该模块),或者尝试为您想要的内容找到更明确的行为。(这也是单元测试如此重要的原因)如果你认为这是一个缺陷,你应该向C++的开发人员提出。


这一切仍然很难阅读,所以试着包括我认为相关的内容,这样你就可以自己阅读了。

glvalue glvalue表达式是lvalue或xvalue。

属性:

glvalue可以隐式转换为prvalue隐式的左值到右值、数组到指针或函数到指针转变glvalue可能是多态的:它标识的对象不一定是表示glvalue在表示


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

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

与右值相同(如下)。与glvalue(如下)相同。特别是,像所有右值、xvalues都绑定到右值引用,并且像所有glvalues一样,x值可以是多态的,并且非类x值可以被cv限定。


左值以下表达式是左值表达式:

变量、函数或数据成员的名称,与类型,例如std::cin或std::endl。即使变量的类型为右值引用,由其名称组成的表达式是一个左值表示函数调用或重载运算符表达式,其返回类型为左值引用,例如std::getline(std::cin,str),std::cout<lt;1,str1=str2,或++it;a=b,a+=b,a%=b,以及所有其他内置赋值表达式和复合赋值表达式;++a和-a,内置的预增量和预减量表达式;*p、 内置的间接表达式;a[n]和p[n],内置的下标表达式,除非a是数组右值(因为C++11);a.m,对象表达式的成员,除非m是成员枚举器或非静态成员函数,或者其中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(如下)相同。左值的地址可以是:&+i1和&std::endl是有效的表达式。可以使用可修改的左值作为内置赋值和复合的左侧操作数赋值运算符。左值可用于初始化左值参考这会将新名称与标识为表达式。


如同规则

只要以下情况保持不变,C++编译器就可以对程序执行任何更改:

1) 在每个序列点,所有易失性对象的值都是稳定的(以前的评估已经完成,新的评估没有开始)(直到C++11)1) 对易失性对象的访问(读取和写入)严格按照它们所在的表达式的语义进行。特别是,相对于同一线程上的其他易失性访问,它们不会被重新排序。(自C++11起)2) 在程序终止时,写入文件的数据与程序在写入时执行的数据完全相同。3) 在程序等待输入之前,将显示发送到交互式设备的提示文本。4) 如果支持ISO C pragma#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型号限定符

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

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

…从而使fn()返回一个非零值。

我想我从未见过使用volatile的局部变量不是指向volatile。如:

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

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

更容易解释为什么优化会改变可观察到的效果。但同样的规则也适用于局部可变变量。编译器必须表现得好像对x的访问是可观察的,并且无法对其进行优化。

最新更新