我对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四字节版本总是占用其他时间的一半时间,也是混淆的来源
答案 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 a
或volatile 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
是类类型(threeBytes
或fourBytes
),则应用左值到右值转换需要通过volatile lvalue创建临时初始化{{ 1}}([conv.lval] / p2)。但是,隐式声明的复制构造函数总是通过非易失性引用([class.copy] / p8)获取其参数;这样的引用不能绑定到volatile对象。因此,该计划形成不良。
如果a
为T
,则应用左值到右值转换会产生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&)
的子对象(或 其数组)具有复制构造函数,其第一个参数是类型M
或const M&
。否则,隐式声明 复制构造函数将具有const volatile M&
答案 2 :(得分:0)
volatile
没有按照您的想法行事。
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html
如果你依赖于volatile
在Boehm在我链接的页面上提及的三个非常具体的用途之外,你将会得到意想不到的结果。