Istnieje intrygująca technika, która sprawia, że Twój kompilator wykrywa możliwe warunki wyścigu, w dużym stopniu zależne od niestabilnego słowa kluczowego, o którym możesz przeczytać na stronie http://www.ddj.com/cpp/184403766 .
Używam go do blokowania kodu bez blokady / podwójnej kontroli
paulm
Dla mnie volatilebardziej przydatne niż friendsłowo kluczowe.
acegs,
Odpowiedzi:
268
volatile jest potrzebny, jeśli czytasz z miejsca w pamięci, które, powiedzmy, całkowicie oddzielny proces / urządzenie / cokolwiek, do czego może pisać.
Kiedyś pracowałem z podwójnym portem RAM w systemie wieloprocesorowym w prostej C. Użyliśmy sprzętowo zarządzanej 16-bitowej wartości jako semafora, aby wiedzieć, kiedy skończył się drugi facet. Zasadniczo zrobiliśmy to:
void waitForSemaphore(){volatileuint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/while((*semPtr)!= IS_OK_FOR_ME_TO_PROCEED);}
Bez volatiletego optymalizator uważa pętlę za bezużyteczną (facet nigdy nie ustawia wartości! On jest szalony, pozbywa się tego kodu!), A mój kod działałby bez uzyskania semafora, powodując problemy później.
W takim przypadku, co by się stało, gdyby uint16_t* volatile semPtrzamiast tego napisano? Powinno to oznaczać wskaźnik jako niestabilny (zamiast wskazywanej wartości), tak że kontrole samego wskaźnika, np. semPtr == SOME_ADDRMogą nie zostać zoptymalizowane. Implikuje to jednak również ponownie zmienną wartość punktową. Nie?
Zyl
@Zyl Nie, nie ma. W praktyce prawdopodobnie sugerujesz, co się stanie. Ale teoretycznie można skończyć z kompilatorem, który optymalizuje dostęp do wartości, ponieważ zdecydował, że żadna z tych wartości nigdy się nie zmieni. A jeśli miałeś na myśli niestabilność w odniesieniu do wartości, a nie wskaźnika, byłbyś przykręcony. Ponownie, mało prawdopodobne, ale lepiej jest popsuć się robieniem rzeczy, niż skorzystać z zachowań, które zdarzają się dzisiaj.
@curiousguy nie zdecydował się źle. Dokonał prawidłowego odliczenia na podstawie podanych informacji. Jeśli nie oznaczysz czegoś niestabilnego, kompilator może założyć, że nie jest niestabilny . To właśnie robi kompilator podczas optymalizacji kodu. Jeśli jest więcej informacji, a mianowicie, że wspomniane dane są w rzeczywistości niestabilne, odpowiedzialność za dostarczenie tych informacji ponosi programista. To, czego twierdzisz przez błędny kompilator, to po prostu złe programowanie.
iheanyi
1
@curiousguy nie, tylko dlatego, że słowo zmienne pojawia się raz, nie oznacza, że wszystko nagle staje się niestabilne. Podałem scenariusz, w którym kompilator robi to, co właściwe i osiąga wynik, który jest sprzeczny z tym, czego błędnie oczekuje programista. Podobnie jak „najbardziej dokuczliwa analiza” nie jest oznaką błędu kompilatora, tak też nie jest w tym przypadku.
iheanyi
82
volatilejest potrzebny podczas opracowywania systemów wbudowanych lub sterowników urządzeń, w których należy odczytać lub zapisać urządzenie sprzętowe zamapowane w pamięci. Zawartość konkretnego rejestru urządzenia może ulec zmianie w dowolnym momencie, dlatego potrzebujesz volatilesłowa kluczowego, aby kompilator nie zoptymalizował takich dostępów.
Dotyczy to nie tylko systemów wbudowanych, ale także wszystkich sterowników sterowników.
Mladen Janković
Jedyny raz, kiedy potrzebowałem go na 8-bitowej magistrali ISA, gdzie dwa razy czytałeś ten sam adres - kompilator miał błąd i zignorował go (wczesny Zortech c ++)
Martin Beckett
Lotne bardzo rzadko jest odpowiednie do sterowania urządzeniami zewnętrznymi. Jego semantyka jest niewłaściwa dla współczesnego MMIO: musisz sprawić, by zbyt wiele obiektów było niestabilnych, a to boli optymalizację. Ale nowoczesne MMIO zachowuje się jak normalna pamięć, dopóki nie zostanie ustawiona flaga, więc zmienność nie powinna być potrzebna. Wielu kierowców nigdy nie używa lotnych.
ciekawy,
69
Niektóre procesory mają rejestry zmiennoprzecinkowe, które mają więcej niż 64 bity dokładności (np. 32-bitowy x86 bez SSE, patrz komentarz Petera). W ten sposób, jeśli uruchomisz kilka operacji na liczbach podwójnej precyzji, faktycznie uzyskasz odpowiedź o wyższej precyzji, niż gdybyś obciął każdy wynik pośredni do 64 bitów.
Jest to zwykle świetne, ale oznacza to, że w zależności od tego, w jaki sposób kompilator przypisał rejestry i dokonał optymalizacji, uzyskasz różne wyniki dla dokładnie tych samych operacji na dokładnie tych samych danych wejściowych. Jeśli potrzebujesz spójności, możesz zmusić każdą operację do powrotu do pamięci za pomocą niestabilnego słowa kluczowego.
Jest to również przydatne w przypadku niektórych algorytmów, które nie mają sensu algebraicznego, ale zmniejszają błąd zmiennoprzecinkowy, na przykład sumowanie Kahana. Algebraicznie jest to nop, więc często będzie niepoprawnie zoptymalizowane, chyba że niektóre zmienne pośrednie są zmienne.
Przy obliczaniu pochodnych numerycznych jest również przydatne, aby upewnić się, że x + h - x == h zdefiniujesz hh = x + h - x jako zmienną, aby można było obliczyć odpowiednią deltę.
Alexandre C.
5
+1, rzeczywiście z mojego doświadczenia wynika, że obliczenia zmiennoprzecinkowe dały różne wyniki w Debugowaniu i Wydawaniu, więc testy jednostkowe napisane dla jednej konfiguracji zakończyły się niepowodzeniem dla innej. Rozwiązaliśmy go, zadeklarując jedną zmienną zmiennoprzecinkową jako volatile doublezamiast po prostu double, aby upewnić się, że jest ona skracana z precyzji FPU do 64-bitowej (RAM) przed kontynuowaniem dalszych obliczeń. Wyniki były zasadniczo różne ze względu na dalszą przesadę błędu zmiennoprzecinkowego.
Serge Rogatch,
Twoja definicja „nowoczesna” jest trochę nieprecyzyjna. Wpływa na to tylko 32-bitowy kod x86, który omija SSE / SSE2, i nie był „nowoczesny” nawet 10 lat temu. Wszystkie MIPS / ARM / POWER mają 64-bitowe rejestry sprzętowe, podobnie jak x86 z SSE2. Implementacje C ++ x86-64 zawsze używają SSE2, a kompilatory mają opcje takie jak g++ -mfpmath=sseużycie go również dla 32-bitowego x86. Można używać gcc -ffloat-storedo wymuszenia zaokrąglania wszędzie , nawet podczas korzystania z x87, czy można ustawić precyzję x87 dla 53-bitowej mantysy: randomascii.wordpress.com/2012/03/21/... .
Peter Cordes
Ale wciąż dobra odpowiedź, w przypadku przestarzałego kodu x87 można użyć volatilewymuszania zaokrąglania w kilku konkretnych miejscach bez utraty korzyści wszędzie.
Peter Cordes
1
Czy też mylę niedokładne z niekonsekwentnym?
Chipster
49
Z artykułu „Zmienna jak obietnica” Dana Saksa:
(...) obiekt lotny to taki, którego wartość może ulec spontanicznej zmianie. Oznacza to, że kiedy deklarujesz, że obiekt jest ulotny, informujesz kompilator, że obiekt może zmienić stan, nawet jeśli nie wydają się go zmieniać żadne instrukcje w programie. ”
Oto linki do trzech jego artykułów dotyczących volatilesłowa kluczowego:
MUSISZ używać niestabilnych podczas implementowania struktur danych bez blokady. W przeciwnym razie kompilator może optymalizować dostęp do zmiennej, która zmieni semantykę.
Innymi słowy, volatile mówi kompilatorowi, który uzyskuje dostęp do tej zmiennej, musi odpowiadać operacji odczytu / zapisu w pamięci fizycznej.
Na przykład tak deklaruje się InterlockedIncrement w API Win32:
LONG __cdecl InterlockedIncrement(
__inout LONG volatile*Addend);
Absolutnie NIE musisz deklarować zmiennej lotnej, aby móc korzystać z InterlockedIncrement.
ciekawy
Ta odpowiedź jest już nieaktualna, ponieważ C ++ 11 umożliwia std::atomic<LONG>bezpieczniejsze pisanie kodu bez blokady, bez problemów z optymalizacją czystych ładowań / czystych sklepów, zmianą kolejności lub cokolwiek innego.
Peter Cordes
10
Duża aplikacja, nad którą pracowałem na początku lat 90., zawierała obsługę wyjątków opartych na języku C przy użyciu setjmp i longjmp. Zmienne słowo kluczowe było konieczne w przypadku zmiennych, których wartości musiały zostać zachowane w bloku kodu, który służył jako klauzula „catch”, aby te zmienne nie zostały zapisane w rejestrach i usunięte przez longjmp.
W standardzie C jednym z miejsc do użycia volatilejest moduł obsługi sygnału. W rzeczywistości w standardzie C wszystko, co można bezpiecznie zrobić w module obsługi sygnału, to zmodyfikować volatile sig_atomic_tzmienną lub szybko wyjść. Rzeczywiście, AFAIK, jest to jedyne miejsce w standardzie C, którego użycie volatilejest wymagane, aby uniknąć nieokreślonego zachowania.
ISO / IEC 9899: 2011 §7.14.1.1 signal Funkcja
¶5 Jeśli sygnał występuje inaczej niż w wyniku wywołania funkcji abortlub raise, zachowanie jest niezdefiniowane, jeśli procedura obsługi sygnału odnosi się do dowolnego obiektu o statycznym lub czasowym przechowywaniu wątku, który nie jest obiektem atomowym bez blokady, innym niż poprzez przypisanie wartości do obiektu zadeklarowanego jako volatile sig_atomic_tlub procedura obsługi sygnału wywołuje dowolną funkcję w bibliotece standardowej inną niż abortfunkcja, _Exitfunkcja,
quick_exitfunkcja lub signalfunkcja z pierwszym argumentem równym liczbie sygnałów odpowiadającej sygnałowi, który spowodował wywołanie treser. Ponadto, jeśli takie wywołanie signalfunkcji powoduje powrót SIG_ERR, wartość errnojest nieokreślona. 252)
252) Jeśli asynchroniczny moduł obsługi sygnału generuje jakikolwiek sygnał, zachowanie jest niezdefiniowane.
POSIX jest znacznie łagodniejszy w kwestii tego, co można zrobić w procedurze obsługi sygnałów, ale wciąż istnieją ograniczenia (jednym z ograniczeń jest to, że printf()nie można bezpiecznie używać standardowej biblioteki we / wy - i innych).
Opracowując dla osadzonego, mam pętlę, która sprawdza zmienną, którą można zmienić w module obsługi przerwań. Bez „niestabilnej” pętla staje się noop - o ile kompilator może powiedzieć, zmienna nigdy się nie zmienia, więc optymalizuje sprawdzenie.
To samo dotyczyłoby zmiennej, która może zostać zmieniona w innym wątku w bardziej tradycyjnym środowisku, ale tam często wykonujemy wywołania synchronizacji, więc kompilator nie jest tak wolny od optymalizacji.
Oprócz używania go zgodnie z przeznaczeniem, lotny jest wykorzystywany w metaprogramowaniu (szablonowym). Można go użyć, aby zapobiec przypadkowemu przeładowaniu, ponieważ zmienny atrybut (jak const) bierze udział w rozwiązywaniu problemu przeciążenia.
To jest legalne; oba przeciążenia są potencjalnie wywoływalne i robią prawie to samo. Obsada w volatileprzeciążeniu jest legalna, ponieważ wiemy, że pasek i tak nie przejdzie nieulotnego T. volatileWersja jest surowo gorzej, choć, więc nigdy wybrany w rozdzielczości przeciążenia jeśli nieulotnaf jest dostępna.
Pamiętaj, że kod nigdy nie zależy od volatiledostępu do pamięci.
Lepiej jest użyć biblioteki, wbudowanego kompilatora lub wbudowanego kodu asemblera. Lotne jest zawodne.
Zan Lynx
1
Zarówno 1, jak i 2 wykorzystują operacje atomowe, ale lotność nie zapewnia semantyki atomowej, a implementacje atomowe specyficzne dla platformy zastąpią potrzebę użycia lotności, więc w przypadku 1 i 2, nie zgadzam się, NIE potrzebujesz do nich lotności.
Kto mówi cokolwiek o niestabilności zapewniającej semantykę atomową? Powiedziałem, że musisz UŻYWAĆ lotnych Z operacjami atomowymi, a jeśli nie uważasz, że to prawda, spójrz na deklaracje powiązanych operacji Win32 API (ten facet również to wyjaśnił w swojej odpowiedzi)
Mladen Janković
4
Słowo volatilekluczowe ma zapobiegać stosowaniu przez kompilator optymalizacji na obiektach, które mogą się zmieniać w sposób niemożliwy do określenia przez kompilator.
Obiekty zadeklarowane jako volatilesą pominięte w optymalizacji, ponieważ ich wartości można w dowolnym momencie zmienić za pomocą kodu poza zakresem bieżącego kodu. System zawsze odczytuje bieżącą wartość volatileobiektu z miejsca w pamięci, zamiast utrzymywać jego wartość w rejestrze tymczasowym w punkcie, w którym jest wymagany, nawet jeśli poprzednia instrukcja prosiła o wartość z tego samego obiektu.
Rozważ następujące przypadki
1) Zmienne globalne zmodyfikowane przez procedurę obsługi przerwań poza zakresem.
2) Zmienne globalne w aplikacji wielowątkowej.
Jeśli nie użyjemy zmiennego kwalifikatora, mogą pojawić się następujące problemy
1) Kod może nie działać zgodnie z oczekiwaniami po włączeniu optymalizacji.
2) Kod może nie działać zgodnie z oczekiwaniami, gdy przerwania są włączone i używane.
Link, który opublikowałeś jest bardzo nieaktualny i nie odzwierciedla aktualnych najlepszych praktyk.
Tim Seguine,
2
Oprócz tego, że zmienne słowo kluczowe jest używane do informowania kompilatora, aby nie optymalizował dostępu do niektórych zmiennych (które mogą być modyfikowane przez wątek lub procedurę przerwania), może być również używany do usuwania niektórych błędów kompilatora - TAK, może być ---.
Na przykład pracowałem na platformie osadzonej, w której kompilator dokonywał błędnych przypuszczeń dotyczących wartości zmiennej. Jeśli kod nie został zoptymalizowany, program działałby poprawnie. W przypadku optymalizacji (które były naprawdę potrzebne, ponieważ była to krytyczna procedura) kod nie działałby poprawnie. Jedynym rozwiązaniem (choć niezbyt poprawnym) było zadeklarowanie zmiennej „wadliwej” jako niestabilnej.
Błędne jest założenie, że kompilator nie optymalizuje dostępu do składników lotnych. Standard nic nie wie o optymalizacjach. Kompilator jest zobowiązany do przestrzegania tego, co nakazuje standard, ale można dokonywać wszelkich optymalizacji, które nie zakłócają normalnego zachowania.
Terminus,
3
Z mojego doświadczenia wynika, że 99,9% wszystkich „błędów” optymalizacji w ramieniu gcc to błędy programisty. Nie mam pojęcia, czy dotyczy to tej odpowiedzi. Tylko rant na temat ogólny
odinthenerd
@ Terminus „ To błędne założenie, że kompilator nie optymalizuje dostępu do składników lotnych ” Źródło?
ciekawy,
2
Twój program wydaje się działać nawet bez volatilesłowa kluczowego? Być może jest to powód:
Jak wspomniano wcześniej, volatilesłowo kluczowe pomaga w przypadkach takich jak
volatileint* p =...;// point to some memorywhile(*p!=0){}// loop until the memory becomes zero
Ale wydaje się, że prawie nie ma żadnego efektu po wywołaniu funkcji zewnętrznej lub nieliniowej. Na przykład:
while(*p!=0){ g();}
Następnie volatilegenerowany jest prawie taki sam wynik z lub bez .
Tak długo, jak g () można całkowicie wstawić, kompilator może zobaczyć wszystko, co się dzieje, i dlatego może zoptymalizować. Ale kiedy program wywołuje miejsce, w którym kompilator nie widzi, co się dzieje, kompilator nie może już przyjmować żadnych założeń. Dlatego kompilator wygeneruje kod, który zawsze odczytuje bezpośrednio z pamięci.
Ale uważaj na dzień, w którym funkcja g () stanie się wbudowana (albo z powodu wyraźnych zmian, albo z powodu sprytności kompilatora / linkera), więc kod może się zepsuć, jeśli zapomnisz volatilesłowa kluczowego!
Dlatego zalecam dodanie volatilesłowa kluczowego, nawet jeśli Twój program wydaje się działać bez niego. Sprawia, że intencja jest jaśniejsza i bardziej solidna w odniesieniu do przyszłych zmian.
Zauważ, że funkcja może mieć wstawiony kod podczas generowania odwołania (rozwiązanego w czasie łączenia) do funkcji konspektu; będzie tak w przypadku częściowo wbudowanej funkcji rekurencyjnej. Funkcja może również mieć semantyczne „wstawione” przez kompilator, to znaczy, że kompilator zakłada, że skutki uboczne i wynik mieszczą się w możliwych skutkach ubocznych i możliwych wynikach zgodnie z jego kodem źródłowym, ale nadal go nie inliniują. Opiera się to na „skutecznej regule jednej definicji”, która stanowi, że wszystkie definicje jednostki muszą być faktycznie równoważne (jeśli nie dokładnie identyczne).
ciekawy
Unikanie przenośnego wstawiania wywołania (lub „wstawiania” jego semantycznego) przez funkcję, której ciało jest widoczne przez kompilator (nawet w czasie łączenia z globalną optymalizacją) jest możliwe przy użyciu volatilewskaźnika kwalifikowanej funkcji:void (* volatile fun_ptr)() = fun; fun_ptr();
ciekawy
2
We wczesnych dniach C kompilatory interpretują wszystkie działania, które odczytują i zapisują wartości lv jako operacje pamięciowe, wykonywane w tej samej kolejności, w jakiej odczyty i zapisy pojawiły się w kodzie. Wydajność można by znacznie poprawić w wielu przypadkach, gdyby kompilatory miały pewną swobodę zmiany kolejności i konsolidacji operacji, ale był z tym problem. Nawet operacje były często określane w określonej kolejności tylko dlatego, że konieczne było ich podanie w niektórych kolejności, dlatego programista wybrał jedną z wielu równie dobrych alternatyw, co nie zawsze tak było. Czasami ważne jest, aby pewne operacje zachodziły w określonej kolejności.
To, które szczegóły sekwencjonowania są ważne, będzie się różnić w zależności od platformy docelowej i pola zastosowania. Zamiast zapewniać szczególnie szczegółową kontrolę, Standard zdecydował się na prosty model: jeśli sekwencja dostępu jest wykonywana z wartościami niekwalifikowanymi volatile, kompilator może zmienić ich kolejność i konsolidować według własnego uznania. Jeśli akcja jest wykonywana przy użyciu volatilekwalifikowanej wartości, implementacja wysokiej jakości powinna oferować wszelkie dodatkowe gwarancje porządkowania, które mogą być wymagane przez kod ukierunkowany na docelową platformę i pole aplikacji, bez konieczności stosowania niestandardowej składni.
Niestety, zamiast określać, jakich gwarancji będą potrzebować programiści, wielu kompilatorów zdecydowało się zaoferować gwarancje minimalne wymagane przez standard. To sprawia, że volatilejest mniej użyteczny niż powinien. Na przykład na gcc lub clang programista, który musi wdrożyć podstawowy „mutex hand-off” [taki, w którym zadanie, które nabyło i wydało mutex, nie zrobi tego ponownie, dopóki inne zadanie tego nie zrobi] musi to zrobić z czterech rzeczy:
Umieść akwizycję i wydanie muteksu w funkcji, której kompilator nie może wstawić i do której nie może zastosować optymalizacji całego programu.
Zakwalifikuj wszystkie obiekty strzeżone przez volatilemuteks jako - coś, co nie powinno być konieczne, jeśli wszystkie dostępy wystąpią po uzyskaniu muteksu i przed jego zwolnieniem.
Użyj poziomu optymalizacji 0, aby wymusić na kompilatorze generowanie kodu tak, jakby wszystkie obiekty, które nie registersą kwalifikowane, były volatile.
Użyj dyrektyw specyficznych dla gcc.
Natomiast w przypadku korzystania z kompilatora wyższej jakości, który jest bardziej odpowiedni do programowania systemów, takich jak icc, istnieje inna opcja:
Upewnij się, że volatilekwalifikowane zapisy będą wykonywane wszędzie tam, gdzie potrzebne jest nabycie lub wydanie.
Zdobycie podstawowego „mutexu hand-off” wymaga volatileodczytu (aby zobaczyć, czy jest gotowy) i nie powinno również wymagać volatilezapisu (druga strona nie będzie próbowała go odzyskać, dopóki nie zostanie oddana), ale musi wykonanie bezsensownego volatilezapisu jest wciąż lepsze niż dowolna z opcji dostępnych w gcc lub clang.
Jednym z zastosowań, które powinienem przypomnieć, jest to, że w funkcji obsługi sygnału, jeśli chcesz uzyskać dostęp do / zmodyfikować zmienną globalną (na przykład oznaczyć ją jako exit = true), musisz zadeklarować tę zmienną jako „niestabilną”.
Wszystkie odpowiedzi są doskonałe. Ale przede wszystkim chciałbym podzielić się przykładem.
Poniżej znajduje się mały program cpp:
#include<iostream>int x;int main(){char buf[50];
x =8;if(x ==8)
printf("x is 8\n");else
sprintf(buf,"x is not 8\n");
x=1000;while(x >5)
x--;return0;}
Teraz wygenerujmy zestaw powyższego kodu (i wkleję tylko te części zestawu, które są tutaj istotne):
Polecenie generowania zestawu:
g++-S -O3 -c -fverbose-asm-Wa,-adhln assembly.cpp
I montaż:
main:.LFB1594:
subq $40,%rsp #,.seh_stackalloc 40.seh_endprologue
# assembly.cpp:5: int main(){
call __main ## assembly.cpp:10: printf("x is 8\n");
leaq .LC0(%rip),%rcx #,# assembly.cpp:7: x = 8;
movl $8, x(%rip)#, x# assembly.cpp:10: printf("x is 8\n");
call _ZL6printfPKcz.constprop.0## assembly.cpp:18: }
xorl %eax,%eax #
movl $5, x(%rip)#, x
addq $40,%rsp #,
ret
.seh_endproc
.p2align 4,,15.def _GLOBAL__sub_I_x;.scl 3;.type 32;.endef
.seh_proc _GLOBAL__sub_I_x
W zestawie można zobaczyć, że kod zestawu nie został wygenerowany, sprintfponieważ kompilator założył, że xnie zmieni się on poza programem. To samo dotyczy whilepętli. whilePętla została całkowicie usunięta z powodu optymalizacji, ponieważ kompilator widział to jako bezużyteczny kod, a zatem bezpośrednio przypisany 5do x(patrz movl $5, x(%rip)).
Problem występuje, gdy co jeśli zewnętrzny proces / sprzęt zmieniłby wartość xgdzieś pomiędzy x = 8;a if(x == 8). Spodziewalibyśmy się, że elseblok zadziała, ale niestety kompilator usunął tę część.
Teraz, aby rozwiązać ten problem assembly.cpp, przejdźmy int x;do volatile int x;i wygenerujmy kod asemblera wygenerowany:
Tutaj widać, że kody do montażu sprintf, printforaz whilepętla zostały wygenerowane. Zaletą jest to, że jeśli xzmienna zostanie zmieniona przez jakiś zewnętrzny program lub sprzęt, sprintfczęść kodu zostanie wykonana. Podobnie, whilepętla może być teraz używana do zajętego oczekiwania.
Inne odpowiedzi już wspominają o unikaniu optymalizacji w celu:
użyj rejestrów zmapowanych w pamięci (lub „MMIO”)
napisz sterowniki urządzeń
umożliwiają łatwiejsze debugowanie programów
uczynić obliczenia zmiennoprzecinkowe bardziej deterministycznymi
Zmienna jest niezbędna, gdy potrzebujesz wartości, która wydaje się pochodzić z zewnątrz i być nieprzewidywalna, i unikać optymalizacji kompilatora w oparciu o znaną wartość, a gdy wynik nie jest faktycznie używany, ale trzeba go obliczyć lub jest on używany, ale chcesz go obliczyć kilka razy dla testu porównawczego i potrzebujesz, aby obliczenia rozpoczynały się i kończyły w ściśle określonych punktach.
Odczyt zmienny jest jak operacja wprowadzania (podobna scanflub zastosowanie cin): wydaje się, że wartość pochodzi z zewnątrz programu, więc każde obliczenie zależne od wartości musi rozpocząć się po niej .
Zapis niestabilny jest jak operacja wyjściowa (podobna printflub zastosowanie cout): wartość wydaje się być przekazywana poza programem, więc jeśli wartość zależy od obliczeń, należy ją wcześniej zakończyć .
Tak więc parę niestabilnych odczytów / zapisów można wykorzystać do oswojenia testów i nadania sensowi pomiaru czasu .
Bez niestabilności obliczenia mogłyby być wcześniej uruchomione przez kompilator, ponieważ nic nie stoi na przeszkodzie zmianie kolejności obliczeń za pomocą funkcji takich jak pomiar czasu .
volatile
można go efektywnie wykorzystać, zestawiony w dość laicki sposób. Link: publications.gbdirect.co.uk/c_book/chapter8/…volatile
bardziej przydatne niżfriend
słowo kluczowe.Odpowiedzi:
volatile
jest potrzebny, jeśli czytasz z miejsca w pamięci, które, powiedzmy, całkowicie oddzielny proces / urządzenie / cokolwiek, do czego może pisać.Kiedyś pracowałem z podwójnym portem RAM w systemie wieloprocesorowym w prostej C. Użyliśmy sprzętowo zarządzanej 16-bitowej wartości jako semafora, aby wiedzieć, kiedy skończył się drugi facet. Zasadniczo zrobiliśmy to:
Bez
volatile
tego optymalizator uważa pętlę za bezużyteczną (facet nigdy nie ustawia wartości! On jest szalony, pozbywa się tego kodu!), A mój kod działałby bez uzyskania semafora, powodując problemy później.źródło
uint16_t* volatile semPtr
zamiast tego napisano? Powinno to oznaczać wskaźnik jako niestabilny (zamiast wskazywanej wartości), tak że kontrole samego wskaźnika, np.semPtr == SOME_ADDR
Mogą nie zostać zoptymalizowane. Implikuje to jednak również ponownie zmienną wartość punktową. Nie?volatile
jest potrzebny podczas opracowywania systemów wbudowanych lub sterowników urządzeń, w których należy odczytać lub zapisać urządzenie sprzętowe zamapowane w pamięci. Zawartość konkretnego rejestru urządzenia może ulec zmianie w dowolnym momencie, dlatego potrzebujeszvolatile
słowa kluczowego, aby kompilator nie zoptymalizował takich dostępów.źródło
Niektóre procesory mają rejestry zmiennoprzecinkowe, które mają więcej niż 64 bity dokładności (np. 32-bitowy x86 bez SSE, patrz komentarz Petera). W ten sposób, jeśli uruchomisz kilka operacji na liczbach podwójnej precyzji, faktycznie uzyskasz odpowiedź o wyższej precyzji, niż gdybyś obciął każdy wynik pośredni do 64 bitów.
Jest to zwykle świetne, ale oznacza to, że w zależności od tego, w jaki sposób kompilator przypisał rejestry i dokonał optymalizacji, uzyskasz różne wyniki dla dokładnie tych samych operacji na dokładnie tych samych danych wejściowych. Jeśli potrzebujesz spójności, możesz zmusić każdą operację do powrotu do pamięci za pomocą niestabilnego słowa kluczowego.
Jest to również przydatne w przypadku niektórych algorytmów, które nie mają sensu algebraicznego, ale zmniejszają błąd zmiennoprzecinkowy, na przykład sumowanie Kahana. Algebraicznie jest to nop, więc często będzie niepoprawnie zoptymalizowane, chyba że niektóre zmienne pośrednie są zmienne.
źródło
volatile double
zamiast po prostudouble
, aby upewnić się, że jest ona skracana z precyzji FPU do 64-bitowej (RAM) przed kontynuowaniem dalszych obliczeń. Wyniki były zasadniczo różne ze względu na dalszą przesadę błędu zmiennoprzecinkowego.g++ -mfpmath=sse
użycie go również dla 32-bitowego x86. Można używaćgcc -ffloat-store
do wymuszenia zaokrąglania wszędzie , nawet podczas korzystania z x87, czy można ustawić precyzję x87 dla 53-bitowej mantysy: randomascii.wordpress.com/2012/03/21/... .volatile
wymuszania zaokrąglania w kilku konkretnych miejscach bez utraty korzyści wszędzie.Z artykułu „Zmienna jak obietnica” Dana Saksa:
Oto linki do trzech jego artykułów dotyczących
volatile
słowa kluczowego:źródło
MUSISZ używać niestabilnych podczas implementowania struktur danych bez blokady. W przeciwnym razie kompilator może optymalizować dostęp do zmiennej, która zmieni semantykę.
Innymi słowy, volatile mówi kompilatorowi, który uzyskuje dostęp do tej zmiennej, musi odpowiadać operacji odczytu / zapisu w pamięci fizycznej.
Na przykład tak deklaruje się InterlockedIncrement w API Win32:
źródło
std::atomic<LONG>
bezpieczniejsze pisanie kodu bez blokady, bez problemów z optymalizacją czystych ładowań / czystych sklepów, zmianą kolejności lub cokolwiek innego.Duża aplikacja, nad którą pracowałem na początku lat 90., zawierała obsługę wyjątków opartych na języku C przy użyciu setjmp i longjmp. Zmienne słowo kluczowe było konieczne w przypadku zmiennych, których wartości musiały zostać zachowane w bloku kodu, który służył jako klauzula „catch”, aby te zmienne nie zostały zapisane w rejestrach i usunięte przez longjmp.
źródło
W standardzie C jednym z miejsc do użycia
volatile
jest moduł obsługi sygnału. W rzeczywistości w standardzie C wszystko, co można bezpiecznie zrobić w module obsługi sygnału, to zmodyfikowaćvolatile sig_atomic_t
zmienną lub szybko wyjść. Rzeczywiście, AFAIK, jest to jedyne miejsce w standardzie C, którego użycievolatile
jest wymagane, aby uniknąć nieokreślonego zachowania.Oznacza to, że w standardzie C możesz pisać:
i niewiele więcej.
POSIX jest znacznie łagodniejszy w kwestii tego, co można zrobić w procedurze obsługi sygnałów, ale wciąż istnieją ograniczenia (jednym z ograniczeń jest to, że
printf()
nie można bezpiecznie używać standardowej biblioteki we / wy - i innych).źródło
Opracowując dla osadzonego, mam pętlę, która sprawdza zmienną, którą można zmienić w module obsługi przerwań. Bez „niestabilnej” pętla staje się noop - o ile kompilator może powiedzieć, zmienna nigdy się nie zmienia, więc optymalizuje sprawdzenie.
To samo dotyczyłoby zmiennej, która może zostać zmieniona w innym wątku w bardziej tradycyjnym środowisku, ale tam często wykonujemy wywołania synchronizacji, więc kompilator nie jest tak wolny od optymalizacji.
źródło
Użyłem go w kompilacjach debugowania, gdy kompilator nalega na zoptymalizowanie zmiennej, którą chcę widzieć, gdy przechodzę przez kod.
źródło
Oprócz używania go zgodnie z przeznaczeniem, lotny jest wykorzystywany w metaprogramowaniu (szablonowym). Można go użyć, aby zapobiec przypadkowemu przeładowaniu, ponieważ zmienny atrybut (jak const) bierze udział w rozwiązywaniu problemu przeciążenia.
To jest legalne; oba przeciążenia są potencjalnie wywoływalne i robią prawie to samo. Obsada w
volatile
przeciążeniu jest legalna, ponieważ wiemy, że pasek i tak nie przejdzie nieulotnegoT
.volatile
Wersja jest surowo gorzej, choć, więc nigdy wybrany w rozdzielczości przeciążenia jeśli nieulotnaf
jest dostępna.Pamiętaj, że kod nigdy nie zależy od
volatile
dostępu do pamięci.źródło
źródło
Słowo
volatile
kluczowe ma zapobiegać stosowaniu przez kompilator optymalizacji na obiektach, które mogą się zmieniać w sposób niemożliwy do określenia przez kompilator.Obiekty zadeklarowane jako
volatile
są pominięte w optymalizacji, ponieważ ich wartości można w dowolnym momencie zmienić za pomocą kodu poza zakresem bieżącego kodu. System zawsze odczytuje bieżącą wartośćvolatile
obiektu z miejsca w pamięci, zamiast utrzymywać jego wartość w rejestrze tymczasowym w punkcie, w którym jest wymagany, nawet jeśli poprzednia instrukcja prosiła o wartość z tego samego obiektu.Rozważ następujące przypadki
1) Zmienne globalne zmodyfikowane przez procedurę obsługi przerwań poza zakresem.
2) Zmienne globalne w aplikacji wielowątkowej.
Jeśli nie użyjemy zmiennego kwalifikatora, mogą pojawić się następujące problemy
1) Kod może nie działać zgodnie z oczekiwaniami po włączeniu optymalizacji.
2) Kod może nie działać zgodnie z oczekiwaniami, gdy przerwania są włączone i używane.
Lotny: najlepszy przyjaciel programisty
https://en.wikipedia.org/wiki/Volatile_(computer_programming)
źródło
Oprócz tego, że zmienne słowo kluczowe jest używane do informowania kompilatora, aby nie optymalizował dostępu do niektórych zmiennych (które mogą być modyfikowane przez wątek lub procedurę przerwania), może być również używany do usuwania niektórych błędów kompilatora - TAK, może być ---.
Na przykład pracowałem na platformie osadzonej, w której kompilator dokonywał błędnych przypuszczeń dotyczących wartości zmiennej. Jeśli kod nie został zoptymalizowany, program działałby poprawnie. W przypadku optymalizacji (które były naprawdę potrzebne, ponieważ była to krytyczna procedura) kod nie działałby poprawnie. Jedynym rozwiązaniem (choć niezbyt poprawnym) było zadeklarowanie zmiennej „wadliwej” jako niestabilnej.
źródło
Twój program wydaje się działać nawet bez
volatile
słowa kluczowego? Być może jest to powód:Jak wspomniano wcześniej,
volatile
słowo kluczowe pomaga w przypadkach takich jakAle wydaje się, że prawie nie ma żadnego efektu po wywołaniu funkcji zewnętrznej lub nieliniowej. Na przykład:
Następnie
volatile
generowany jest prawie taki sam wynik z lub bez .Tak długo, jak g () można całkowicie wstawić, kompilator może zobaczyć wszystko, co się dzieje, i dlatego może zoptymalizować. Ale kiedy program wywołuje miejsce, w którym kompilator nie widzi, co się dzieje, kompilator nie może już przyjmować żadnych założeń. Dlatego kompilator wygeneruje kod, który zawsze odczytuje bezpośrednio z pamięci.
Ale uważaj na dzień, w którym funkcja g () stanie się wbudowana (albo z powodu wyraźnych zmian, albo z powodu sprytności kompilatora / linkera), więc kod może się zepsuć, jeśli zapomnisz
volatile
słowa kluczowego!Dlatego zalecam dodanie
volatile
słowa kluczowego, nawet jeśli Twój program wydaje się działać bez niego. Sprawia, że intencja jest jaśniejsza i bardziej solidna w odniesieniu do przyszłych zmian.źródło
volatile
wskaźnika kwalifikowanej funkcji:void (* volatile fun_ptr)() = fun; fun_ptr();
We wczesnych dniach C kompilatory interpretują wszystkie działania, które odczytują i zapisują wartości lv jako operacje pamięciowe, wykonywane w tej samej kolejności, w jakiej odczyty i zapisy pojawiły się w kodzie. Wydajność można by znacznie poprawić w wielu przypadkach, gdyby kompilatory miały pewną swobodę zmiany kolejności i konsolidacji operacji, ale był z tym problem. Nawet operacje były często określane w określonej kolejności tylko dlatego, że konieczne było ich podanie w niektórych kolejności, dlatego programista wybrał jedną z wielu równie dobrych alternatyw, co nie zawsze tak było. Czasami ważne jest, aby pewne operacje zachodziły w określonej kolejności.
To, które szczegóły sekwencjonowania są ważne, będzie się różnić w zależności od platformy docelowej i pola zastosowania. Zamiast zapewniać szczególnie szczegółową kontrolę, Standard zdecydował się na prosty model: jeśli sekwencja dostępu jest wykonywana z wartościami niekwalifikowanymi
volatile
, kompilator może zmienić ich kolejność i konsolidować według własnego uznania. Jeśli akcja jest wykonywana przy użyciuvolatile
kwalifikowanej wartości, implementacja wysokiej jakości powinna oferować wszelkie dodatkowe gwarancje porządkowania, które mogą być wymagane przez kod ukierunkowany na docelową platformę i pole aplikacji, bez konieczności stosowania niestandardowej składni.Niestety, zamiast określać, jakich gwarancji będą potrzebować programiści, wielu kompilatorów zdecydowało się zaoferować gwarancje minimalne wymagane przez standard. To sprawia, że
volatile
jest mniej użyteczny niż powinien. Na przykład na gcc lub clang programista, który musi wdrożyć podstawowy „mutex hand-off” [taki, w którym zadanie, które nabyło i wydało mutex, nie zrobi tego ponownie, dopóki inne zadanie tego nie zrobi] musi to zrobić z czterech rzeczy:Umieść akwizycję i wydanie muteksu w funkcji, której kompilator nie może wstawić i do której nie może zastosować optymalizacji całego programu.
Zakwalifikuj wszystkie obiekty strzeżone przez
volatile
muteks jako - coś, co nie powinno być konieczne, jeśli wszystkie dostępy wystąpią po uzyskaniu muteksu i przed jego zwolnieniem.Użyj poziomu optymalizacji 0, aby wymusić na kompilatorze generowanie kodu tak, jakby wszystkie obiekty, które nie
register
są kwalifikowane, byłyvolatile
.Użyj dyrektyw specyficznych dla gcc.
Natomiast w przypadku korzystania z kompilatora wyższej jakości, który jest bardziej odpowiedni do programowania systemów, takich jak icc, istnieje inna opcja:
volatile
kwalifikowane zapisy będą wykonywane wszędzie tam, gdzie potrzebne jest nabycie lub wydanie.Zdobycie podstawowego „mutexu hand-off” wymaga
volatile
odczytu (aby zobaczyć, czy jest gotowy) i nie powinno również wymagaćvolatile
zapisu (druga strona nie będzie próbowała go odzyskać, dopóki nie zostanie oddana), ale musi wykonanie bezsensownegovolatile
zapisu jest wciąż lepsze niż dowolna z opcji dostępnych w gcc lub clang.źródło
Jednym z zastosowań, które powinienem przypomnieć, jest to, że w funkcji obsługi sygnału, jeśli chcesz uzyskać dostęp do / zmodyfikować zmienną globalną (na przykład oznaczyć ją jako exit = true), musisz zadeklarować tę zmienną jako „niestabilną”.
źródło
Wszystkie odpowiedzi są doskonałe. Ale przede wszystkim chciałbym podzielić się przykładem.
Poniżej znajduje się mały program cpp:
Teraz wygenerujmy zestaw powyższego kodu (i wkleję tylko te części zestawu, które są tutaj istotne):
Polecenie generowania zestawu:
I montaż:
W zestawie można zobaczyć, że kod zestawu nie został wygenerowany,
sprintf
ponieważ kompilator założył, żex
nie zmieni się on poza programem. To samo dotyczywhile
pętli.while
Pętla została całkowicie usunięta z powodu optymalizacji, ponieważ kompilator widział to jako bezużyteczny kod, a zatem bezpośrednio przypisany5
dox
(patrzmovl $5, x(%rip)
).Problem występuje, gdy co jeśli zewnętrzny proces / sprzęt zmieniłby wartość
x
gdzieś pomiędzyx = 8;
aif(x == 8)
. Spodziewalibyśmy się, żeelse
blok zadziała, ale niestety kompilator usunął tę część.Teraz, aby rozwiązać ten problem
assembly.cpp
, przejdźmyint x;
dovolatile int x;
i wygenerujmy kod asemblera wygenerowany:Tutaj widać, że kody do montażu
sprintf
,printf
orazwhile
pętla zostały wygenerowane. Zaletą jest to, że jeślix
zmienna zostanie zmieniona przez jakiś zewnętrzny program lub sprzęt,sprintf
część kodu zostanie wykonana. Podobnie,while
pętla może być teraz używana do zajętego oczekiwania.źródło
Inne odpowiedzi już wspominają o unikaniu optymalizacji w celu:
Zmienna jest niezbędna, gdy potrzebujesz wartości, która wydaje się pochodzić z zewnątrz i być nieprzewidywalna, i unikać optymalizacji kompilatora w oparciu o znaną wartość, a gdy wynik nie jest faktycznie używany, ale trzeba go obliczyć lub jest on używany, ale chcesz go obliczyć kilka razy dla testu porównawczego i potrzebujesz, aby obliczenia rozpoczynały się i kończyły w ściśle określonych punktach.
Odczyt zmienny jest jak operacja wprowadzania (podobna
scanf
lub zastosowaniecin
): wydaje się, że wartość pochodzi z zewnątrz programu, więc każde obliczenie zależne od wartości musi rozpocząć się po niej .Zapis niestabilny jest jak operacja wyjściowa (podobna
printf
lub zastosowaniecout
): wartość wydaje się być przekazywana poza programem, więc jeśli wartość zależy od obliczeń, należy ją wcześniej zakończyć .Tak więc parę niestabilnych odczytów / zapisów można wykorzystać do oswojenia testów i nadania sensowi pomiaru czasu .
Bez niestabilności obliczenia mogłyby być wcześniej uruchomione przez kompilator, ponieważ nic nie stoi na przeszkodzie zmianie kolejności obliczeń za pomocą funkcji takich jak pomiar czasu .
źródło