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


79

是否允许编译器对此进行优化(根据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()为优化版​​本。此转换有效吗?如果不是,哪一步无效?


1
评论不作进一步讨论;此对话已转移至聊天


@philipxy:这与“可以产生什么”无关。关于是否允许转换。因为,如果不允许,则它不能产生转换后的版本。
geza

该标准为程序定义了对实现必须遵守的volatile和其他可观察对象的访问序列。但是访问易失性手段的方式是实现定义的。因此,问一个实现可以产生什么是毫无意义的—它产生了定义要产生的东西。给定一些实现行为的描述,您可以寻求另一个您更喜欢的实现。但是您需要一个开始。也许您实际上对标准的可观察规则感兴趣,因为除了必须满足标准和实现规则之外,代码生成无关紧要。
philipxy

1
@philipxy:我将澄清有关标准的问题。这些问题通常暗示了这一点。我对标准的内容很感兴趣。
geza

Answers:


63

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

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

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

[...]

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

N3690,[介绍执行],¶8

如何准确,这是观察到超出标准范围,并径直落入实现特定的领土,正是因为I / O,并获得全球volatile对象。volatile意思是“您认为您知道这里发生的所有事情,但事实并非如此;请相信我,并且不要太聪明,因为我正在您的程序中使用您的字节来做我的秘密工作”。这实际上在[dcl.type.cv]¶7中有解释:

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


2
由于这是最受关注的问题,并且该问题通过编辑得到扩展,因此最好编辑此答案以讨论新的优化示例。
海德

正确的是“是”。这个答案不能清楚地将抽象的机器可观察值与生成的代码区分开。后者是实现定义的。例如,为了与给定的调试器一起使用,可以保证易失性对象位于内存和/或寄存器中;例如,通常在相关目标体系结构下,可以保证在编译指示指定的特殊存储位置处对易失性对象进行写入和/或读取。该实现定义了如何在代码中反映访问。它决定“何时可以通过实现无法检测到的方式更改对象”的方式和时间。(请参阅我对这个问题的评论。)
philipxy

12

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

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。


那么,您是说问题中的最终版本xor ax, ax(在哪里ax被认为volatile x是有效的)?IOW,您对这个问题的回答是什么?
海德

@hyde:我读的问题是“可以消除变量”,我的回答是“否”。对于提出了是否可以将volatile放入寄存器的问题的特定x86实现,我不确定。即使将其简化为xor ax, ax,即使看起来毫无用处,该操作码也无法消除,也无法将其合并。在我的循环示例中,编译后的代码必须执行xor ax, axn次才能满足可观察到的行为规则。希望编辑可以回答您的问题。
rici

是的,这个问题在编辑后已经扩展了很多,但是既然您在编辑后回答了,我想这个答案应该涵盖了新内容……
hyde

2
@hyde:实际上,我确实在基准测试中以这种方式使用了volatile,以避免编译器优化掉一个循环,否则该循环将不起作用。所以我真的希望我对此是正确的:=)
rici

该标准说,对volatile对象的操作本身就是一种副作用。一个实现可以以一种不要求它们生成任何实际CPU指令的方式来定义其语义,但是访问易失性对象的循环会产生副作用,因此不适合省略。
超级猫

10

尽管完全理解这volatile意味着可以观察到的I / O ,但我还是反对大多数意见。

如果您有此代码:

{
    volatile int x;
    x = 0;
}

我相信编译器可以根据假设规则优化它,前提是:

  1. volatile否则该变量就不会通过指针从外部可见(这显然不是问题,因为在给定范围内没有这样的东西)

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

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

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

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

{
    volatile int x = 0;
}

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


6

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

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

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


1
或者,您可以通过在中设置断点来更方便地使用调试器来执行此操作fn()。使用volatile会产生类似于gcc -O0该变量的代码生成:在每个C语句之间溢出/重新加载。(-O0仍然可以在一个语句中合并多个访问,而不会破坏调试器的一致性,但volatile不允许这样做。)
Peter Cordes

或更容易地,使用调试器:)但是,哪个标准说该变量需要可观察?我的意思是,实现可以选择它必须是可观察的。另一个人可以说,这是不可观察的。后者是否违反标准?也许不会。它不是标准规定的,本地可变变量如何才能完全观察到。
geza

甚至,“可观察”是什么意思?是否应该将其放在堆栈上?如果登记册成立x怎么办?如果在x86-64上xor rax, rax保持零(我的意思是,返回值寄存器保持x),那当然可以由调试器轻松观察/修改(即,x存储在中的调试符号信息rax)。这是否违反标准?
geza

2
-1任何呼叫fn()都可以内联。有了MSVC 2017和默认发布模式,它就是。这样就没有“fn()功能内”。无论如何,由于变量是自动存储的,因此没有“可预测的偏移量”。
干杯和健康。-Alf

1
0 @berendi:是的,你是对的,我是错的。抱歉,在这方面我的早晨不好(两次错)。IMO仍然毫无争议地争论编译器如何支持通过其他软件的访问,因为无论,它都可以做到这一点volatile,并且因为volatile不强制它提供这种支持。因此,我删除了弃权票(我错了),但我没有弃权票,因为我认为这种推理方式并不清楚。
干杯和健康。

6

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

特别是,我希望您阅读本节

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

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

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

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


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

glvalue glvalue表达式是lvalue或xvalue。

特性:

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


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

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

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


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

变量,函数或数据成员的名称,无论类型如何,例如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起);am,对象表达式的成员,除非m是成员枚举数或非静态成员函数,或其中a是右值,m是非引用类型的非静态数据成员;或者 p-> m,指针表达式的内置成员,除非m是成员枚举数或非静态成员函数;a。* mp,对象表达式成员的指针,其中a是左值,mp是数据成员的指针;p-> * mp,指向指针表达式成员的内置指针,其中mp是指向数据成员的指针;a,b是内置的逗号表达式,其中b是左值;一种 ?b:c,某些b和c的三元条件表达式(例如,当它们都是相同类型的左值时,请参见定义);字符串文字,例如“ Hello,world!”;转换为左值引用类型的表达式,例如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,


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

参考文献

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类型限定符


根据标准,这可能不正确,但是任何在执行过程中依靠堆栈被其他东西触摸的人都应该停止编码。我认为这是一个标准缺陷。
meneldal

1
@meneldal:这太宽泛了。_AddressOfReturnAddress例如,使用涉及分析堆栈。人们出于正当的理由对堆栈进行分析,但这不一定是因为函数本身依赖于堆栈是否正确。
user541686

1
glvalue在这里:return x;
geza

@geza对不起,这很难读。是因为x是变量的glvalue吗?另外,对于“无法优化”,是否意味着编译器根本无法优化,或者不能通过更改表达式来优化?(这听起来像编译器仍在此处进行优化,因为它们没有维护访问的顺序,并且表达式仍在解析中,只是以一种更优化的方式进行)我可以看到在没有更好地理解眼镜。
Tezra

这是您自己的答案的引文:)“以下表达式是左值表达式:变量的名称...”
geza

-1

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

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

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

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


3
但这不是局部的volatile变量,它是指向知名地址处的volatile int的局部非易失性指针。
没用

这使得更容易推理出正确的行为。如前所述,访问变量的规则对于局部变量和取消引用变量的指针都是相同的。
Goswin von Brederlow

我只是在回答您的答案的第一句话,这似乎表明x您的代码中是一个“局部易失性变量”。不是。
没用

当int fn(const volatile int参数)没有编译时,我很生气。
约书亚

4
编辑使您的答案没有错,但是根本无法回答问题。这是的教科书用例volatile,与它作为本地语言无关。它也可能static volatile int *const x = ...在全球范围内,您所说的一切仍然完全相同。这就像了解该问题所必需的额外背景知识一样,我想也许不是每个人都知道,但这不是真正的答案。
彼得·科德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.