Визначення volatile
volatile
повідомляє компілятору, що значення змінної може змінюватися, не знаючи компілятора. Отже, компілятор не може припустити, що значення не змінилося лише тому, що програма C, здається, не змінила його.
З іншого боку, це означає, що значення змінної може знадобитися (читати) десь ще, про який компілятор не знає, отже, він повинен переконатися, що кожне присвоєння змінній насправді виконується як операція запису.
Використовуйте випадки
volatile
потрібно, коли
- представляючи регістри апаратних засобів (або вбудований введення / виведення з картою пам'яті) як змінні - навіть якщо реєстр ніколи не буде прочитаний, компілятор не повинен просто пропускати операцію запису, думаючи: "Дурний програміст. Намагається зберігати значення в змінній, яку він / вона" він ніколи не прочитає назад. Він / вона навіть не помітить, якщо ми пропустимо написання ". І навпаки, навіть якщо програма ніколи не записує значення в змінну, її значення все одно може бути змінено апаратними засобами.
- обмін змінними між контекстами виконання (наприклад, ISR / основна програма) (див. відповідь @ kkramo)
Ефекти volatile
Коли оголошена змінна, volatile
компілятор повинен переконатися, що кожне присвоєння їй у програмному коді відображається в дійсній операції запису і що кожне прочитане в програмному коді зчитує значення з (mmapped) пам'яті.
Для енергонезалежних змінних компілятор припускає, що він знає, чи / коли значення змінної змінюється, і може оптимізувати код різними способами.
Для одного, компілятор може зменшити кількість читання / запису в пам'яті, зберігаючи значення в регістрах процесора.
Приклад:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
Тут компілятор, ймовірно, навіть не виділяє оперативну пам’ять для result
змінної і ніколи не зберігатиме проміжні значення ніде, а не в регістрі процесора.
Якщо result
він мінливий, кожне виникнення result
коду С вимагає від компілятора здійснити доступ до оперативної пам'яті (або порту вводу / виводу), що призводить до зниження продуктивності.
По-друге, компілятор може переупорядкувати операції над енергонезалежними змінними для продуктивності та / або розміру коду. Простий приклад:
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
у наведеному вище прикладі. На AVR було б особливо болісно мати функцію doSysTick()
доступу до тих самих чотирьох байтів у пам'яті (чотири інструкції = 8 циклів процесора на доступ до 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;
(Для повноти я мушу сказати, що бар'єри пам’яті, як і ті, що маються на увазі макроси sei / cli, насправді можуть унеможливити використання volatile
, якщо всі звернення закреслені цими бар'єрами.)