的定义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
是 volatile,C 代码中的每次出现result
都需要编译器执行对 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 是变量值的“生产者”时,这很常见,就像sysTickCount
上面的例子一样。doSysTick()
在 AVR 上,让函数访问内存中相同的四个字节(四个指令 = 每次访问 8 个 CPU 周期)五到六次而不是仅仅两次会特别痛苦sysTickCount
,因为程序员确实知道该值不会在他/她doSysTick()
运行时从其他代码更改。
使用此技巧,您基本上可以执行编译器对非易失性变量所做的完全相同的操作,即仅在必须时从内存中读取它们,将值保存在寄存器中一段时间,并仅在必须时才写回内存; 但是这一次,如果/何时必须发生读/写,你比编译器更清楚,所以你把编译器从这个优化任务中解脱出来,自己做。
的限制volatile
非原子访问
volatile
不提供对多字变量的原子访问。对于这些情况,除了使用volatile
. 在 AVR 上,您可以使用ATOMIC_BLOCK
from<util/atomic.h>
或 simplecli(); ... sei();
调用。相应的宏也充当内存屏障,这在访问顺序方面很重要:
执行顺序
volatile
仅对其他 volatile 变量施加严格的执行顺序。这意味着,例如
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
保证先赋值 1i
再赋值2 j
。但是,不能保证a
会在两者之间分配;编译器可以在代码片段之前或之后执行该分配,基本上在任何时间直到第一次(可见)读取a
.
如果不是因为上述宏的内存屏障,编译器将被允许翻译
uint32_t x;
cli();
x = volatileVar;
sei();
到
x = volatileVar;
cli();
sei();
要么
cli();
sei();
x = volatileVar;
(为了完整起见,我必须说,内存屏障,就像 sei/cli 宏所暗示的那样,实际上可以避免使用volatile
,如果所有访问都用这些屏障括起来。)