易失性不按预期工作

时间:2016-05-07 01:38:52

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

考虑以下代码:

struct A{ 
  volatile int x;
  A() : x(12){
  }
};

A foo(){
  A ret;
  //Do stuff
  return ret;
}

int main()
{
  A a;
  a.x = 13;
  a = foo();
}

使用g++ -std=c++14 -pedantic -O3我得到这个程序集:

foo():
        movl    $12, %eax
        ret
main:
        xorl    %eax, %eax
        ret

根据我的估计,变量x应写入至少三次(可能是四次),但它甚至不会写一次(函数foo甚至不均匀)叫!)

inline关键字添加到foo时更糟糕的是结果:

main:
        xorl    %eax, %eax
        ret

我认为volatile意味着即使编译器看不到读/写点,每一次读或写都必须

这里发生了什么?

更新

A a;的声明放在main之外:

A a;
int main()
{  
  a.x = 13;
  a = foo();
}

生成此代码:

foo():
        movl    $12, %eax
        ret
main:
        movl    $13, a(%rip)
        xorl    %eax, %eax
        movl    $12, a(%rip)
        ret
        movl    $12, a(%rip)
        ret
a:
        .zero   4

这更接近你的期望......我甚至更加困惑

2 个答案:

答案 0 :(得分:2)

Visual C ++ 2015不会优化分配:

A a;
mov         dword ptr [rsp+8],0Ch  <-- write 1
a.x = 13;
mov         dword ptr [a],0Dh      <-- write2
a = foo();
mov         dword ptr [a],0Ch      <-- write3
mov         eax,dword ptr [rsp+8]  
mov         dword ptr [rsp+8],eax  
mov         eax,dword ptr [rsp+8]  
mov         dword ptr [rsp+8],eax  
}
xor         eax,eax  
ret  

/ O2(最大化速度)和/ Ox(完全优化)都会发生同样的情况。

使用-O2和-O3

也可以通过gcc 3.4.4保持易失性写入
_main:
pushl   %ebp
movl    $16, %eax
movl    %esp, %ebp
subl    $8, %esp
andl    $-16, %esp
call    __alloca
call    ___main
movl    $12, -4(%ebp)  <-- write1
xorl    %eax, %eax
movl    $13, -4(%ebp)  <-- write2
movl    $12, -8(%ebp)  <-- write3
leave
ret

使用这两个编译器,如果我删除volatile关键字,main()基本上是空的。

我说你有一个案例,编译过于激烈(并且错误的恕我直言)决定,因为&#39; a&#39;不使用,对它的操作不是&#39;必要的,忽略了不稳定的成员。制作&#39; a&#39;本身挥发性可以得到你想要的东西,但由于我没有一个能够重现这一点的编译器,我无法肯定地说。

最后(虽然这是微软特定的),https://msdn.microsoft.com/en-us/library/12a04hfd.aspx说:

  

如果struct成员被标记为volatile,那么volatile将传播到整个结构。

这也指向您看到的编译器问题。

最后,如果你做了一个&#39;一个全局变量,编译器不太愿意将其视为未使用并丢弃它,这有点可以理解。默认情况下,全局变量是extern,所以不可能说全局变量是全局变量。仅通过查看主要功能就不习惯了。其他一些编译单元(.cpp文件)可能正在使用它。

答案 1 :(得分:0)

GCC的Volatile access页面提供了一些有关其工作原理的信息:

  

该标准鼓励编译器避免对易失性对象的访问进行优化,但是将其实现定义为构成易失性访问的内容。最低要求是在序列点处对易失性对象的所有先前访问已经稳定并且没有发生后续访问。因此,实现可以自由地重新排序和组合在序列点之间发生的易失性访问,但是对于跨序列点的访问不能这样做。 volatile的使用不允许您违反在两个序列点之间多次更新对象的限制。

在C标准中:

  

§5.1.2.3

     

2访问易失性对象,修改对象,修改文件,   或调用执行任何这些操作的函数都是方   效果 11)这是状态的变化   执行环境。评价表达可能会产生副作用   效果。在执行序列中的某些指定点处调用   序列点,先前评估的所有副作用应完整,后续评估的副作用不得有   发生在。 (序列点的摘要见附件C.)

     

3在抽象机器中,所有表达式都按指定的方式进行计算   通过语义。实际的实现不需要评估部分内容   表达式,如果它可以推断出它的值没有被使用而且没有   产生所需的副作用(包括通过调用a引起的任何副作用)   功能或访问易失性对象。)

     

[...]

     

5符合实施的最低要求是:

     
      
  • 在序列点处,易失性对象在先前访问完成且后续访问尚未完成的意义上是稳定的   发生了。 [...]
  •   

我选择了C标准,因为语言比较简单,但规则在C ++中基本相同。请参阅“as-if”规则。

现在,在我的计算机上,-O1并未优化对foo()的调用,所以让我们使用-fdump-tree-optimized来查看差异:

-O1

*[definition to foo() omitted]*

;; Function int main() (main, funcdef_no=4, decl_uid=2131, cgraph_uid=4, symbol_order=4) (executed once)

int main() ()
{
  struct A a;

  <bb 2>:
  a.x ={v} 12;
  a.x ={v} 13;
  a = foo ();
  a ={v} {CLOBBER};
  return 0;
} 

-O3

*[definition to foo() omitted]*

;; Function int main() (main, funcdef_no=4, decl_uid=2131, cgraph_uid=4, symbol_order=4) (executed once)

int main() ()
{
  struct A ret;
  struct A a;

  <bb 2>:
  a.x ={v} 12;
  a.x ={v} 13;
  ret.x ={v} 12;
  ret ={v} {CLOBBER};
  a ={v} {CLOBBER};
  return 0;
}

gdb在两种情况下都显示a最终已经过优化,但我们担心foo()。转储向我们显示GCC重新排序访问,因此甚至不需要foo(),随后优化了main()中的所有代码。这是真的吗?让我们看一下-O1的汇编输出:

foo():
        mov     eax, 12
        ret
main:
        call    foo()
        mov     eax, 0
        ret

这基本上证实了我上面所说的。一切都被优化了:唯一的区别是对foo()的调用是否也是如此。