Definicja volatile
volatile
informuje kompilator, że wartość zmiennej może ulec zmianie bez wiedzy kompilatora. Dlatego kompilator nie może założyć, że wartość się nie zmieniła tylko dlatego, że program C wydaje się jej nie zmieniać.
Z drugiej strony oznacza to, że wartość zmiennej może być wymagana (odczyt) w innym miejscu, o którym kompilator nie wie, dlatego musi upewnić się, że każde przypisanie do zmiennej jest faktycznie wykonywane jako operacja zapisu.
Przypadków użycia
volatile
jest wymagane, gdy
- reprezentujący rejestry sprzętowe (lub I / O zamapowane w pamięci) jako zmienne - nawet jeśli rejestr nigdy nie zostanie odczytany, kompilator nie może po prostu pominąć operacji zapisu myśląc „Głupi programista. Próbuje zapisać wartość w zmiennej, którą on / ona” nigdy nie przeczyta. Nie zauważy nawet, jeśli pominiemy pisanie. ” I odwrotnie, nawet jeśli program nigdy nie zapisuje wartości zmiennej, jej wartość może nadal zostać zmieniona sprzętowo.
- współdzielenie zmiennych między kontekstami wykonania (np. ISR / główny program) (patrz odpowiedź @ kkramo)
Efekty volatile
Kiedy deklarowana jest zmienna, volatile
kompilator musi upewnić się, że każde przypisanie do niej w kodzie programu jest odzwierciedlone w rzeczywistej operacji zapisu, oraz że każdy odczyt w kodzie programu odczytuje wartość z (mmapped) pamięci.
W przypadku zmiennych nieulotnych kompilator zakłada, że wie, czy / kiedy zmienia się wartość zmiennej, i może optymalizować kod na różne sposoby.
Po pierwsze, kompilator może zmniejszyć liczbę odczytów / zapisów w pamięci, zachowując wartość w rejestrach procesora.
Przykład:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
Tutaj kompilator prawdopodobnie nawet nie przydzieli pamięci RAM dla result
zmiennej i nigdy nie będzie przechowywać wartości pośrednich nigdzie poza rejestrem procesora.
Jeśli result
był niestabilny, każde wystąpienie result
kodu C wymagałoby od kompilatora dostępu do pamięci RAM (lub portu I / O), co prowadziłoby do obniżenia wydajności.
Po drugie, kompilator może zmienić kolejność operacji na nielotnych zmiennych w celu zwiększenia wydajności i / lub rozmiaru kodu. Prosty przykład:
int a = 99;
int b = 1;
int c = 99;
można ponownie zamówić
int a = 99;
int c = 99;
int b = 1;
co może zapisać instrukcję asemblera, ponieważ wartość 99
nie musi być ładowana dwukrotnie.
Jeśli a
, b
i c
były niestabilne kompilator musiałby emitują instrukcji, które przypisać wartości w takiej kolejności, jak podano w programie.
Inny klasyczny przykład jest taki:
volatile uint8_t signal;
void waitForSignal() {
while ( signal == 0 ) {
// Do nothing.
}
}
Gdyby w tym przypadku signal
tak nie było volatile
, kompilator „pomyślałby”, że while( signal == 0 )
może to być nieskończona pętla (ponieważ signal
nigdy nie zostanie zmieniona przez kod wewnątrz pętli ) i może wygenerować odpowiednik
void waitForSignal() {
if ( signal != 0 ) {
return;
} else {
while(true) { // <-- Endless loop!
// do nothing.
}
}
}
Rozważne postępowanie z volatile
wartościami
Jak wspomniano powyżej, volatile
zmienna może wprowadzić karę wydajności, gdy jest uzyskiwana częściej niż jest to faktycznie wymagane. Aby złagodzić ten problem, można „nieulotną” wartość przez przypisanie do zmiennej nieulotnej, takiej jak
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
}
Może to być szczególnie korzystne w ISR, gdzie chcesz być jak najszybciej nie dostępu do tego samego sprzętu lub urządzeń pamięci kilka razy kiedy pan wie, że nie jest potrzebna, ponieważ wartość ta nie ulegnie zmianie w czasie, gdy Izrael jest uruchomiony. Jest to powszechne, gdy ISR jest „producentem” wartości zmiennej, jak sysTickCount
w powyższym przykładzie. W AVR byłoby szczególnie bolesne, gdyby funkcja miała doSysTick()
dostęp do tych samych czterech bajtów w pamięci (cztery instrukcje = 8 cykli procesora na dostęp sysTickCount
) pięć lub sześć razy zamiast tylko dwa razy, ponieważ programista wie, że wartość nie będzie zostać zmieniony z innego kodu podczas jego / jej doSysTick()
uruchomienia.
Dzięki tej sztuczce robisz dokładnie to samo, co kompilator robi dla zmiennych nieulotnych, tj. Odczytujesz je z pamięci tylko wtedy, gdy jest to konieczne, przechowujesz wartość w rejestrze przez pewien czas i zapisujesz w pamięci tylko wtedy, gdy trzeba ; ale tym razem, ty lepiej niż kompilator wiedzieć, jeśli / kiedy odczytu / zapisu musi się zdarzyć, więc zwalnia kompilator od tego zadania optymalizacji i zrobić to samemu.
Ograniczenia volatile
Dostęp bezatomowy
volatile
nie nie zapewniają dostęp do zmiennych atomowej multi-word. W takich przypadkach oprócz korzystania należy zapewnić wzajemne wykluczenie w inny sposób volatile
. W AVR możesz korzystać ATOMIC_BLOCK
z <util/atomic.h>
lub z prostych cli(); ... sei();
połączeń. Odpowiednie makra działają również jako bariera pamięci, co jest ważne, jeśli chodzi o kolejność dostępu:
Realizacja zamówienia
volatile
nakłada ścisłe polecenie wykonania tylko w odniesieniu do innych zmiennych zmiennych. Oznacza to na przykład, że
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
gwarantuje się najpierw przypisanie 1 do, i
a następnie przypisanie 2 do j
. Nie ma jednak gwarancji, że a
zostaną one przypisane pomiędzy; kompilator może wykonać to przypisanie przed fragmentem kodu lub po nim, w zasadzie w dowolnym momencie do pierwszego (widocznego) odczytu a
.
Gdyby nie bariera pamięci wyżej wspomnianych makr, kompilator byłby w stanie dokonać translacji
uint32_t x;
cli();
x = volatileVar;
sei();
do
x = volatileVar;
cli();
sei();
lub
cli();
sei();
x = volatileVar;
(Ze względu na kompletność muszę powiedzieć, że bariery pamięci, takie jak te sugerowane przez makra sei / cli, mogą faktycznie uniemożliwić użycie volatile
, jeśli wszystkie dostępy zostaną uzupełnione tymi barierami.)