的定义 volatile
volatile
告诉编译器变量的值可能会在编译器不知道的情况下更改。因此,编译器不能仅仅因为C程序似乎没有更改它就不能假定该值没有改变。
另一方面,这意味着在编译器不知道的其他地方可能需要(读取)变量的值,因此必须确保对变量的每个赋值实际上都是作为写操作执行的。
用例
volatile
何时需要
- 将硬件寄存器(或内存映射的I / O)表示为变量-即使永远不会读取寄存器,编译器也不能只是跳过写操作,认为“愚蠢的程序员。试图将值存储在他/她所用的变量中永远都不会回读。如果我们忽略了写作,他甚至不会注意到。” 相反,即使程序从不向变量写入值,硬件也可能会更改其值。
- 在执行上下文(例如ISR /主程序)之间共享变量(请参阅@kkramo的答案)
的影响 volatile
声明变量后volatile
,编译器必须确保程序代码中对该变量的每个赋值均反映在实际的写操作中,并且每次程序代码中的读取均从(映射的)内存中读取该值。
对于非易失性变量,编译器假定它知道是否/何时改变了变量的值,并可以以不同的方式优化代码。
首先,编译器可以通过将值保留在CPU寄存器中来减少对内存的读/写次数。
例:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
在这里,编译器可能甚至不会为result
变量分配RAM ,并且永远不会将中间值存储在CPU寄存器中的任何位置。
如果result
是易失性的,每次result
在C代码中出现C都将要求编译器执行对RAM(或I / O端口)的访问,从而导致性能降低。
其次,编译器可以针对性能和/或代码大小对非易失性变量重新排序。简单的例子:
int a = 99;
int b = 1;
int c = 99;
可以重新订购
int a = 99;
int c = 99;
int b = 1;
这可以节省汇编指令,因为该值99
不必两次加载。
如果a
,b
并c
呈震荡编译器将不得不为发出,因为它们是在程序给出指定的确切顺序值的说明。
另一个经典示例是这样的:
volatile uint8_t signal;
void waitForSignal() {
while ( signal == 0 ) {
// Do nothing.
}
}
如果signal
不是volatile
,编译器将“认为”这while( signal == 0 )
可能是一个无限循环(因为该循环内的signal
代码将永远不会对其进行更改),并且可能会生成等效的
void waitForSignal() {
if ( signal != 0 ) {
return;
} else {
while(true) { // <-- Endless loop!
// do nothing.
}
}
}
妥善处理volatile
价值
如上所述,当volatile
变量被访问的次数比实际需要的次数多时,可能会导致性能下降。为了缓解此问题,您可以通过将值分配给非易失性变量来使值“不变”,例如
volatile uint32_t sysTickCount;
void doSysTick() {
uint32_t ticks = sysTickCount; // A single read access to sysTickCount
ticks = ticks + 1;
setLEDState( ticks < 500000L );
if ( ticks >= 1000000L ) {
ticks = 0;
}
sysTickCount = ticks; // A single write access to volatile sysTickCount
}
这在ISR中尤其有用,因为在ISR中,您知道不需要时,希望尽快不多次访问相同的硬件或内存,因为ISR运行时值不会改变。当ISR是变量值的“生产者”时,这很常见,sysTickCount
如上例所示。在AVR上,让函数doSysTick()
访问内存中相同的四个字节(四个指令=每次访问需要8个CPU周期sysTickCount
)五到六次而不是只有两次是特别痛苦的,因为程序员确实知道该值不会在他/她doSysTick()
运行时从其他代码更改。
使用此技巧,您实际上可以对非易失性变量执行与编译器完全相同的操作,即仅在必须时才从内存中读取它们,将值保留在寄存器中一段时间,然后仅在必须时将其写回内存中。 ; 但是这一次,您是否/何时必须进行读/写比编译器更了解,因此您可以使编译器免于执行此优化任务,并自己完成。
局限性 volatile
非原子访问
volatile
并没有提供多字变量的原子访问。对于这些情况,除了使用之外,您还需要通过其他方式提供互斥volatile
。在AVR,您可以使用ATOMIC_BLOCK
从<util/atomic.h>
或简单的cli(); ... sei();
电话。各个宏也充当内存屏障,这在访问顺序方面很重要:
执行顺序
volatile
仅对其他易失变量施加严格的执行顺序。这意味着,例如
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
保证首先给分配1 i
,然后给分配2 j
。但是,不能保证a
在两者之间进行分配。编译器可以在代码段之前或之后进行分配,基本上可以在任何时候进行,直到第一次读取(可见)a
。
如果不是因为上述宏的内存障碍,编译器将被允许翻译
uint32_t x;
cli();
x = volatileVar;
sei();
至
x = volatileVar;
cli();
sei();
要么
cli();
sei();
x = volatileVar;
(为了完整起见,我必须说volatile
,如果所有访问都用这些障碍括起来,那么像sei / cli宏所隐含的那些内存障碍实际上可能会避免使用。)