【发布时间】:2016-07-06 22:55:12
【问题描述】:
背景
这是受到此问题/答案以及随后在 cmets 中的讨论的启发:Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems?。根据其他人和我对应该发生的事情的解释,正如 cmets 中所讨论的,我已将其提交给 GCC Bugzilla:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 其他相关回复仍然欢迎。
此外,该线程已经引起了这个问题:Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?
简介
我知道volatile 不是大多数人认为的那样,是实现定义的毒蛇巢。而且我当然不想在任何实际代码中使用以下构造。也就是说,我对这些示例中发生的事情完全感到困惑,所以我非常感谢任何说明。
我的猜测是,这是由于对标准的高度细微的解释,或者(更有可能?)只是使用的优化器的极端情况。无论哪种方式,虽然学术性大于实用性,但我希望这被认为是有价值的分析,尤其是考虑到 volatile 被误解的程度。更多的数据点 - 或者更有可能是反对它的点 - 一定是好的。
输入
鉴于此代码:
#include <cstddef>
void f(void *const p, std::size_t n)
{
unsigned char *y = static_cast<unsigned char *>(p);
volatile unsigned char const x = 42;
// N.B. Yeah, const is weird, but it doesn't change anything
while (n--) {
*y++ = x;
}
}
void g(void *const p, std::size_t n, volatile unsigned char const x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
void h(void *const p, std::size_t n, volatile unsigned char const &x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
int main(int, char **)
{
int y[1000];
f(&y, sizeof y);
volatile unsigned char const x{99};
g(&y, sizeof y, x);
h(&y, sizeof y, x);
}
输出
g++ 来自 gcc (Debian 4.9.2-10) 4.9.2(Debian stable 又名 Jessie)和命令行 g++ -std=c++14 -O3 -S test.cpp 为 main() 生成以下 ASM。版本Debian 5.4.0-6(当前unstable)产生等效代码,但我只是碰巧先运行旧版本,所以它是:
main:
.LFB3:
.cfi_startproc
# f()
movb $42, -1(%rsp)
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L21:
subq $1, %rax
movzbl -1(%rsp), %edx
jne .L21
# x = 99
movb $99, -2(%rsp)
movzbl -2(%rsp), %eax
# g()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L22:
subq $1, %rax
jne .L22
# h()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L23:
subq $1, %rax
movzbl -2(%rsp), %edx
jne .L23
# return 0;
xorl %eax, %eax
ret
.cfi_endproc
分析
所有 3 个函数都是内联的,并且分配 volatile 局部变量的两个函数都在堆栈上这样做,原因很明显。但这是他们唯一分享的东西......
-
f()确保在每次迭代时从x读取,可能是因为它的volatile- 但只是将结果转储到edx,可能是因为目标y不是'未声明volatile并且从未被读取,这意味着可以根据 as-if 规则对其进行更改。好的,有道理。- 嗯,我的意思是……有点。就像,不是真的,因为
volatile真的是用于硬件寄存器,显然本地值不能是其中之一 - 并且不能以volatile方式修改,除非它的地址被传递出去......它不是。看,volatile本地值没有多大意义。但是 C++ 允许我们声明它们并尝试对它们做一些事情。于是,我们一如既往地迷茫,跌跌撞撞地向前走。
- 嗯,我的意思是……有点。就像,不是真的,因为
g():什么。 通过将volatile源移动到传递值参数中,这仍然只是另一个局部变量, GCC 以某种方式决定它不是或 lessvolatile,因此它不需要每次迭代都读取它......但它仍然执行循环,尽管它的主体现在什么都不做.-
h():通过将传递的volatile作为pass-by-reference,恢复了与f()相同的有效行为,因此循环读取volatile。 p>- 仅这个案例对我来说实际上是有实际意义的,因为上面针对
f()列出的原因。详细说明:想象x指的是一个硬件寄存器,每次读取都有副作用。您不会想跳过其中任何一个。
- 仅这个案例对我来说实际上是有实际意义的,因为上面针对
添加#define volatile /**/ 会导致main() 成为无操作,正如您所料。因此,当存在时,即使在局部变量 volatile 上也会做一些事情......我只是不知道在g() 的情况下 what。那里到底发生了什么?
问题
- 为什么在体内声明的局部值会产生与按值参数不同的结果,而前者会让读取被优化掉?两者都被声明为
volatile。既没有传递地址 - 也没有static地址,排除了任何内联 ASMPOKEry - 所以它们永远不能被函数修改。编译器可以看到每个都是常量,不需要重新读取,volatile只是不正确 -- 所以 (A) 是否在这种约束下允许被省略? (表现好像他们没有被宣布
volatile)- - 和 (B) 为什么只有一个被省略?某些
volatile局部变量比其他volatile多吗?
- 所以 (A) 是否在这种约束下允许被省略? (表现好像他们没有被宣布
- 暂时搁置这种不一致:优化读取后,为什么编译器仍会生成循环? 它什么都不做!为什么优化器不删除它好像没有循环被编码?
由于优化分析的顺序等原因,这是一个奇怪的极端情况吗?由于代码是一个愚蠢的思想实验,我不会为此责备 GCC,但很高兴知道这一点。 (或者g() 是人们多年来梦寐以求的手动计时循环?)如果我们断定这与标准无关,我会将其移至他们的 Bugzilla 以供参考。
当然,从实际的角度来看,更重要的问题是,尽管我不希望这掩盖编译器极客的潜力...根据标准,如果有的话,哪个是明确定义/正确的?
【问题讨论】:
-
TL;DR - 如果它不改变程序的可观察行为真的重要吗?
-
@CaptainObvlious 对 volatile 变量(甚至是自动变量)的修改被认为是可观察的行为
-
我会说
g是根据标准的编译器错误 -
@underscore_d 是的,读取也是可观察到的行为
-
@DavidSchwartz 该标准规定,系统必须对与
x对应的内存位置进行读取,每次循环迭代一次。如果系统(无论是编译器,还是 CPU 或其他)将所有这些组合到一个读取中,那将是不合格的。
标签: c++ optimization g++ volatile pass-by-value