标准在哪里定义了 volatile 变量可以改变?

22 浏览
0 Comments

标准在哪里定义了 volatile 变量可以改变?

标准在哪里定义了volatile变量可以不被检测地改变?我找到了两个关于volatile的规范文本:

intro.execution/7

读取由volatile glvalue指定的对象、修改对象、调用库I/O函数或调用执行这些操作的函数都属于副作用,即执行环境状态的改变。对表达式(或子表达式)的评估通常包括值计算(包括确定glvalue评估的对象的标识和获取以前分配给prvalue评估的对象的值)和副作用的启动。当库I/O函数调用返回或对volatile glvalue进行访问的评估时,副作用被认为是完成的,即使由调用所暗示的一些外部动作(比如I/O本身)或volatile访问可能尚未完成。

这段话是关于未被检测的改变吗?副作用是否意味着这个?


或者有dcl.type.cv/5

通过volatile glvalue进行访问的语义是由实现定义的。如果尝试通过非volatile glvalue访问以volatile修饰的类型定义的对象,则行为是未定义的。

这段话是否涉及到我的问题?"通过volatile glvalue进行访问的语义是由实现定义的"具体意味着什么?你能给出不同"通过volatile glvalue进行访问的语义"的例子吗?


还有dcl.type.cv/6,它是关于我的问题的,但只是一个注释:

【注】:volatile是对实现的提示,以避免涉及对象的过度优化,因为对象的值可能通过实现无法检测到的方式被改变。此外,对于某些实现,volatile可能表示需要特殊的硬件指令来访问对象。详细的语义请参见[intro.execution]。通常情况下,volatile的语义在C++中意味着与C中的语义相同。— 结束注释

0
0 Comments

标准从下面的内容中定义了volatile变量可以改变的位置:

关键点在于“执行环境的状态变化”。

执行环境是指程序外部的环境。这可能包括操作系统、文件系统、屏幕等等。它通常是不可预测的。你不能假设如果你向文件写入一个0,那么这个文件就不会被另一个进程用1覆盖。

volatile变量在逻辑上属于执行环境的一部分。就C++而言,环境可以枚举、读取和写入它们,就像文件一样。而且这可以在程序不知情的情况下发生。

另一方面,你的实现意识到了程序和它的执行环境之间的链接,所以它对可能发生的事情有一些了解。如果它有一种私有的RAM磁盘实现,那么它可能知道某些文件名在OS文件系统中是不可见的。而且它可能知道volatile int i存在于CPU寄存器中,所以它不能通过内存映射来访问。这都是C++标准允许的。它只是以一般的术语讨论执行环境,实现必须更加精确。这就是“实现定义的语义”所指的意思。

我不认为有任何东西会使一个普通的volatile int i与普通的int有什么不同。至少在gcc中,你必须使用一个扩展和声明变量作为寄存器,才能使它成为一个寄存器。我的意思是,编译器将有特殊的扩展和语法来声明一个变量是一个寄存器,并不是通过名字的魔法或者使每个volatile int成为特殊的寄存器。因此,真正的“存在于CPU中”的变量在源代码中看起来会有所不同。

不是CPU寄存器。是IO硬件寄存器。这是完全不同的东西。没有人说volatile将一个变量映射到CPU寄存器上(我们过去使用register关键字来做这个,但是它已经废弃多年了)。

当然,很容易看出volatile变量和普通变量在任何合理的编译器中是非常不同的。只需要查看汇编输出就足够了。例如,参见这里

.m.: 我没有具体指定类型;在这个上下文中,“寄存器”只是指“不在C++指针可以指向的常规地址空间中”。至于“GCC是怎么做的”,这个问题标记为“language-lawyer”,所以你需要考虑所有的实现变化。这个问题特别依赖于实现,因为它讨论的是外部执行环境,而这个环境是多样化的。

"在这个上下文中,“寄存器”只是指“不在C++指针可以指向的常规地址空间中”。一个IO硬件寄存器绝对肯定存在于C++指针可以指向的常规地址空间中。这就是内存映射IO的全部意义所在。否则它就不会被称为“内存映射”。

.m.:你可能想看一下英特尔的x86手册,或者ARM的协处理器接口。我们之所以谈论内存映射I/O,是为了将其与非内存映射I/O区分开来。

一个实现通常不会将一个volatile变量放在CPU寄存器中,因为这会破坏volatile的目的。对于静态存储期的volatile变量来说尤其如此(自动volatile变量是相当无用的,静态存储期变量通常不能存在于普通指针不能访问的寄存器中,因为这样你就无法与同一平台上的任何其他语言正常交互了。标准允许不与任何东西交互的实现,但是谁会使用这样的实现呢?)

我对x86手册非常熟悉。我不明白非内存映射IO与手头的问题有什么关系。如果一个实现足够疯狂,把一个volatile变量分配给一个IO端口,那么它可以被硬件访问。如果你指的是一个CPU寄存器,那么惊讶的是,这些也可以被程序外部的方式访问,尽管不能通过直接内存访问(内核模块、调试器、电路调试等)。除非实现对整个生态系统有完全的控制,并且能够保证永远不会出现这种情况,否则它必须在这里也要尊重volatile。

虽然你的回答绝对是有道理的,但我无法将其与标准联系起来。根据引文,“修改对象...执行环境的状态变化”。所以,任何非volatile对象的值都可以“不可检测地”改变吗?希望不是。

.m.:“一个实现通常不会将一个volatile变量放在CPU寄存器中,因为这会破坏volatile的目的”不会。目的是禁用优化,而不是使程序更低效。与非volatile变量一样频繁地将volatile变量放在寄存器中对于程序来说非常有用和合理,只要程序不能区分(寄存器不能有更多的可见精度位)。

对不起,我不理解你说的话。人们需要volatile变量来映射I/O端口,或者以其他方式使内存地址对程序外部的力量可访问(以下简称“I/O硬件”)。禁用优化不是volatile的目的,它是正确地与I/O硬件进行通信的必要条件。与I/O硬件通信才是目的。将一个变量放在寄存器中会破坏这个目的,因为很难将CPU寄存器映射到外部世界。

.m.:“否则使内存地址对程序外部的力量可访问”,I/O端口只是volatile的一个历史性的理由。它们是固有的机器特定的。在C中有可移植的volatile用法。而异步信号不完全是“外部力量”,它是C/C++代码,只是在不可预测的时刻被调用。"与I/O硬件通信是其中一个值得注意的目的。

“将一个变量放在寄存器中会破坏这个目的”好吧,那么给我看一个例子,对于volatile变量与非volatile变量一样经常放在寄存器中,即将volatile用于寄存器分配的目的,会破坏volatile的可能用途。

可能存在可移植的用途,但编译器无法知道程序员的意图。我不知道我必须展示什么。全局变量永远不会进入寄存器,无论是否是volatile。编译器和链接器不是这样工作的。我不在乎标准是否真的这样说,这只是事实。

.m.:“无论是否是volatile。”那么你似乎在这一点上同意我。没有必要特殊情况。

我不明白你的主张具体是什么。

.m.:给定一个全局变量unsigned x, arr[10];,编译器可以用unsigned register temp=x; for (int i=0; i<10; i++) temp+=arr[i]; x=temp;来替换for (int i=0; i<10; i++) x+=arr[i];,从而在循环的持续时间内将x保存在寄存器中。只有当xvolatile时,这将被禁止。

0
0 Comments

标准在哪里规定了volatile变量可以改变?

volatile只是一种请求,要求编译器在每次访问时重新加载变量。它用于两种常见情况:

  • 变量可以被不同的线程(甚至是对该内存区域具有写访问权限的不同程序)或内核模式代码(例如特殊驱动程序)更改
  • 该变量表示物理内存寄存器,主要用于内核模式编程,或者在没有用户/内核模式概念的操作系统上,例如旧版MS/DOS。

一旦你知道了这一点,标准中的不同引用都变得有意义了。

通过volatile glvalue访问对象([basic.lval])等都是副作用,即执行环境状态的改变。

读取硬件寄存器可能会对底层系统产生影响,这就是为什么它被称为“可观察的副作用”的原因。

通过volatile glvalue进行访问的语义是实现定义的。如果尝试通过使用非volatile glvalue访问以volatile修饰的对象,行为是未定义的。

如果使用非volatile指针访问volatile硬件寄存器,编译器可能会缓存先前的值并不执行物理访问。

[注:使用volatile是提示实现避免对对象进行激进优化,因为对象的值可能会被实现无法检测到的方式更改。此外,对于某些实现,volatile可能表示需要使用特殊硬件指令来访问对象。有关详细语义,请参见[intro.execution]。一般而言,volatile的语义在C++中意图与C相同。-注释-]。

某些实现可能会为特殊的低级IO端口操作保留一块内存区域。在这种情况下,可能需要volatile限定符和该特殊内存区域地址的组合来验证使用特殊IO操作进行的转换或正常内存访问操作。

0