C ++中忽略了易失性说明符

时间:2015-04-25 18:03:07

标签: c++ c++11 volatile

我对C ++很陌生,最近我遇到了一些关于变量volatile意味着什么的信息。据我所知,这意味着对变量的读取或写入永远不会优化不存在。

然而,当我声明一个不是1,2,4,8字节大的volatile变量时会出现一种奇怪的情况:编译器(启用了C ++ 11的gnu)似乎忽略了{{ 1}}说明符

volatile

他们的时间是

  • #define expand1 a, a, a, a, a, a, a, a, a, a #define expand2 // ten expand1 here, expand3 to expand5 follows // expand5 is the equivalent of 1e+005 a, a, .... struct threeBytes { char x, y, z; }; struct fourBytes { char w, x, y, z; }; int main() { // requires ~1.5sec foo<int>(); // doesn't take time foo<threeBytes>(); // requires ~1.5sec foo<fourBytes>(); } template<typename T> void foo() { volatile T a; // With my setup, the loop does take time and isn't optimized out clock_t start = clock(); for(int i = 0; i < 100000; i++); clock_t end = clock(); int interval = end - start; start = clock(); for(int i = 0; i < 100000; i++) expand5; end = clock(); cout << end - start - interval << endl; } :~1.5s
  • foo<int>():0

我用不同的变量(用户定义与否)测试了1到8个字节,只有1,2,4,8需要时间才能运行。这是一个只存在于我的设置中的错误,还是foo<threeBytes>()对编译器的请求而不是绝对的?

PS四字节版本总是占用其他时间的一半时间,也是混淆的来源

3 个答案:

答案 0 :(得分:5)

结构版本可能会被优化,因为编译器意识到没有副作用(没有读取或写入变量a),无论volatile如何。你基本上有一个no-op,a;,所以编译器可以做任何你喜欢的事情;它不是强制展开循环或优化它,所以volatile在这里并不重要。对于int s,似乎没有优化,但这与volatile的用例一致:当您拥有时,您应该期望非优化 循环中可能的“访问对象”(即读取或写入)。然而,构成“访问对象”的内容是实现定义的(尽管大部分时间它遵循常识),请参见底部的 EDIT 3

这里的玩具示例:

#include <iostream>
#include <chrono>

int main()
{
    volatile int a = 0;

    const std::size_t N = 100000000;

    // side effects, never optimized
    auto start = std::chrono::steady_clock::now();
    for (std::size_t i = 0 ; i < N; ++i)
        ++a; // side effect (write)
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              <<  " ms" << std::endl;

    // no side effects, may or may not be optimized out
    start = std::chrono::steady_clock::now();
    for (std::size_t i = 0 ; i < N; ++i)
        a; // no side effect, this is a no-op
    end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              <<  " ms" << std::endl;
}

修改

对于标量类型,no-op实际上没有优化,正如您在this minimal example中看到的那样。但是对于struct 优化了。在我链接的示例中,clang没有优化代码而没有优化,但使用-O3优化了两个循环。 gcc不优化循环,也不优化,但仅优化第一个循环并进行优化。

编辑2

clang发出警告:warning: expression result unused; assign into a variable to force a volatile load [-Wunused-volatile-lvalue]。所以我最初的猜测是正确的,编译器可以优化no-ops,但它不是强制的。为什么它为struct s而不是标量类型是我不理解的东西,但它是编译器的选择,并且它是标准兼容的。出于某种原因,只有当no-op为struct时才会发出此警告,并且当它是标量类型时不会发出警告。

另请注意,您没有“读/写”,只有非操作,因此您不应期待volatile的任何内容。

编辑3

从黄金书(C ++标准)

7.1.6.1/8 cv-qualifiers [dcl.type.cv]

  

什么构成对具有volatile限定的对象的访问权限   type是实现定义的。 ...

因此,编译器需要决定何时优化循环。在大多数情况下,它遵循常识:读取或写入对象时。

答案 1 :(得分:4)

这个问题比第一次出现时更有趣(对于某些定义&#34;有趣&#34;)。看起来您发现了编译器错误(或故意不合格),但它并不是您期望的错误。

根据标准,您的foo个调用之一具有未定义的行为,另外两个调用格式不正确。我先说明应该发生什么;休息后可以找到相关的标准报价。出于我们的目的,我们可以只分析给定a, a, a;的简单表达式语句volatile T a;

此表达式语句中的

a, a, a是一个废弃值表达式([stmt.expr] / p1)。表达式a, a, a的类型是右操作数的类型,即 id-expression avolatile T;由于a是左值,表达式a, a, a([expr.comma] / p1)也是如此。因此,这个表达式是一个volatile限定类型的左值,它是一个逗号表达式,其中右操作数是这些表达式中的一个&#34; - 特别是 id-expression - 因此[expr] / p11要求将左值到右值的转换应用于表达式a, a, a。类似地,在a, a, a内,左表达式a, a也是一个废弃值表达式,在该表达式中,左表达式a也是一个废弃值表达式;类似的逻辑表明,[expr] / p11要求将左值到右值的转换应用于表达式a, a的结果和表达式a(最左边的一个)的结果。

如果T是类类型(threeBytesfourBytes),则应用左值到右值转换需要通过volatile lvalue创建临时初始化{{ 1}}([conv.lval] / p2)。但是,隐式声明的复制构造函数总是通过非易失性引用([class.copy] / p8)获取其参数;这样的引用不能绑定到volatile对象。因此,该计划形成不良。

如果aT,则应用左值到右值转换会产生int中包含的值。但是,在您的代码中,a永远不会被初始化;因此,此评估会生成一个不确定的值,并且每[dcl.init] / p12会导致不确定的行为。

标准报价如下。全部来自C ++ 14:

[EXPR] / P11:

  

在某些情况下,表达式仅出现其副作用。   这种表达式称为丢弃值表达式。该   计算表达式并丢弃其值。该   数组到指针(4.2)和函数到指针(4.3)标准   转化不适用。左值到右值的转换(4.1)是   当且仅当表达式是glvalue时才应用   volatile限定类型,它是以下之一:

     
      
  • expression ),其中表达式是其中一个表达式,
  •   
  • id-expression (5.1.1),
  •   
  • [省略了几个不适用的子弹]或
  •   
  • 逗号表达式(5.18),其中右操作数是这些表达式之一。
  •   
     

[注意:使用重载运算符会导致函数调用;该   以上仅涵盖具有内置含义的运营商。如果左值是   类类型,它必须有一个volatile的复制构造函数来初始化   临时是左值到右值转换的结果。 - 端   注意]

[expr.comma] / P1:

  

用逗号分隔的一对表达式从左到右进行评估;   左表达式是一个废弃值表达式(第5条)[...]类型   结果的值是右操作数的类型和值;   结果与右操作数[...]具有相同的值类别。

[stmt.expr] / P1:

  

表达式语句的格式为

a
     

表达式是废弃值表达式(第5条)。

[conv.lval] / P1-2:

  

1非函数非数组类型expression-statement: expression_opt; 的glvalue(3.10)可以是   转换为prvalue。如果T是一个不完整的类型,那么该程序就是   需要这种转换是不正确的。如果T是非类   类型,prvalue的类型是T的cv非限定版本。   否则,prvalue的类型是T.

     

2 [这里不相关的一些特殊规则]在所有其他情况下,   转换结果根据以下确定   规则:

     
      
  • [不适用的子弹省略]
  •   
  • 否则,如果T具有类类型,则转换复制 - 从glvalue初始化类型为T的临时值,并且结果为   转换是临时的prvalue。
  •   
  • [不适用的子弹省略]
  •   
  • 否则,glvalue指示的对象中包含的值是prvalue结果。
  •   

[dcl.init] / P12:

  

如果没有为对象指定初始化程序,则该对象为   默认初始化。使用自动或自动存储对象时   获得动态存储持续时间,该对象具有不确定性   值,如果没有为对象执行初始化,那么   对象保留不确定的值,直到替换该值   (5.17)。 [...]如果评估产生不确定的价值,   除以下情况外,行为未定义:[确定   与无符号窄字符类型相关的不适用例外]

[class.copy] / P8:

  

T的隐式声明的复制构造函数将具有   形式

X
     

如果每个可能构造的类类型X::X(const X&) 的子对象(或   其数组)具有复制构造函数,其第一个参数是类型   Mconst M&。否则,隐式声明   复制构造函数将具有

形式
const volatile M&

答案 2 :(得分:0)

volatile没有按照您的想法行事。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html

如果你依赖于volatile在Boehm在我链接的页面上提及的三个非常具体的用途之外,你将会得到意想不到的结果。