关键字有什么作用volatile
?在 C++ 中它解决了什么问题?
就我而言,我从来没有故意需要它。
volatile
如果您正在从内存中的某个位置读取,例如,一个完全独立的进程/设备/可能写入的任何内容,则需要。
我曾经在直接 C 语言的多处理器系统中使用双端口 ram。我们使用硬件管理的 16 位值作为信号量来了解其他人何时完成。本质上我们是这样做的:
void waitForSemaphore()
{
volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}
没有volatile
,优化器会认为循环是无用的(这家伙从不设置值!他疯了,摆脱那个代码!)我的代码将在没有获取信号量的情况下继续运行,从而导致以后出现问题。
volatile
在开发嵌入式系统或设备驱动程序时需要它,您需要读取或写入内存映射的硬件设备。特定设备寄存器的内容可能随时更改,因此您需要volatile
关键字来确保编译器不会优化此类访问。
一些处理器具有精度超过 64 位的浮点寄存器(例如,没有 SSE 的 32 位 x86,请参阅 Peter 的评论)。这样,如果您对双精度数运行多个操作,您实际上会得到比将每个中间结果截断为 64 位时更高精度的答案。
这通常很好,但这意味着根据编译器分配寄存器和优化的方式,在完全相同的输入上执行完全相同的操作会产生不同的结果。如果您需要一致性,则可以使用 volatile 关键字强制每个操作返回内存。
它对于一些没有代数意义但减少浮点误差的算法也很有用,例如 Kahan 求和。从代数上讲,它是一个 nop,因此它通常会被错误地优化,除非某些中间变量是易失的。
摘自 Dan Saks 的“Volatile as a promise”文章:
(...) volatile 对象是其值可能自发改变的对象。也就是说,当你声明一个对象是 volatile 时,你是在告诉编译器该对象可能会改变状态,即使程序中似乎没有任何语句会改变它。”
以下是他关于volatile
关键字的三篇文章的链接:
实现无锁数据结构时必须使用 volatile。否则编译器可以自由地优化对变量的访问,这将改变语义。
换句话说, volatile 告诉编译器对该变量的访问必须对应于物理内存读/写操作。
例如,这是在 Win32 API 中声明 InterlockedIncrement 的方式:
LONG __cdecl InterlockedIncrement(
__inout LONG volatile *Addend
);
我曾在 1990 年代初从事的一个大型应用程序包含使用 setjmp 和 longjmp 进行的基于 C 的异常处理。volatile 关键字对于需要保留在充当“catch”子句的代码块中的变量的值是必需的,以免这些变量存储在寄存器中并被 longjmp 清除。
在标准 C 中,使用volatile
信号处理程序的地方之一。事实上,在标准 C 中,您可以在信号处理程序中安全地做的就是修改volatile sig_atomic_t
变量或快速退出。事实上,AFAIK,它是标准 C 中唯一volatile
需要使用以避免未定义行为的地方。
ISO/IEC 9899:2011 §7.14.1.1
signal
功能¶5 如果信号的出现不是调用
abort
orraise
函数的结果,则如果信号处理程序引用具有静态或线程存储持续时间的任何对象,该对象不是无锁原子对象,而不是通过分配值,则行为未定义到声明为 的对象volatile sig_atomic_t
,或者信号处理程序调用标准库中的任何函数,而不是abort
函数、_Exit
函数、quick_exit
函数或signal
函数,其第一个参数等于与导致调用的信号对应的信号编号处理程序。此外,如果对signal
函数的这种调用导致 SIG_ERR 返回,则 的值errno
是不确定的。252)252)如果任何信号由异步信号处理程序生成,则行为未定义。
这意味着在标准 C 中,您可以编写:
static volatile sig_atomic_t sig_num = 0;
static void sig_handler(int signum)
{
signal(signum, sig_handler);
sig_num = signum;
}
其他的不多。
POSIX 对您可以在信号处理程序中执行的操作要宽松得多,但仍然存在限制(其中一个限制是标准 I/O 库 -printf()
等 - 不能安全使用)。
为嵌入式开发,我有一个循环来检查可以在中断处理程序中更改的变量。没有“volatile”,循环就变成了一个 noop——据编译器所知,变量永远不会改变,所以它优化了检查。
同样的事情也适用于在更传统的环境中可能在不同线程中更改的变量,但是我们经常进行同步调用,因此编译器在优化方面并不那么自由。
当编译器坚持优化我希望在我逐步执行代码时能够看到的变量时,我在调试版本中使用了它。
除了按预期使用它之外,volatile 还用于(模板)元编程。它可用于防止意外重载,因为 volatile 属性(如 const)参与重载决议。
template <typename T>
class Foo {
std::enable_if_t<sizeof(T)==4, void> f(T& t)
{ std::cout << 1 << t; }
void f(T volatile& t)
{ std::cout << 2 << const_cast<T&>(t); }
void bar() { T t; f(t); }
};
这是合法的;这两个重载都可能是可调用的,并且几乎相同。重载中的强制转换是合法的,因为我们知道 bar无论如何volatile
都不会传递非易失性。T
但是,该volatile
版本更糟糕,因此如果非易失性f
可用,则永远不要在重载解决方案中选择。
请注意,代码实际上从不依赖于volatile
内存访问。
该volatile
关键字旨在防止编译器对可能以编译器无法确定的方式更改的对象应用任何优化。
声明为的对象volatile
从优化中省略,因为它们的值可以随时被当前代码范围之外的代码更改。系统总是从内存位置读取对象的当前值,volatile
而不是在请求时将其值保存在临时寄存器中,即使先前的指令要求来自同一对象的值也是如此。
考虑以下情况
1) 由范围外的中断服务程序修改的全局变量。
2) 多线程应用程序中的全局变量。
如果我们不使用 volatile 限定符,可能会出现以下问题
1) 打开优化时,代码可能无法按预期工作。
2) 启用和使用中断时,代码可能无法按预期工作。
https://en.wikipedia.org/wiki/Volatile_(computer_programming)
所有的答案都很棒。但最重要的是,我想分享一个例子。
下面是一个小cpp程序:
#include <iostream>
int x;
int main(){
char buf[50];
x = 8;
if(x == 8)
printf("x is 8\n");
else
sprintf(buf, "x is not 8\n");
x=1000;
while(x > 5)
x--;
return 0;
}
现在,让我们生成上述代码的程序集(我将只粘贴与此处相关的程序集部分):
生成程序集的命令:
g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp
和大会:
main:
.LFB1594:
subq $40, %rsp #,
.seh_stackalloc 40
.seh_endprologue
# assembly.cpp:5: int main(){
call __main #
# assembly.cpp:10: printf("x is 8\n");
leaq .LC0(%rip), %rcx #,
# assembly.cpp:7: x = 8;
movl $8, x(%rip) #, x
# assembly.cpp:10: printf("x is 8\n");
call _ZL6printfPKcz.constprop.0 #
# assembly.cpp:18: }
xorl %eax, %eax #
movl $5, x(%rip) #, x
addq $40, %rsp #,
ret
.seh_endproc
.p2align 4,,15
.def _GLOBAL__sub_I_x; .scl 3; .type 32; .endef
.seh_proc _GLOBAL__sub_I_x
您可以在程序集中看到没有生成程序集代码,sprintf
因为编译器假定它x
不会在程序之外更改。while
循环也是如此。while
由于优化,循环被完全删除,因为编译器将其视为无用的代码,因此直接分配5
给x
(参见 参考资料movl $5, x(%rip)
)。
如果外部进程/硬件会改变 和之间x
某处的值,就会出现问题。我们希望block 可以工作,但不幸的是编译器已经删除了那部分。x = 8;
if(x == 8)
else
现在,为了解决这个问题,在 中assembly.cpp
,让我们更改int x;
为volatile int x;
并快速查看生成的汇编代码:
main:
.LFB1594:
subq $104, %rsp #,
.seh_stackalloc 104
.seh_endprologue
# assembly.cpp:5: int main(){
call __main #
# assembly.cpp:7: x = 8;
movl $8, x(%rip) #, x
# assembly.cpp:9: if(x == 8)
movl x(%rip), %eax # x, x.1_1
# assembly.cpp:9: if(x == 8)
cmpl $8, %eax #, x.1_1
je .L11 #,
# assembly.cpp:12: sprintf(buf, "x is not 8\n");
leaq 32(%rsp), %rcx #, tmp93
leaq .LC0(%rip), %rdx #,
call _ZL7sprintfPcPKcz.constprop.0 #
.L7:
# assembly.cpp:14: x=1000;
movl $1000, x(%rip) #, x
# assembly.cpp:15: while(x > 5)
movl x(%rip), %eax # x, x.3_15
cmpl $5, %eax #, x.3_15
jle .L8 #,
.p2align 4,,10
.L9:
# assembly.cpp:16: x--;
movl x(%rip), %eax # x, x.4_3
subl $1, %eax #, _4
movl %eax, x(%rip) # _4, x
# assembly.cpp:15: while(x > 5)
movl x(%rip), %eax # x, x.3_2
cmpl $5, %eax #, x.3_2
jg .L9 #,
.L8:
# assembly.cpp:18: }
xorl %eax, %eax #
addq $104, %rsp #,
ret
.L11:
# assembly.cpp:10: printf("x is 8\n");
leaq .LC1(%rip), %rcx #,
call _ZL6printfPKcz.constprop.1 #
jmp .L7 #
.seh_endproc
.p2align 4,,15
.def _GLOBAL__sub_I_x; .scl 3; .type 32; .endef
.seh_proc _GLOBAL__sub_I_x
在这里您可以看到生成了sprintf
,printf
和while
loop 的汇编代码。好处是,如果x
变量被一些外部程序或硬件改变,sprintf
部分代码将被执行。同样while
循环现在可以用于忙等待。
除了 volatile 关键字用于告诉编译器不要优化对某些变量的访问(可以由线程或中断例程修改)之外,它还可以用来消除一些编译器错误——是的,它可以成为---。
例如,我在嵌入式平台上工作,编译器对变量的值做出了一些错误的假设。如果代码没有优化,程序可以正常运行。使用优化(这是非常需要的,因为它是一个关键的例程),代码将无法正常工作。唯一的解决方案(虽然不是很正确)是将“错误”变量声明为 volatile。
volatile
即使没有关键字,您的程序似乎也可以工作?或许这就是原因:
如前所述,volatile
关键字有助于以下情况
volatile int* p = ...; // point to some memory
while( *p!=0 ) {} // loop until the memory becomes zero
但是,一旦调用了外部或非内联函数,似乎几乎没有效果。例如:
while( *p!=0 ) { g(); }
然后有或没有volatile
几乎相同的结果。
只要 g() 可以完全内联,编译器就可以看到正在发生的一切,因此可以进行优化。但是当程序调用编译器看不到发生了什么的地方时,编译器再做任何假设是不安全的。因此,编译器将生成始终直接从内存中读取的代码。
但是要当心,当您的函数 g() 变为内联时(由于显式更改或由于编译器/链接器的巧妙性),如果您忘记了volatile
关键字,您的代码可能会中断!
volatile
因此,即使您的程序似乎无法正常工作,我也建议您添加关键字。对于未来的变化,它使意图更加清晰和稳健。
在 C 语言的早期,编译器会将所有读取和写入左值的操作解释为内存操作,按照与代码中出现的读取和写入相同的顺序执行。在许多情况下,如果给编译器一定的自由来重新排序和合并操作,效率可以大大提高,但是这样做存在问题。尽管经常以某种顺序指定操作仅仅是因为必须以某种顺序指定它们,因此程序员选择了许多同样好的替代方案之一,但情况并非总是如此。有时,某些操作以特定顺序发生是很重要的。
究竟哪些测序细节很重要,取决于目标平台和应用领域。该标准没有提供特别详细的控制,而是选择了一个简单的模型:如果一系列访问是使用不合格的左值完成的volatile
,编译器可以在它认为合适的时候重新排序和合并它们。如果使用volatile
-qualified 左值完成操作,则质量实现应提供针对其预期平台和应用程序领域的代码可能需要的任何额外排序保证,而不要求程序员使用非标准语法。
不幸的是,许多编译器没有确定程序员需要什么保证,而是选择提供标准规定的最低限度的保证。这使得volatile
它没有应有的用处。例如,在 gcc 或 clang 上,需要实现基本“切换互斥锁”的程序员 [在另一个任务完成之前,已获取和释放互斥锁的任务不会再次这样做] 必须执行一个四件事:
将互斥体的获取和释放放在编译器无法内联的函数中,也无法对其应用全程序优化。
将由互斥锁保护的所有对象限定为volatile
-- 如果所有访问都发生在获取互斥锁之后和释放它之前,则不需要。
使用优化级别 0 强制编译器生成代码,就好像所有不合格的对象register
都是volatile
.
使用 gcc 特定的指令。
相比之下,当使用更适合系统编程的更高质量的编译器(例如 icc)时,会有另一种选择:
volatile
需要获取或释放的每个地方都执行 -qualified 写入。获取基本的“切换互斥体”需要volatile
读取(以查看它是否已准备好),并且也不需要volatile
写入(对方不会尝试重新获取它,直到它被交还)但必须执行无意义的volatile
写入仍然比 gcc 或 clang 下可用的任何选项更好。
我应该提醒您的一种用途是,在信号处理函数中,如果您想访问/修改一个全局变量(例如,将其标记为 exit = true),您必须将该变量声明为“volatile”。
其他答案已经提到避免一些优化,以便:
每当您需要一个值看起来来自外部且不可预测并避免基于已知值的编译器优化时,以及当实际未使用结果但您需要计算它,或者它已被使用时,易失性是必不可少的您想为基准计算多次,并且您需要计算在精确点开始和结束。
易失性读取类似于输入操作(例如scanf
或使用cin
):值似乎来自程序外部,因此任何依赖于该值的计算都需要在它之后开始。
易失性写入类似于输出操作(类似于printf
或使用cout
):该值似乎在程序外部进行通信,因此如果该值取决于计算,则需要在 之前完成。
因此可以使用一对易失性读/写来驯服基准并使时间测量变得有意义。
如果没有 volatile,您的计算之前可以由编译器启动,因为没有什么可以阻止使用诸如时间测量之类的函数对计算进行重新排序。
我想从他的GotW #95中引用 Herb Sutter 的话,这有助于理解volatile
变量的含义:
C++
volatile
C#
变量(在和等语言中没有类似物Java
)总是超出本文和任何其他关于内存模型和同步的文章的范围。那是因为C++
volatile
变量根本与线程或通信无关,并且不与这些事物交互。相反,C++
volatile
变量应该被视为通向语言之外不同世界的门户——根据定义,该内存位置不遵守语言的内存模型,因为该内存位置是由硬件访问的(例如,由子卡写入),具有一个以上的地址,或者以其他方式“奇怪”且超出语言范围。所以C++
volatile
变量通常是每个关于同步的准则的例外,因为使用普通工具(互斥锁、原子等)总是固有地“活泼”和不可同步,并且更普遍地存在于语言和编译器的所有正常之外,包括它们通常无法优化由编译器(因为不允许编译器知道它们的语义; a 的volatile int vi;
行为可能不像 normalint
,而且您甚至不能假设代码 likevi = 5; int read_back = vi;
保证会导致read_back == 5
,或者类似的代码int i = vi; int j = vi;
读取 vi 两次会例如,如果是硬件计数器,结果i == j
将不正确)。vi