Dlaczego lotność jest potrzebna w C?


Odpowiedzi:


423

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;
    }

46
Osobiście wolę rozmiar całkowity od dokładności, np. Int8 / int16 / int32 podczas rozmowy ze sprzętem. Ładna odpowiedź;)
tonylo

22
tak, powinieneś zadeklarować rzeczy ze stałym rozmiarem rejestru, ale hej - to tylko przykład.
Nils Pipenbrinck

69
Zmienna jest również potrzebna w kodzie wątkowym, gdy grasz z danymi, które nie są chronione przed współbieżnością. I tak, są na to odpowiednie czasy, możesz na przykład napisać bezpieczną dla wątku kolejkę komunikatów cyklicznych bez potrzeby jawnej ochrony przed współbieżnością, ale będzie ona wymagać składników lotnych.
Gordon Wrigley,

14
Przeczytaj specyfikację C mocniej. Zmienna ma tylko określone zachowanie we / wy urządzenia zamapowanego w pamięci lub w pamięci dotkniętej przez asynchroniczną funkcję przerywającą. Nic nie mówi o wątkach, a kompilator, który optymalizuje dostęp do pamięci dotykany przez wiele wątków, jest zgodny.
ephemient

17
@tolomea: całkowicie źle. smutne 17 osób nie wie o tym. lotny nie jest płotem pamięci. wiąże się to jedynie z unikaniem eliminacji kodu podczas optymalizacji w oparciu o założenie niewidocznych efektów ubocznych .
v.oddou

187

volatilew 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 volatilezmiennej 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 volatilepomoże nam za każdym razem uzyskać dostęp do wartości.


Zostało stworzone? Czy „lotny” nie został pierwotnie zapożyczony z C ++? Wydaje mi się, że pamiętam ...
składniaerror

Nie jest to wcale niestabilne - zabrania również niektórych zmian kolejności, jeśli jest określone jako niestabilne ..
FaceBro

4
@FaceBro: Celem volatilekompilatora 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 ...
supercat

1
... że implementacja może być zgodna bez wystarczającej jakości, aby faktycznie była odpowiednia do dowolnego celu, ale nie uważała za konieczne, aby temu zapobiec).
supercat

1
@syntaxerror, w jaki sposób można go wypożyczyć z C ++, gdy C był o ponad dekadę starszy od C ++ (zarówno w pierwszych wydaniach, jak i pierwszych standardach)?
phuclv,

178

Innym zastosowaniem volatilejest 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 quitzmiennej i przekształca pętlę w while (true)pętlę. Nawet jeśli quitzmienna jest ustawiona w module obsługi sygnałów dla SIGINTi SIGTERM; kompilator nie ma sposobu, aby to wiedzieć.

Jeśli jednak quitzmienna 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.


kiedy mówisz: „kompilator jest zmuszany do wczytywania go za każdym razem, to tak, jakby kompilator decydował się zoptymalizować określoną zmienną, a my nie deklarujemy zmiennej jako niestabilnej, w czasie wykonywania ta zmienna jest ładowana do rejestrów procesora nie znajdujących się w pamięci ?
Amit Singh Tomar,

1
@AmitSinghTomar Oznacza to, co mówi: Za każdym razem, gdy kod sprawdza wartość, jest ponownie ładowana. W przeciwnym razie kompilator może założyć, że funkcje, które nie odwołują się do zmiennej, nie mogą jej modyfikować, więc zakładając, że CesarB zamierzał nie ustawić powyższej pętli quit, kompilator może zoptymalizować ją do stałej pętli, zakładając, że że nie ma możliwości quitzmiany między iteracjami. Uwaga: niekoniecznie jest to dobry zamiennik dla rzeczywistego programowania wątkowego.
underscore_d

jeśli quit jest zmienną globalną, to kompilator nie będzie optymalizował pętli while, prawda?
Pierre G.

2
@PierreG. Nie, kompilator zawsze może zakładać, że kod jest jednowątkowy, chyba że podano inaczej. Oznacza to, że przy braku volatilelub 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.
CesarB

1
@PierreG. Tak, na przykład spróbować kompilacji extern int global; void fn(void) { while (global != 0) { } }z gcc -O3 -Si 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ć volatilei zobacz różnicę.
CesarB

60

volatileinformuje 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.


30

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 Ci 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 ++.


Czy standard określa, czy odczyt jest uważany za „obserwowalne zachowanie”, jeśli wartość nigdy nie jest używana? Mam wrażenie, że tak powinno być, ale kiedy twierdziłem, że to gdzie indziej, ktoś rzucił mi wyzwanie za cytowanie. Wydaje mi się, że na każdej platformie, na której odczyt zmiennej lotnej mógłby mieć jakikolwiek wpływ, kompilator powinien wymagać wygenerowania kodu, który wykonuje każdy wskazany odczyt dokładnie raz; bez tego wymogu trudno byłoby napisać kod, który wygenerował przewidywalną sekwencję odczytów.
supercat

@ superupat: Zgodnie z pierwszym artykułem: „Jeśli użyjesz zmiennego modyfikatora zmiennej, kompilator nie buforuje tej zmiennej w rejestrach - każdy dostęp uderzy w rzeczywistą lokalizację pamięci tej zmiennej”. Ponadto w sekcji §6.7.3.6 standardu c99 napisano: „Obiekt o typie lotnym może zostać zmodyfikowany w sposób nieznany implementacji lub mieć inne nieznane skutki uboczne”. Oznacza to ponadto, że zmienne lotne nie mogą być buforowane w rejestrach i że wszystkie odczyty i zapisy muszą być wykonywane w kolejności względem punktów sekwencji, aby były w rzeczywistości możliwe do zaobserwowania.
Robert S. Barnes

Ten ostatni artykuł rzeczywiście wyraźnie stwierdza, że ​​odczyty są efektami ubocznymi. Ten pierwszy wskazuje, że odczytów nie można wykonać poza kolejnością, ale nie wyklucza to możliwości ich całkowitego pominięcia.
supercat

„kompilatorowi nie wolno buforować go w rejestrze” - większość architektur RISC to maszyny rejestrujące, więc każdy odczyt-modyfikacja-zapis musi buforować obiekt w rejestrach. volatilenie gwarantuje atomowości.
zbyt uczciwy dla tej strony

1
@Olaf: Ładowanie czegoś do rejestru to nie to samo, co buforowanie. Buforowanie wpłynęłoby na liczbę ładunków lub sklepów lub ich czas.
supercat

28

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 volatilezapobiega 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_flagjest 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.


19

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-xgeneralnie nie jest równy z hpowodu 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.


Jaka jest różnica między użyciem hlub hhw formule pochodnej? Kiedy hhjest obliczana, ostatnia formuła używa jej jak pierwszej, bez różnicy. Może powinno być (f(x+h) - f(x))/hh?
Sergey Zhukov,

2
Różnica między hi hhpolega na tym, że hhoperacja ta obcina do pewnej ujemnej potęgi dwóch x + h - x. W tym przypadku x + hhi xróżnią się dokładnie o hh. Możesz również wziąć swoją formułę, da to ten sam wynik, ponieważ x + hi x + hhsą równe (to mianownik jest tutaj ważny).
Alexandre C.

3
Czy nie byłoby bardziej czytelnym sposobem na napisanie tego x1=x+h; d = (f(x1)-f(x))/(x1-x)? bez użycia substancji lotnych.
Sergey Zhukov

Jakieś odniesienie, że kompilator może wyczyścić ten drugi wiersz funkcji?
CoffeeTableEspresso

@CoffeeTableEspresso: Nie, przepraszam. Im więcej wiem o liczbach zmiennoprzecinkowych, tym bardziej uważam, że kompilator może go zoptymalizować tylko wtedy, gdy zostanie to wyraźnie podane, z -ffast-mathlub równoważne.
Alexandre C.,

11

Istnieją dwa zastosowania. Są one częściej używane częściej w programowaniu wbudowanym.

  1. Kompilator nie zoptymalizuje funkcji wykorzystujących zmienne zdefiniowane za pomocą niestabilnego słowa kluczowego

  2. 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


„Kompilator nie zoptymalizuje funkcji wykorzystujących zmienne zdefiniowane za pomocą niestabilnego słowa kluczowego” - to po prostu źle.
zbyt uczciwy jak na tę stronę

10

Lotny jest również przydatny, gdy chcesz zmusić kompilator do nieoptymalizowania określonej sekwencji kodu (np. Do napisania mikroprocesora).


10

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.


7

„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.


Żaden kompilator nie jest zmienny w znaczeniu „fizyczny adres w pamięci RAM” lub „pomijanie pamięci podręcznej”.
ciekawy


5

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. volatilesł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 = dataprzed gadget->command = commandtylko 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.


2
Powiedziałbym, że niestabilna jest używana, aby uniemożliwić kompilatorowi dokonywanie optymalizacji, które normalnie byłyby przydatne i pożądane. Jak napisano, wygląda na to, że volatileobniż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.
supercat

5

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 volatilesemantyki. Niestety, ponieważ Standard nie zasugerował, że kompilatory jakości przeznaczone do programowania niskopoziomowego na platformie powinny obsługiwać volatilew 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.


5

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.


1
Czy w tej odpowiedzi jest coś nowego, o czym nie wspomniano wcześniej?
slfan

3

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.


0

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.


-2

nie pozwala kompilatorowi na automatyczną zmianę wartości zmiennych. zmienna lotna służy do użycia dynamicznego.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.