Dlaczego jest volatile
potrzebny w C? Do czego jest to używane? Co to zrobi
Dlaczego jest volatile
potrzebny w C? Do czego jest to używane? Co to zrobi
Odpowiedzi:
Zmienna mówi kompilatorowi, aby nie optymalizował niczego, co ma związek ze zmienną zmienną.
Istnieją co najmniej trzy typowe powody, dla których warto go używać, wszystkie związane z sytuacjami, w których wartość zmiennej może się zmienić bez działania widocznego kodu: Kiedy łączysz się ze sprzętem, który zmienia samą wartość; gdy działa inny wątek, który również korzysta ze zmiennej; lub gdy istnieje moduł obsługi sygnału, który może zmienić wartość zmiennej.
Załóżmy, że masz mały sprzęt, który jest gdzieś zmapowany w pamięci RAM i ma dwa adresy: port poleceń i port danych:
typedef struct
{
int command;
int data;
int isbusy;
} MyHardwareGadget;
Teraz chcesz wysłać polecenie:
void SendCommand (MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
Wygląda na łatwe, ale może się nie powieść, ponieważ kompilator może dowolnie zmieniać kolejność zapisywania danych i poleceń. Spowodowałoby to, że nasz mały gadżet wydawałby polecenia z poprzednią wartością danych. Spójrz także na pętlę oczekiwania podczas zajętości. Ten zostanie zoptymalizowany. Kompilator spróbuje być sprytny, odczyta wartość isbusy tylko raz, a następnie przejdzie w nieskończoną pętlę. Nie tego chcesz.
Aby obejść ten problem, zadeklaruj gadżet wskaźnika jako niestabilny. W ten sposób kompilator jest zmuszony do robienia tego, co napisałeś. Nie może usuwać przypisań pamięci, nie może buforować zmiennych w rejestrach i nie może zmieniać kolejności przypisań:
To jest poprawna wersja:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
volatile
w C faktycznie powstało w celu nie buforowania wartości zmiennej automatycznie. Powie kompilatorowi, aby nie buforował wartości tej zmiennej. Wygeneruje więc kod, który pobierze wartość danej volatile
zmiennej z pamięci głównej za każdym razem, gdy ją napotka. Ten mechanizm jest używany, ponieważ w dowolnym momencie wartość może być modyfikowana przez system operacyjny lub dowolne przerwanie. Używanie volatile
pomoże nam za każdym razem uzyskać dostęp do wartości.
volatile
kompilatora było umożliwienie kompilatorom optymalizacji kodu, jednocześnie umożliwiając programistom osiągnięcie semantyki, która zostałaby osiągnięta bez takich optymalizacji. Autorzy Standardu oczekiwali, że implementacje jakościowe będą wspierać każdą semantykę, która byłaby użyteczna, biorąc pod uwagę ich docelowe platformy i pola aplikacji, i nie spodziewali się, że autorzy kompilatorów będą dążyć do zaoferowania semantyki o najniższej jakości, która jest zgodna ze Standardem i nie są w 100% głupie (zauważ, że autorzy Standardu wyraźnie uznają w uzasadnieniu ...
Innym zastosowaniem volatile
jest obsługa sygnałów. Jeśli masz taki kod:
int quit = 0;
while (!quit)
{
/* very small loop which is completely visible to the compiler */
}
Kompilator może zauważyć, że ciało pętli nie dotyka quit
zmiennej i przekształca pętlę w while (true)
pętlę. Nawet jeśli quit
zmienna jest ustawiona w module obsługi sygnałów dla SIGINT
i SIGTERM
; kompilator nie ma sposobu, aby to wiedzieć.
Jeśli jednak quit
zmienna zostanie zadeklarowana volatile
, kompilator będzie zmuszony ją ładować za każdym razem, ponieważ można ją zmodyfikować w innym miejscu. Właśnie tego chcesz w tej sytuacji.
quit
, kompilator może zoptymalizować ją do stałej pętli, zakładając, że że nie ma możliwości quit
zmiany między iteracjami. Uwaga: niekoniecznie jest to dobry zamiennik dla rzeczywistego programowania wątkowego.
volatile
lub innych znacznikach zakłada, że nic poza pętlą nie modyfikuje tej zmiennej po wejściu do pętli, nawet jeśli jest to zmienna globalna.
extern int global; void fn(void) { while (global != 0) { } }
z gcc -O3 -S
i spojrzenie na pliku wynikowego montażowej, na moim komputerze to robi movl global(%rip), %eax
; testl %eax, %eax
; je .L1
; .L4: jmp .L4
, czyli nieskończona pętla, jeśli globalny nie jest równy zero. Następnie spróbuj dodać volatile
i zobacz różnicę.
volatile
informuje kompilator, że zmienna może zostać zmieniona w inny sposób niż kod, który ma do niej dostęp. np. może to być lokalizacja pamięci odwzorowana we / wy. Jeśli nie zostanie to określone w takich przypadkach, niektóre zmienne dostępy można zoptymalizować, np. Jego zawartość może być przechowywana w rejestrze, a lokalizacja pamięci nie może zostać ponownie odczytana.
Zobacz ten artykuł Andrei Alexandrescu, „ volatile - Najlepszy przyjaciel wielowątkowego programisty ”
Lotny kluczowe został opracowany, aby zapobiec kompilatora, optymalizacji kodu, które mogłoby spowodować nieprawidłowe w obecności pewnych zdarzeń asynchronicznych. Na przykład, jeśli zadeklarujesz zmienną prymitywną jako zmienną , kompilator nie będzie mógł buforować jej w rejestrze - powszechna optymalizacja, która byłaby katastrofalna, gdyby zmienna ta była współużytkowana przez wiele wątków. Ogólna zasada jest taka, że jeśli masz zmienne typu pierwotnego, które muszą być współużytkowane przez wiele wątków, zadeklaruj te zmienne jako zmienne. Ale z tym słowem kluczowym możesz zrobić znacznie więcej: możesz go użyć do przechwycenia kodu, który nie jest bezpieczny dla wątków, i możesz to zrobić w czasie kompilacji. W tym artykule pokazano, jak to się robi; rozwiązanie obejmuje prosty inteligentny wskaźnik, który ułatwia także serializację krytycznych części kodu.
Artykuł dotyczy zarówno C
i C++
.
Zobacz także artykuł „ C ++ i niebezpieczeństwa podwójnie sprawdzonego blokowania ” Scott Meyers i Andrei Alexandrescu:
Tak więc w przypadku niektórych lokalizacji pamięci (np. Portów zmapowanych w pamięci lub pamięci, do których odwołują się ISR [Interrupt Service Routines]), niektóre optymalizacje muszą zostać zawieszone. zmienna istnieje w celu określenia specjalnego traktowania takich lokalizacji, w szczególności: (1) zawartość zmiennej lotnej jest „niestabilna” (może się zmieniać w sposób nieznany kompilatorowi), (2) wszystkie zapisy w danych lotnych są „obserwowalne”, więc muszą być wykonywane religijnie i (3) wszystkie operacje na danych ulotnych są wykonywane w kolejności, w jakiej występują w kodzie źródłowym. Dwie pierwsze zasady zapewniają prawidłowe czytanie i pisanie. Ostatni pozwala na implementację protokołów I / O, które łączą wejścia i wyjścia. Jest to nieformalnie to, co niestabilne gwarancje C i C ++.
volatile
nie gwarantuje atomowości.
Moje proste wyjaśnienie to:
W niektórych scenariuszach, na podstawie logiki lub kodu, kompilator dokona optymalizacji zmiennych, które jego zdaniem nie ulegną zmianie. W volatile
zapobiega kluczowych zmienne są zoptymalizowane.
Na przykład:
bool usb_interface_flag = 0;
while(usb_interface_flag == 0)
{
// execute logic for the scenario where the USB isn't connected
}
Z powyższego kodu kompilator może pomyśleć, że usb_interface_flag
jest zdefiniowany jako 0 i że w pętli while na zawsze będzie zero. Po optymalizacji kompilator będzie go traktował jak while(true)
cały czas, co spowoduje powstanie nieskończonej pętli.
Aby uniknąć tego rodzaju scenariuszy, deklarujemy flagę jako niestabilną, informujemy kompilator, że ta wartość może zostać zmieniona przez zewnętrzny interfejs lub inny moduł programu, tzn. Nie optymalizuj jej. To jest przypadek użycia lotnych.
Krańcowe zastosowanie substancji lotnych jest następujące. Powiedzmy, że chcesz obliczyć pochodną numeryczną funkcji f
:
double der_f(double x)
{
static const double h = 1e-3;
return (f(x + h) - f(x)) / h;
}
Problem w tym, że x+h-x
generalnie nie jest równy z h
powodu błędów zaokrągleń. Pomyśl o tym: odejmując bardzo bliskie liczby, tracisz wiele znaczących cyfr, które mogą zrujnować obliczanie pochodnej (pomyśl 1.00001-1). Możliwym obejściem może być
double der_f2(double x)
{
static const double h = 1e-3;
double hh = x + h - x;
return (f(x + hh) - f(x)) / hh;
}
ale w zależności od platformy i przełączników kompilatora druga linia tej funkcji może zostać usunięta przez agresywnie optymalizujący kompilator. Zamiast tego piszesz
volatile double hh = x + h;
hh -= x;
aby zmusić kompilator do odczytania lokalizacji pamięci zawierającej hh, rezygnując z ewentualnej możliwości optymalizacji.
h
lub hh
w formule pochodnej? Kiedy hh
jest obliczana, ostatnia formuła używa jej jak pierwszej, bez różnicy. Może powinno być (f(x+h) - f(x))/hh
?
h
i hh
polega na tym, że hh
operacja ta obcina do pewnej ujemnej potęgi dwóch x + h - x
. W tym przypadku x + hh
i x
różnią się dokładnie o hh
. Możesz również wziąć swoją formułę, da to ten sam wynik, ponieważ x + h
i x + hh
są równe (to mianownik jest tutaj ważny).
x1=x+h; d = (f(x1)-f(x))/(x1-x)
? bez użycia substancji lotnych.
-ffast-math
lub równoważne.
Istnieją dwa zastosowania. Są one częściej używane częściej w programowaniu wbudowanym.
Kompilator nie zoptymalizuje funkcji wykorzystujących zmienne zdefiniowane za pomocą niestabilnego słowa kluczowego
Funkcja Volatile służy do uzyskiwania dostępu do dokładnych lokalizacji pamięci w pamięci RAM, ROM itp. Jest używana częściej do kontrolowania mapowanych urządzeń, uzyskiwania dostępu do rejestrów procesora i lokalizowania określonych lokalizacji pamięci.
Zobacz przykłady z listą zestawów. Re: Użycie słowa kluczowego „niestabilnego” w programowaniu wbudowanym
Lotny jest również przydatny, gdy chcesz zmusić kompilator do nieoptymalizowania określonej sekwencji kodu (np. Do napisania mikroprocesora).
Wspomnę o innym scenariuszu, w którym substancje lotne są ważne.
Załóżmy, że mapujesz pamięć pliku, aby uzyskać szybsze operacje we / wy, a plik ten może ulec zmianie za kulisami (np. Plik nie znajduje się na lokalnym dysku twardym, ale jest obsługiwany przez sieć przez inny komputer).
Jeśli uzyskasz dostęp do danych pliku odwzorowanego w pamięci za pośrednictwem wskaźników do obiektów nieulotnych (na poziomie kodu źródłowego), kod wygenerowany przez kompilator może pobrać te same dane wiele razy, nie zdając sobie z tego sprawy.
Jeśli te dane się zmienią, Twój program może korzystać z dwóch lub więcej różnych wersji danych i przejść w niespójny stan. Może to prowadzić nie tylko do logicznie niepoprawnego zachowania programu, ale także do luk w zabezpieczeniach, które mogą zostać wykorzystane, jeśli przetwarza niezaufane pliki lub pliki z niezaufanych lokalizacji.
Jeśli zależy Ci na bezpieczeństwie i powinieneś, jest to ważny scenariusz do rozważenia.
„niestabilna” oznacza, że pamięć prawdopodobnie zmieni się w dowolnym momencie i zostanie zmieniona, ale coś poza kontrolą programu użytkownika. Oznacza to, że jeśli odwołujesz się do zmiennej, program powinien zawsze sprawdzać adres fizyczny (tj. Mapowane wejście fifo) i nie używać go w pamięci podręcznej.
Wiki mówi wszystko o volatile
:
Dokumentacja jądra Linuksa stanowi również doskonały zapis na temat volatile
:
Moim zdaniem nie należy oczekiwać zbyt wiele volatile
. Aby to zilustrować, spójrz na przykład w wysoko głosowanej odpowiedzi Nilsa Pipenbrincka .
Powiedziałbym, że jego przykład nie jest odpowiedni volatile
. volatile
służy tylko do:
uniemożliwienia kompilatorowi wykonania przydatnych i pożądanych optymalizacji . Nie ma nic o bezpieczeństwie wątku, dostępie atomowym, a nawet porządku pamięci.
W tym przykładzie:
void SendCommand (volatile MyHardwareGadget * gadget, int command, int data)
{
// wait while the gadget is busy:
while (gadget->isbusy)
{
// do nothing here.
}
// set data first:
gadget->data = data;
// writing the command starts the action:
gadget->command = command;
}
gadget->data = data
przed gadget->command = command
tylko jest zagwarantowana tylko w skompilowanego kodu przez kompilator. W czasie wykonywania procesor nadal prawdopodobnie zmienia kolejność danych i przypisywanie poleceń w odniesieniu do architektury procesora. Sprzęt może uzyskać nieprawidłowe dane (załóżmy, że gadżet jest odwzorowany na sprzętowe We / Wy). Bariera pamięci jest potrzebna między przypisaniem danych a poleceniami.
volatile
obniża wydajność bez powodu. To, czy będzie wystarczające, będzie zależeć od innych aspektów systemu, o których programiści mogą wiedzieć więcej niż kompilator. Z drugiej strony, jeśli procesor gwarantuje, że instrukcja zapisu pod określonym adresem opróżni pamięć podręczną procesora, ale kompilator nie zapewnił możliwości opróżnienia zmiennych buforowanych w rejestrze, o których procesor nic nie wie, opróżnienie pamięci podręcznej byłoby bezużyteczne.
W języku zaprojektowanym przez Dennisa Ritchiego każdy dostęp do dowolnego obiektu, z wyjątkiem automatycznych obiektów, których adres nie został pobrany, zachowywałby się tak, jakby obliczał adres obiektu, a następnie czytał lub zapisywał pamięć pod tym adresem. To sprawiło, że język był bardzo mocny, ale znacznie ograniczył możliwości optymalizacji.
Chociaż możliwe byłoby dodanie kwalifikatora, który zachęciłby kompilator do założenia, że dany obiekt nie zostanie zmieniony w dziwny sposób, takie założenie byłoby odpowiednie dla zdecydowanej większości obiektów w programach C i miałoby niepraktyczne było dodanie kwalifikatora do wszystkich obiektów, dla których takie założenie byłoby odpowiednie. Z drugiej strony niektóre programy muszą korzystać z niektórych obiektów, dla których takie założenie nie miałoby miejsca. Aby rozwiązać ten problem, Standard mówi, że kompilatory mogą założyć, że obiekty, które nie zostały zadeklarowane volatile
, nie będą miały obserwowanej lub zmienianej wartości w sposób, który jest poza kontrolą kompilatora lub będzie poza rozsądnym zrozumieniem kompilatora.
Ponieważ różne platformy mogą mieć różne sposoby obserwowania lub modyfikowania obiektów poza kontrolą kompilatora, właściwe jest, aby kompilatory jakości dla tych platform różniły się pod względem dokładnej obsługi volatile
semantyki. Niestety, ponieważ Standard nie zasugerował, że kompilatory jakości przeznaczone do programowania niskopoziomowego na platformie powinny obsługiwać volatile
w sposób, który rozpoznaje wszelkie odpowiednie efekty konkretnej operacji odczytu / zapisu na tej platformie, wiele kompilatorów nie radzi sobie więc w sposób, który utrudnia przetwarzanie takich operacji, jak We / Wy w tle, w sposób, który jest wydajny, ale nie może zostać zepsuty przez „optymalizacje” kompilatora.
Mówiąc najprościej, informuje kompilator, aby nie przeprowadzał żadnej optymalizacji konkretnej zmiennej. Zmienne mapowane do rejestru urządzenia są modyfikowane pośrednio przez urządzenie. W takim przypadku należy użyć substancji lotnej.
Zmienny można zmienić spoza skompilowanego kodu (na przykład program może zmapować zmienną zmienną do rejestru odwzorowanego w pamięci). Kompilator nie zastosuje pewnych optymalizacji w kodzie, który obsługuje zmienną zmienną - na przykład wygrał załaduj go do rejestru bez zapisywania go w pamięci. Jest to ważne w przypadku rejestrów sprzętowych.
Jak słusznie sugeruje wielu tutaj, popularnym zastosowaniem lotnego słowa kluczowego jest pominięcie optymalizacji zmiennej lotnej.
Najlepszą korzyścią, która przychodzi mi do głowy i o której warto wspomnieć po przeczytaniu o niestabilności, jest - zapobieganie cofnięciu zmiennej w przypadku longjmp
. Nielokalny skok.
Co to znaczy?
Oznacza to po prostu, że ostatnia wartość zostanie zachowana po cofnięciu stosu , aby powrócić do poprzedniej ramki stosu; zazwyczaj w przypadku jakiegoś błędnego scenariusza.
Ponieważ byłoby to poza zakresem tego pytania, nie będę tu wchodził w szczegóły setjmp/longjmp
, ale warto o tym przeczytać; i jak można wykorzystać funkcję zmienności do zachowania ostatniej wartości.