volatile
关键字有什么作用?在C ++中它解决了什么问题?
就我而言,我从未故意需要它。
答案 0 :(得分:239)
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
,优化器将循环视为无用(该伙伴永远不会设置值!他很疯狂,摆脱代码!)并且我的代码将在没有获取信号量的情况下继续进行,导致以后出现问题
答案 1 :(得分:77)
volatile
,您需要读取或写入内存映射的硬件设备。特定设备寄存器的内容可能随时更改,因此您需要volatile
关键字以确保编译器不会优化此类访问。
答案 2 :(得分:68)
某些处理器具有超过64位精度的浮点寄存器(例如,没有SSE的32位x86,请参阅Peter的评论)。这样,如果对双精度数运行多次操作,实际上得到的答案高于将每个中间结果截断为64位的答案。
这通常很好,但这意味着根据编译器分配寄存器的方式和优化,您将对完全相同的输入完全相同的操作产生不同的结果。如果需要一致性,则可以使用volatile关键字强制每个操作返回内存。
对于一些没有代数意义但减少浮点误差的算法,例如Kahan求和,它也很有用。代数上它是一个nop,因此除非某些中间变量是易变的,否则它往往会被错误地优化出来。
答案 3 :(得分:43)
来自Dan Saks的嵌入式系统文章:
“volatile对象的值可能会自发地改变。也就是说,当你声明一个对象是volatile时,你告诉编译器该对象可能会改变状态,即使程序中的任何语句都没有改变它。“
链接到Saks先生关于volatile关键字的两篇精彩文章:
http://www.embedded.com/columns/programmingpointers/174300478 http://www.embedded.com/columns/programmingpointers/175801310
答案 4 :(得分:23)
实现无锁数据结构时必须使用volatile。否则,编译器可以自由地优化对变量的访问,这将改变语义。
换句话说,volatile告诉编译器访问此变量必须对应于物理内存读/写操作。
例如,这是在Win32 API中声明InterlockedIncrement的方式:
LONG __cdecl InterlockedIncrement(
__inout LONG volatile *Addend
);
答案 5 :(得分:10)
在标准C中,使用volatile
的其中一个地方是信号处理程序。实际上,在标准C中,您可以安全地在信号处理程序中执行的操作是修改volatile sig_atomic_t
变量,或快速退出。实际上,AFAIK,它是标准C中唯一需要使用volatile
来避免未定义行为的地方。
ISO / IEC 9899:2011§7.14.1.1
signal
函数¶5如果信号的出现不是调用
abort
或raise
函数的结果, 如果信号处理程序引用具有静态或线程的任何对象,则行为未定义 存储持续时间不是无锁原子对象,而是通过为其分配值 声明为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()
等 - 不能使用安全地)。
答案 6 :(得分:10)
我在20世纪90年代早期使用的大型应用程序包含使用setjmp和longjmp的基于C的异常处理。 volatile的关键字对于需要在作为“catch”子句的代码块中保存的变量是必要的,以免这些变量存储在寄存器中并被longjmp消灭。
答案 7 :(得分:7)
除了按预期使用它之外,在(模板)元编程中使用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); }
};
这是合法的;两个重载都可以调用,并且几乎完全相同。 volatile
重载中的强制转换是合法的,因为我们知道bar无论如何都不会传递非易失性T
。但volatile
版本严格更糟,因此如果非易失性f
可用,则永远不会在重载解析中选择。
请注意,代码实际上并不依赖于volatile
内存访问。
答案 8 :(得分:7)
我已经在调试版本中使用它,当编译器坚持优化我希望能够在逐步执行代码时看到的变量。
答案 9 :(得分:7)
为嵌入式开发,我有一个循环来检查可以在中断处理程序中更改的变量。如果没有“volatile”,循环就会变成noop - 就编译器而言,变量永远不会改变,因此它会优化检查。
同样的事情适用于在更传统的环境中可能在不同线程中更改的变量,但是我们经常进行同步调用,因此编译器不是那么自由优化。
答案 10 :(得分:6)
答案 11 :(得分:4)
volatile
关键字旨在防止编译器对可能以编译器无法确定的方式更改的对象应用任何优化。
声明为volatile
的对象在优化中被省略,因为它们的值可以随时由当前代码范围之外的代码更改。系统始终从内存位置读取volatile
对象的当前值,而不是将其值保存在请求点的临时寄存器中,即使前一条指令要求来自同一对象的值。
请考虑以下情况
1)由范围外的中断服务程序修改的全局变量。
2)多线程应用程序中的全局变量。
如果我们不使用volatile限定符,可能会出现以下问题
1)打开优化时,代码可能无法正常工作。
2)启用和使用中断时,代码可能无法正常工作。
Volatile: A programmer’s best friend
https://en.wikipedia.org/wiki/Volatile_(computer_programming)
答案 12 :(得分:2)
除了volatile关键字用于告诉编译器不优化对某个变量的访问(可以通过线程或中断例程修改)之外,还可以用于删除某些编译器错误 - 是的,它可以是 ---。
例如,我在嵌入式平台上工作的是编译器对变量值进行了一些错误的分析。如果代码没有优化,程序将运行正常。通过优化(这是真正需要的,因为它是一个关键的例程)代码将无法正常工作。唯一的解决方案(虽然不是很正确)是将'faulty'变量声明为volatile。
答案 13 :(得分:2)
即使没有volatile
关键字,您的计划似乎也能正常运作?也许这就是原因:
如前所述,volatile
关键字有助于处理
volatile int* p = ...; // point to some memory
while( *p!=0 ) {} // loop until the memory becomes zero
但是一旦调用外部或非内联函数,似乎几乎没有效果。 E.g:
while( *p!=0 ) { g(); }
然后有或没有volatile
生成几乎相同的结果。
只要g()可以完全内联,编译器就可以看到正在进行的所有事情,因此可以进行优化。但是当程序调用一个编译器无法看到正在发生的事情的地方时,编译器不再需要做出任何假设是不安全的。因此编译器将生成始终直接从内存中读取的代码。
但要注意当天,当你的函数g()变为内联时(由于显式更改或由于编译器/链接器的聪明),如果你忘记了volatile
关键字,你的代码可能会中断!
因此,即使您的程序似乎没有,我也建议添加volatile
关键字。它使意图在未来的变化方面更清晰,更稳健。
答案 14 :(得分:2)
在C语言的早期,编译器会将读取和写入左值的所有操作解释为内存操作,以与代码中出现的读写相同的顺序执行。如果赋予编译器一定程度的自由来重新排序和合并操作,那么在许多情况下,效率可以大大提高,但这是有问题的。甚至经常以某种顺序指定操作,只是因为有必要以 some 顺序指定它们,因此程序员选择了许多效果很好的替代方法之一,但并非总是如此。有时候,按照特定顺序进行某些操作非常重要。
确切的测序细节很重要,具体取决于目标平台和应用领域。该标准没有提供特别详细的控制,而是选择了一个简单的模型:如果使用不合格using ptA as this
的左值完成访问序列,则编译器可能会重新排列并合并它们认为合适的值。如果使用ptA->
限定的左值完成操作,那么质量实现应提供针对其预期平台和应用程序域的代码可能需要的任何其他顺序保证,而不必要求使用非标准语法。
不幸的是,许多编译器没有确定程序员需要什么保证,而是选择提供标准要求的最低限度保证。这使得volatile
的使用效率大大降低。例如,在gcc或clang上,需要实现基本的“手动互斥量”的程序员(一个已经获得并释放互斥量的任务将不会再次执行该任务,直到另一个任务已经这样做)。四件事:
将互斥量的获取和发布置于编译器无法内联且无法对其应用“完整程序优化”的功能中。
将互斥锁保护的所有对象都限定为volatile
-如果在获取互斥锁之后且释放互斥锁之前进行所有访问,则不需要这样做。
使用优化级别0强制编译器生成代码,就好像所有不合格的volatile
对象都是volatile
。
使用特定于gcc的指令。
相反,当使用更适合系统编程的高质量编译器(例如icc)时,则会有另一种选择:
register
要求的写操作。获取基本的“切换互斥体”需要进行volatile
的读取(以查看其是否已准备就绪),也不需要进行volatile
的写入(另一端也不会尝试)重新获取它,直到将其退还为止),但是必须执行无意义的volatile
写操作仍然比gcc或clang下可用的任何选项都要好。
答案 15 :(得分:1)
我应该提醒你的一个用途是,在信号处理函数中,如果要访问/修改全局变量(例如,将其标记为exit = true),则必须将该变量声明为&#39; volatile& #39;
答案 16 :(得分:1)
所有答案都很出色。但最重要的是,我想分享一个例子。
下面是一个小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
和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
循环的汇编代码已生成。这样做的好处是,如果x
变量被某些外部程序或硬件更改,则将执行代码的sprintf
部分。同样,while
循环现在可以用于繁忙的等待。
答案 17 :(得分:1)
我想引用赫伯萨特的GotW #95的话,可以帮助理解volatile
变量的含义:
C++
volatile
变量(在 C#
和 Java
等语言中没有类似物)总是超出了本文和任何其他关于内存模型和同步。这是因为 C++
volatile
变量根本与线程或通信无关,也不与这些东西交互。相反,C++
volatile
变量应该被视为进入语言之外的不同世界的门户——根据定义,一个内存位置不遵守语言的内存模型,因为该内存位置是由硬件访问的(例如,由子卡写入),有多个地址,或者“奇怪”且超出语言范围。因此,C++
volatile
变量普遍是每个关于同步的指南的例外,因为使用普通工具(互斥锁、原子等)总是天生“活泼”和不可同步的,并且更普遍地存在于所有正常之外语言和编译器,包括它们通常不能被编译器优化(因为不允许编译器知道它们的语义;volatile int vi;
的行为可能不像正常的 int
,你可以甚至假设像 vi = 5; int read_back = vi;
这样的代码肯定会导致 read_back == 5
,或者像 int i = vi; int j = vi;
这样的代码读取 vi 两次将导致 i == j
,如果 {例如,{1}} 是一个硬件计数器)。
答案 18 :(得分:0)
其他答案已经提到了避免某些优化,以便:
无论何时您需要一个值似乎来自外部且不可预测并避免基于已知值的编译器优化以及当结果未实际使用但您需要对它进行计算时,可变性都是必不可少的它已被使用,但您要针对基准进行多次计算,并且需要从精确点开始和结束的计算。
易失性读取就像一个输入操作(例如scanf
或对cin
的使用):该值似乎来自程序外部,因此任何具有对值的依赖需要在此之后开始。
易失性写入就像一个输出操作(例如printf
或对cout
的使用):该值似乎是在程序外部传递的,因此,如果该值取决于a计算,它需要在之前完成。
因此可以使用一对易失性的读/写操作来驯服基准测试并使时间测量变得有意义。
在没有波动的情况下,您的计算可以由编译器在开始之前进行,因为没有什么可以阻止对诸如时间测量等功能的计算进行重新排序。