是否允许编译器对此进行优化(根据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
.但是现在,为什么要停在这里?由于edx
和eax
均为零,我们可以只说:
xor eax,eax // eax is the return, and x as well
ret
我们转变fn()
为优化版本.这种转变有效吗?如果没有,哪一步无效?
1> Matteo Itali..:
对volatile
对象的访问被认为是可观察的行为,与I/O完全相同,本地和全局之间没有特别的区别.
符合实施的最低要求是:
volatile
严格按照抽象机的规则评估对象的访问.
[...]
这些统称为程序的可观察行为.
N3690,[intro.execution],8
如何准确,这是观察到的是标准的范围之内,直落入实现特定的领土,正是因为I/O,并获得全球volatile
对象.volatile
意思是"你认为你知道这里发生的一切,但它不是那样的;相信我并做这些事情而不是太聪明,因为我在你的程序中用你的字节做我的秘密事情".这实际上是在[dcl.type.cv]7中解释的:
[注意:volatile
是对实现的暗示,以避免涉及对象的激进优化,因为对象的值可能会被实现无法检测到的方式更改.此外,对于某些实现,volatile可能指示访问对象需要特殊的硬件指令.有关详细语义,请参见1.9.一般来说,volatile的语义在C++中与在C中的相同. - 最后的注释]
由于这是最受欢迎的问题,并且通过编辑扩展了问题,因此编辑此答案以讨论新的优化示例将会很愉快.
2> rici..:
这个循环可以通过as-if规则进行优化,因为它没有可观察的行为:
for (unsigned i = 0; i
这一个不能:
for (unsigned i = 0; i
第二个循环在每次迭代时都会执行某些操作,这意味着循环需要O(n)时间.我不知道常量是什么,但是我可以测量它然后我有一种忙碌循环的方式(或多或少)已知的时间量.
我可以这样做,因为标准规定必须按顺序进入挥发物.如果编译器决定在这种情况下标准不适用,我想我有权提交错误报告.
如果编译器选择放入looped
寄存器,我想我对此没有好的论据.但是,对于每次循环迭代,它仍然必须将该寄存器的值设置为1.
@hyde:事实上,我确实在基准测试中以这种方式使用了volatile,以避免编译器优化掉一个否则什么都不做的循环.所以我真的希望我这是对的:=)
3> Mehrdad..:
尽管充分理解这volatile
意味着可观察到的I/O ,但我不赞成多数意见.
如果你有这个代码:
{
volatile int x;
x = 0;
}
我相信编译器可以在as-if规则下对其进行优化,假设:
该volatile
变量不会通过例如指针在外部可见(这显然不是问题,因为在给定范围内没有这样的事情)
编译器没有为您提供外部访问它的机制 volatile
理由是,由于标准#2,你无论如何都无法观察到差异.
但是,在编译器中,可能不满足条件#2!编译器可能会尝试为您提供有关volatile
从"外部" 观察变量的额外保证,例如通过分析堆栈.在这种情况下,行为确实是可观察的,因此无法进行优化.
现在的问题是,以下代码是否与上述不同?
{
volatile int x = 0;
}
我相信我已经在Visual C++中观察到了与优化有关的不同行为,但我不完全确定基于什么理由.初始化可能不算作"访问"?我不确定.如果您感兴趣,这可能值得一个单独的问题,但我相信答案就像我上面解释的那样.
4> berendi - pr..:
从理论上讲,中断处理程序可以
检查返回地址是否属于该fn()
函数.它可以通过检测或附加的调试信息访问符号表或源行号.
然后更改值x
,该值将存储在堆栈指针的可预测偏移量中.
...因此fn()
返回非零值.
** - 1**任何对`fn()`的调用都可以内联.使用MSVC 2017和默认发布模式,它是.然后没有"在`fn()`函数内".无论如何,由于变量是自动存储,因此没有"可预测的偏移".
5> Tezra..:
我只是要为as-if规则和volatile关键字添加详细的参考.(在这些页面的底部,按照"see also"和"References"追溯到原始规格,但我发现cppreference.com更容易阅读/理解.)
特别是,我想让你阅读这一节
volatile对象 - 类型为volatile限定的对象,或volatile对象的子对象,或const-volatile对象的可变子对象.通过volatile限定类型的glvalue表达式进行的每次访问(读取或写入操作,成员函数调用等)都被视为可见的副作用,用于优化(即,在单个执行线程内,volatile访问无法优化或重新排序,具有在易失性访问之前排序或排序的另一个可见副作用.这使得易失性对象适合与信号处理程序通信,但不适用于另一个执行线程,请参阅std :: memory_order ).任何通过非易失性glvalue引用易失性对象的尝试(例如通过引用或指向非易失性类型的指针)都会导致未定义的行为.
因此,volatile关键字专门用于禁用glvalues上的编译器优化.volatile关键字可能影响的唯一可能是return x
,编译器可以使用函数的其余部分执行任何操作.
编译器可以优化返回的程度取决于在这种情况下允许编译器优化x访问的程度(因为它不重新排序任何内容,严格来说,不是删除返回表达式.有访问权限) ,但是它正在读取和写入堆栈,应该能够简化.)因此,当我阅读它时,这是允许编译器优化多少的灰色区域,并且可以很容易地双向争论.
旁注:在这些情况下,始终假设编译器将执行您想要/需要的相反操作.您应该禁用优化(至少对于此模块),或尝试根据需要查找更明确的行为.(这也是单元测试如此重要的原因)如果您认为它是一个缺陷,您应该与C++的开发人员一起提出.
这一切仍然很难阅读,所以试图包括我认为相关的内容,以便您自己阅读.
glvalue glvalue表达式是lvalue或xvalue.
属性:
glvalue可以隐式转换为具有左值到右值,数组到指针或函数到指针隐式转换的prvalue.glvalue可以是多态的:它识别的对象的动态类型不一定是表达式的静态类型.glvalue可以具有不完整的类型,表达式允许.
xvalue以下表达式是xvalue表达式:
函数调用或重载的运算符表达式,其返回类型是对象的右值引用,例如std :: move(x); a [n],内置的下标表达式,其中一个操作数是一个数组rvalue; am,对象表达式的成员,其中a是rvalue,m是非引用类型的非静态数据成员; a.*mp,指向对象表达式成员的指针,其中a是rvalue,mp是指向数据成员的指针; 一个 ?b:c,某些b和c的三元条件表达式(详见定义); 一个转换表达式,用于对对象类型的rvalue引用,例如static_cast(x); 临时实现后,指定临时对象的任何表达式.(自C++ 17以来)属性:
与右值相同(下图).与glvalue相同(下图).特别是,像所有rvalues一样,xvalues绑定到rvalue引用,并且像所有glvalues一样,xvalues可能是多态的,非类xvalues可能是cv限定的.
左值以下表达式是左值表达式:
变量,函数或数据成员的名称,无论类型如何,例如std :: cin或std :: endl.即使变量的类型是右值引用,由其名称组成的表达式也是左值表达式; 函数调用或重载的运算符表达式,其返回类型是左值引用,例如std :: getline(std :: cin,str),std :: cout <<1,str1 = str2或++ it; a = b,a + = b,a%= b,以及所有其他内置赋值和复合赋值表达式; ++ a和--a,内置的预增量和预减量表达式;*p,内置的间接表达式; a [n]和p [n],内置的下标表达式,除非a是数组rvalue(自C++ 11起); am,对象表达式的成员,除非m是成员枚举器或非静态成员函数,或者a是rvalue,m是非引用类型的非静态数据成员; p-> m,指针表达式的内置成员,除非m是成员枚举器或非静态成员函数; a.*mp,指向对象表达式成员的指针,其中a是左值,mp是指向数据成员的指针; p - >*mp,指向表达式成员的内置指针,其中mp是指向数据成员的指针; a,b,内置逗号表达式,其中b是左值; 一个 ?b:c,某些b和c的三元条件表达式(例如,当两者都是相同类型的左值时,但参见详细定义); 字符串文字,例如"Hello,world!"; lvalue引用类型的强制转换表达式,例如static_cast(x); 函数调用或重载的运算符表达式,其返回类型是函数的右值引用; 一个强制转换表达式,用于对函数类型的rvalue引用,例如static_cast(x).(自C++ 11以来)属性:
与glvalue相同(下图).可以采用左值的地址:&++ i 1
和&std :: endl是有效的表达式.可修改的左值可以用作内置赋值和复合赋值运算符的左手操作数.左值可用于初始化左值参考; 这会将新名称与表达式标识的对象相关联.
as-if规则
只要满足以下条件,C++编译器就可以对程序执行任何更改:
1)在每个序列点,所有易失性对象的值都是稳定的(之前的评估是完整的,新的评估没有开始)(直到C++ 11)1)对volatile对象的访问(读取和写入)严格按照语义进行他们出现的表达方式.特别是,它们不会针对同一线程上的其他易失性访问进行重新排序.(从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类型限定符