那是什么 volatile
关键词呢?在C ++中,它可以解决什么问题?
就我而言,我从来没有故意需要它。
volatile
可以有效使用,并以通俗易懂的方式组合在一起。链接:publications.gbdirect.co.uk/c_book/chapter8/...
volatile
比friend
关键字有用。
那是什么 volatile
关键词呢?在C ++中,它可以解决什么问题?
就我而言,我从来没有故意需要它。
volatile
可以有效使用,并以通俗易懂的方式组合在一起。链接:publications.gbdirect.co.uk/c_book/chapter8/...
volatile
比friend
关键字有用。
Answers:
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
,则优化器将认为该循环是无用的(该家伙从未设置过该值!他真是疯了,摆脱了该代码!),我的代码将在没有获得信号量的情况下继续进行,导致以后出现问题。
uint16_t* volatile semPtr
改写会怎样?这应将指针标记为易失性(而不是所指向的值),以便对指针本身进行检查,例如semPtr == SOME_ADDR
可能无法优化。但是,这也意味着一个不稳定的指向值。没有?
volatile
在开发嵌入式系统或设备驱动程序(需要读取或写入内存映射的硬件设备)时需要。特定设备寄存器的内容可以随时更改,因此您需要使用volatile
关键字来确保编译器不会对此类访问进行优化。
某些处理器的浮点寄存器的精度超过64位(例如,不带SSE的32位x86,请参见Peter的评论)。这样,如果您对双精度数字运行多个运算,则实际上得到的精度要比将每个中间结果截断为64位的精度更高。
通常这很好,但这意味着根据编译器如何分配寄存器和进行优化,对于完全相同的输入,完全相同的操作将有不同的结果。如果需要一致性,则可以使用volatile关键字强制每个操作返回到内存。
对于某些没有代数意义但减少浮点误差的算法(例如Kahan求和),它也很有用。从代数角度讲,这是一个小问题,因此除非某些中间变量是可变的,否则它经常会被错误地优化。
volatile double
而不是just double
来解决该问题,以确保在继续进行进一步的计算之前将其从FPU精度截断为64位(RAM)精度。由于浮点误差的进一步放大,结果大为不同。
g++ -mfpmath=sse
将其用于32位x86。您可以使用gcc -ffloat-store
强制四舍五入到处使用的x87时甚至,也可以设置的x87精度53位尾数:randomascii.wordpress.com/2012/03/21/...。
volatile
用来强制在一些特定位置进行舍入而不会损失到处的好处。
实现无锁数据结构时,必须使用volatile。否则,编译器可以自由优化对变量的访问,这将改变语义。
换句话说,volatile告诉编译器对该变量的访问必须与物理内存的读/写操作相对应。
例如,这是在Win32 API中声明InterlockedIncrement的方式:
LONG __cdecl InterlockedIncrement(
__inout LONG volatile *Addend
);
std::atomic<LONG>
因此您可以更安全地编写无锁代码,而不会出现优化纯负载/纯存储,重新排序或进行其他操作的问题。
我在1990年代初曾使用的一个大型应用程序包含使用setjmp和longjmp的基于C的异常处理。volatile关键字对于变量的值是必需的,这些变量的值需要保留在用作“ catch”子句的代码块中,以免将这些var存储在寄存器中并由longjmp清除。
在标准C中,使用的地方之一volatile
是信号处理程序。实际上,在Standard 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对您可以在信号处理程序中执行的操作更加宽容,但仍然存在局限性(其中一项局限性是printf()
无法安全使用Standard I / O库等)。
为嵌入式开发,我有一个循环来检查可以在中断处理程序中更改的变量。如果没有“ volatile”,则循环将成为无条件操作-就编译器所知,该变量永不更改,因此可以优化检查。
同样的情况也适用于在更传统的环境中可能在不同线程中更改的变量,但是我们经常进行同步调用,因此编译器在优化方面并不是那么自由。
除了按预期方式使用它以外,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
超载是合法的,因为我们知道巴不会通过非易失性T
反正。volatile
但是,该版本严格较差,因此,如果有非易失性f
可用,切勿在过载分辨率中选择。
请注意,代码实际上从不依赖于volatile
内存访问。
该volatile
关键字旨在防止编译器对可能以编译器无法确定的方式更改的对象进行任何优化。
volatile
优化过程中会忽略声明为的对象,因为它们的值可以随时通过当前代码范围之外的代码进行更改。系统始终volatile
从存储位置读取对象的当前值,而不是在请求时将其值保存在临时寄存器中,即使先前的指令要求从同一对象获取值。
考虑以下情况
1)范围外的中断服务程序修改了全局变量。
2)多线程应用程序中的全局变量。
如果我们不使用volatile限定符,则可能会出现以下问题
1)打开优化后,代码可能无法按预期工作。
2)当启用和使用中断时,代码可能无法按预期工作。
除了使用volatile关键字告诉编译器不要优化对某些变量的访问(可以通过线程或中断例程进行修改)外,还可以使用它来消除一些编译器错误 - 是的,它可以是 ---。
例如,我在嵌入式平台上工作,当时编译器对变量的值进行了一些错误的假设。如果未对代码进行优化,则程序可以正常运行。使用优化(由于这是关键例程,确实需要进行优化),因此代码无法正常工作。唯一的解决方案(尽管不是很正确)是将“ faulty”变量声明为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
即使您的程序似乎无法正常运行,我也建议添加关键字。对于未来的变化,它使意图更清晰,更强大。
volatile
合格的函数指针void (* volatile fun_ptr)() = fun; fun_ptr();
在C语言的早期,编译器会将读取和写入左值的所有操作解释为内存操作,以与代码中出现的读写相同的顺序执行。如果赋予编译器一定程度的自由来重新排序和合并操作,那么在许多情况下,效率可以大大提高,但这是有问题的。甚至经常以某种顺序指定操作,仅仅是因为有必要以某种顺序指定操作,因此程序员选择了许多效果很好的替代方法之一,但并非总是如此。有时,某些操作按特定顺序进行很重要。
到底哪些测序细节很重要,取决于目标平台和应用领域。该标准没有提供特别详细的控制,而是选择了一个简单的模型:如果使用不合格的左值完成访问序列,则volatile
编译器可能会重新排列并合并它们认为合适的值。如果使用volatile
-qualified左值执行某项操作,则质量实现应提供针对目标平台和应用程序域的代码可能需要的任何其他顺序保证,而无需使用非标准语法。
不幸的是,许多编译器没有确定程序员需要什么保证,而是选择提供标准要求的最低限度保证。这使它volatile
的实用性大大降低。例如,在gcc或clang上,需要实现基本的“手动互斥量”的程序员(其中一个已经获得并释放互斥量的任务将不会再执行此操作,直到另一个任务已经这样做)。四件事:
将获取和释放互斥锁放在编译器无法内联并且无法对其应用“完整程序优化”的函数中。
将所有由互斥锁保护的对象限定为- volatile
某种东西,如果所有访问都发生在获取互斥锁之后且释放它之前,则不需要这样做。
使用优化级别0强制编译器生成代码,就好像所有不合格的对象register
都是一样volatile
。
使用特定于gcc的指令。
相比之下,当使用更适合系统编程的高质量编译器(例如icc)时,将有另一种选择:
volatile
在需要获取或释放的所有地方执行合格的写入。获取基本的“交接互斥体”需要进行volatile
读取(以查看其是否已准备就绪),并且也不需要进行volatile
写入(另一方在交回之前不会尝试重新获取它),但是必须执行无意义的volatile
写入仍然比gcc或clang下可用的任何选项都要好。
所有答案都很好。但最重要的是,我想分享一个例子。
下面是一个小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
循环现在可以用于繁忙的等待。
其他答案已经提到避免进行一些优化以:
每当您需要一个值似乎来自外部且不可预测并且避免基于已知的值进行编译器优化时,以及当结果未实际使用但您需要对它进行计算或使用它时,可变性都是必不可少的您想为基准测试多次计算它,并且您需要计算以精确的点开始和结束。
易失性读取就像输入操作(如scanf
或使用cin
):值似乎来自程序的外部,因此任何依赖于该值的计算都必须在该值之后开始。
易失性写入就像输出操作(如printf
或使用cout
):该值似乎在程序外部传递,因此,如果该值取决于计算,则需要在完成之前完成。
因此,可以使用一对易失性读/写来控制基准并使时间测量有意义。
如果没有volatile,则可以由编译器之前开始计算,因为没有什么可以阻止使用诸如time measurement之类的功能对计算进行重新排序。