挥发性物质未按预期工作

Volatile not working as expected

本文关键字:工作 挥发性      更新时间:2023-10-16

考虑以下代码:

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

这更接近你的预期。。。。我甚至比以往任何时候都更困惑

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()基本上变为空。

我想说,在这种情况下,编译器过于同意(错误地IMHO)决定,由于没有使用"a",因此对其进行操作是不必要的,并忽略了volatile成员。让"a"本身变得不稳定可以得到你想要的,但由于我没有一个编译器来复制它,我不能肯定。

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

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

这也指向你所看到的行为是一个编译器问题。

最后,如果您将"a"设为全局变量,编译器不太愿意认为它未使用并将其删除,这在一定程度上是可以理解的。默认情况下,全局变量是外部变量,因此不可能仅通过查看主函数就说全局"a"未使用。其他一些编译单元(.cpp文件)可能正在使用它。

GCC关于Volatile访问的页面深入了解了它的工作原理:

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

在C标准中:

§5.1.2.3

2访问易失性对象、修改对象、修改文件,或者调用执行任何这些操作的函数都是侧效应11),它们是执行环境。评估一个表达可能会产生副作用效果。在调用的执行序列中的某些指定点序列点,之前评估的所有副作用都应完整,后续评估的副作用都不应存在发生。(序列点摘要见附件C。)

3在抽象机器中,所有表达式都按照指定进行求值通过语义。实际实施不需要评估一个表达式,如果它可以推断出它的值没有被使用并且没有产生所需的副作用(包括调用函数或访问易失性对象)。

[…]

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

  • 在序列点上,易失性对象是稳定的,因为之前的访问已经完成,而后续的访问尚未完成发生[…]

我选择C标准是因为语言更简单,但C++中的规则基本相同。请参阅"好像"规则

现在在我的机器上,-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()的调用是否也一样。