Rozumiem, że volatile
informuje kompilator, że wartość może ulec zmianie, ale czy aby wykonać tę funkcję, kompilator musi wprowadzić ogrodzenie pamięci, aby działało?
Z mojego zrozumienia, sekwencji operacji na obiektach ulotnych nie można zmienić i należy ją zachować. Wydaje się to sugerować, że niektóre ogrodzenia pamięci są konieczne i nie ma sposobu na obejście tego. Czy mam rację, mówiąc to?
Na to pokrewne pytanie toczy się interesująca dyskusja
... Kompilator nie może zmienić kolejności dostępu do różnych zmiennych lotnych, o ile występują w oddzielnych pełnych wyrażeniach ... prawda, że zmienne są bezużyteczne ze względu na bezpieczeństwo wątków, ale nie z powodów, które podaje. Nie dzieje się tak dlatego, że kompilator może zmienić kolejność dostępu do obiektów ulotnych, ale dlatego, że procesor może zmienić ich kolejność. Operacje atomowe i bariery pamięci uniemożliwiają kompilatorowi i procesorowi zmianę kolejności
Na co David Schwartz odpowiada w komentarzach :
... Z punktu widzenia standardu C ++ nie ma różnicy między kompilatorem, który coś robi, a kompilatorem emitującym instrukcje, które powodują, że sprzęt coś robi. Jeśli procesor może zmienić kolejność dostępu do składników lotnych, to standard nie wymaga zachowania ich kolejności. ...
... Standard C ++ nie ma żadnego rozróżnienia na temat tego, co powoduje zmianę kolejności. I nie można argumentować, że procesor może zmienić ich kolejność bez zauważalnego efektu, więc to jest w porządku - standard C ++ definiuje ich kolejność jako obserwowalną. Kompilator jest zgodny ze standardem C ++ na platformie, jeśli generuje kod, który sprawia, że platforma robi to, czego wymaga standard. Jeśli standard wymaga, aby dostęp do składników lotnych nie był zmieniany, platforma, na której są one ponownie zamawiane, nie jest zgodna. ...
Chodzi mi o to, że jeśli standard C ++ zabrania kompilatorowi zmiany kolejności dostępów do różnych składników lotnych, na podstawie teorii, że kolejność takich dostępów jest częścią obserwowalnego zachowania programu, to wymaga również, aby kompilator emitował kod, który zabrania procesorowi więc. Standard nie rozróżnia między tym, co robi kompilator, a tym, co generuje kod kompilatora, powoduje, że robi to procesor.
Co rodzi dwa pytania: czy którekolwiek z nich jest „słuszne”? Co tak naprawdę robią rzeczywiste wdrożenia?
źródło
volatile
optymalizacji odczytów zmiennych przez pamięci podręczne procesora. Albo wszystkie te kompilatory są niezgodne, albo standard nie oznacza tego, co myślisz, że oznacza. (Standard nie rozróżnia między tym, co robi kompilator, a tym, co kompilator wymusza na procesorze. Zadaniem kompilatora jest emitowanie kodu, który po uruchomieniu jest zgodny ze standardem.)Odpowiedzi:
Zamiast wyjaśniać, co to
volatile
robi, pozwól mi wyjaśnić, kiedy powinieneś użyćvolatile
.volatile
zmiennej jest właściwie jedyną rzeczą, na którą pozwala standard, z poziomu programu obsługi sygnału. Od C ++ 11 można go używaćstd::atomic
do tego celu, ale tylko wtedy, gdy atomic nie zawiera blokad.setjmp
Intel .Na przykład:
volatile int *foo = some_memory_mapped_device; while (*foo) ; // wait until *foo turns false
Bez
volatile
specyfikatora kompilator może całkowicie zoptymalizować pętlę. Specyfikatorvolatile
mówi kompilatorowi, że może nie zakładać, że 2 kolejne odczyty zwracają tę samą wartość.Zauważ, że
volatile
nie ma to nic wspólnego z wątkami. Powyższy przykład nie działa, jeśli zapisano do innego wątku,*foo
ponieważ nie ma operacji pobierania.We wszystkich innych przypadkach użycie programu
volatile
powinno być uważane za nieprzenośne i nie należy już przechodzić przeglądu kodu, z wyjątkiem sytuacji, gdy mamy do czynienia z kompilatorami sprzed wersji C ++ 11 i rozszerzeniami kompilatora (takimi jak/volatile:ms
przełącznik msvc , który jest domyślnie włączony w X86 / I64).źródło
setjmp
są dwiema gwarancjami, które są standardowymi markami. Z drugiej strony zamiarem , przynajmniej na początku, było wspieranie operacji we / wy mapowanych w pamięci. Które na niektórych procesorach mogą wymagać ogrodzenia lub membara.volatile
dostępu.Kompilator C ++ zgodny ze specyfikacją nie musi wprowadzać ogrodzenia pamięci. Twój konkretny kompilator może; skieruj swoje pytanie do autorów twojego kompilatora.
Funkcja „volatile” w C ++ nie ma nic wspólnego z wątkami. Pamiętaj, że celem „volatile” jest wyłączenie optymalizacji kompilatora, aby odczyt z rejestru, który zmienia się z powodu warunków egzogenicznych, nie został zoptymalizowany. Czy adres pamięci, który jest zapisywany przez inny wątek na innym procesorze, jest rejestrem, który zmienia się z powodu warunków egzogenicznych? Nie. Ponownie, jeśli niektórzy autorzy kompilatorów zdecydowali się traktować adresy pamięci zapisywane przez różne wątki na różnych procesorach tak, jakby były to rejestry zmieniające się z powodu warunków egzogenicznych, to ich sprawa; nie są do tego zobowiązani. Nie są też wymagane - nawet jeśli wprowadza barierę pamięci - na przykład, aby upewnić się, że każdy wątek jest spójny porządkowanie nietrwałych odczytów i zapisów.
W rzeczywistości volatile jest praktycznie bezużyteczne do tworzenia wątków w C / C ++. Najlepszą praktyką jest unikanie tego.
Ponadto: ogrodzenia pamięci są szczegółem implementacji poszczególnych architektur procesorów. W C #, gdzie volatile jest jawnie przeznaczone do wielowątkowości, specyfikacja nie mówi, że zostaną wprowadzone półogrodzenia, ponieważ program może działać na architekturze, która w ogóle nie ma ogrodzeń. Wręcz przeciwnie, specyfikacja zapewnia pewne (wyjątkowo słabe) gwarancje dotyczące tego, jakie optymalizacje zostaną pominięte przez kompilator, środowisko wykonawcze i procesor, aby nałożyć pewne (bardzo słabe) ograniczenia na sposób uporządkowania niektórych efektów ubocznych. W praktyce te optymalizacje są eliminowane przez zastosowanie półotworów, ale jest to szczegół implementacji, który może ulec zmianie w przyszłości.
Fakt, że zależy ci na semantyce zmiennych w dowolnym języku, ponieważ odnoszą się one do wielowątkowości, wskazuje, że myślisz o udostępnianiu pamięci między wątkami. Rozważ po prostu, żeby tego nie robić. To sprawia, że twój program jest dużo trudniejszy do zrozumienia i dużo bardziej prawdopodobne, że zawiera subtelne, niemożliwe do odtworzenia błędy.
źródło
volatile
zostało wprowadzone do standardu C. Mimo to, ponieważ standard nie może określić rzeczy, takich jak to, co faktycznie dzieje się przy „dostępie”, mówi, że „To, co stanowi dostęp do obiektu, który ma zmienny typ kwalifikowany, jest zdefiniowane przez implementację”. Zbyt wiele dzisiejszych implementacji nie dostarcza użytecznej definicji dostępu, co IMHO narusza ducha standardu, nawet jeśli jest zgodny z literą.volatile
semantyka jest silniejsza niż to, kompilator musi wygenerować każdy żądany dostęp (1,9 / 8, 1,9 / 12), a nie tylko zagwarantować, że egzogeniczne zmiany zostaną ostatecznie wykryte (1.10 / 27). W świecie operacji we / wy mapowanych w pamięci, odczyt pamięci może mieć dowolną skojarzoną logikę, na przykład pobierający właściwość. Nie zoptymalizowałbyś wywołań funkcji pobierających właściwości zgodnie z regułami, dla których określiłeśvolatile
, ani standard na to nie pozwala.David przeoczył fakt, że standard C ++ określa zachowanie kilku wątków wchodzących w interakcje tylko w określonych sytuacjach, a wszystko inne skutkuje niezdefiniowanym zachowaniem. Warunek wyścigu obejmujący co najmniej jeden zapis jest niezdefiniowany, jeśli nie używasz zmiennych atomowych.
W konsekwencji kompilator ma pełne prawo zrezygnować z wszelkich instrukcji synchronizacji, ponieważ procesor zauważy tylko różnicę w programie, który wykazuje niezdefiniowane zachowanie z powodu braku synchronizacji.
źródło
volatile
nie ma nic wspólnego z wątkami; jego pierwotnym celem była obsługa operacji we / wy mapowanych w pamięci. Przynajmniej na niektórych procesorach obsługa operacji we / wy mapowanych w pamięci wymagałaby ogrodzeń. (Kompilatory tego nie robią, ale to inny problem.)volatile
ma wiele wspólnego z wątkami:volatile
zajmuje się pamięcią, do której można uzyskać dostęp bez wiedzy kompilatora, że można uzyskać do niej dostęp, i obejmuje wiele rzeczywistych zastosowań współdzielonych danych między wątkami na określonym procesorze.Po pierwsze, standardy C ++ nie gwarantują barier pamięciowych potrzebnych do prawidłowego uporządkowania odczytów / zapisów, które nie są atomowe. zmienne nietrwałe są zalecane do używania z MMIO, obsługą sygnałów itp. W większości implementacji zmienne zmienne nie są przydatne w przypadku wielowątkowości i generalnie nie są zalecane.
Jeśli chodzi o implementację dostępów ulotnych, jest to wybór kompilatora.
W tym artykule , opisując zachowanie gcc, pokazano, że nie można użyć obiektu ulotnego jako bariery pamięci, aby zamówić sekwencję zapisów do pamięci ulotnej.
Jeśli chodzi o zachowanie ICC , znalazłem to źródło, które mówi również, że volatile nie gwarantuje uporządkowania dostępu do pamięci.
Kompilator Microsoft VS2013 ma inne zachowanie. W tej dokumentacji wyjaśniono, w jaki sposób nietrwałość wymusza semantykę Release / Acquire i umożliwia używanie obiektów ulotnych w blokadach / wydaniach aplikacji wielowątkowych.
Innym aspektem, który należy wziąć pod uwagę, jest to, że ten sam kompilator może mieć inne zachowanie wrt. niestabilny w zależności od docelowej architektury sprzętowej . Ten post dotyczący kompilatora MSVS 2013 jasno określa specyfikę kompilacji z wersją volatile dla platform ARM.
Więc moja odpowiedź na:
byłoby: Brak gwarancji, prawdopodobnie nie, ale niektóre kompilatory mogą to zrobić. Nie powinieneś polegać na tym, że tak.
źródło
volatile
uniemożliwiają kompilatorowi zmianę kolejności ładowań / sklepów? A może mówisz, że standard C ++ wymaga tego? A jeśli to drugie, czy możesz odpowiedzieć na mój argument przeciwny, przytoczony w pierwotnym pytaniu?volatile
lwartość. Ponieważ jednak pozostawia definicję „dostępu” w gestii implementacji, nie kupuje to nam wiele, jeśli implementacja nie obchodzi.volatile
, ale w kodzie wygenerowanym przez kompilator w programie Visual Studios 2012 nie mavolatile
jest to, które jest wyszczególnione w standardzie. (setjmp
, sygnały i tak dalej.)Kompilator wstawia jedynie ogrodzenie pamięci w architekturze Itanium, o ile wiem.
Słowo
volatile
kluczowe jest naprawdę najlepiej używane do zmian asynchronicznych, np. Programów obsługi sygnałów i rejestrów mapowanych w pamięci; jest to zwykle niewłaściwe narzędzie do programowania wielowątkowego.źródło
volatile
jest niezwykle przydatny w wielu zastosowaniach, które nigdy nie dotyczą sprzętu. Jeśli chcesz, aby implementacja generowała kod procesora zgodny z kodem C / C ++, użyjvolatile
.Zależy to od tego, jakim kompilatorem jest „kompilator”. Visual C ++ to robi od 2005 roku. Ale Standard tego nie wymaga, więc niektóre inne kompilatory tego nie wymagają.
źródło
int volatile i; int main() { return i; }
generuje główny z dokładnie dwóch instrukcji:mov eax, i; ret 0;
.cl /help
mówi wersja 18.00.21005.1. Katalog, w którym się znajdujeC:\Program Files (x86)\Microsoft Visual Studio 12.0\VC
. Nagłówek w oknie poleceń mówi VS 2013. A więc jeśli chodzi o wersję ... Jedyne opcje, których użyłem, to/c /O2 /Fa
. (Bez/O2
tego ustawia również lokalną ramkę stosu. Ale nadal nie ma instrukcji ogrodzenia.)main
, aby kompilator mógł zobaczyć cały program i wiedział, że nie było innych wątków lub przynajmniej żadnych innych dostępów do zmiennej przed moim (więc nie mogło być problemów z pamięcią podręczną) może mieć na to wpływ również, ale jakoś w to wątpię.Jest to w dużej mierze z pamięci i oparte na wersjach wcześniejszych niż C ++ 11, bez wątków. Ale biorąc udział w dyskusjach na temat wątków w komisji, mogę powiedzieć, że komisja nigdy nie miała zamiaru, który
volatile
można by wykorzystać do synchronizacji między wątkami. Microsoft to zaproponował, ale propozycja nie została zrealizowana.Kluczową specyfikacją
volatile
jest to, że dostęp do zmiennej lotnej reprezentuje „obserwowalne zachowanie”, podobnie jak IO. W ten sam sposób kompilator nie może zmienić kolejności ani usunąć określonych operacji we / wy, nie może zmienić kolejności ani usunąć dostępu do obiektu nietrwałego (lub, dokładniej, dostęp za pośrednictwem wyrażenia lvalue z nietrwałym typem kwalifikowanym). Pierwotnym zamiarem projektu volatile było w rzeczywistości wspieranie operacji we / wy mapowanych w pamięci. „Problem” z tym polega jednak na tym, że to implementacja definiuje, co stanowi „nietrwały dostęp”. I wiele kompilatorów implementuje to tak, jakby definicja brzmiała „instrukcja, która odczytuje lub zapisuje do pamięci, została wykonana”. Co jest legalną, choć bezużyteczną definicją, jeśli implementacja to określa. (Nie znalazłem jeszcze rzeczywistej specyfikacji żadnego kompilatora.Prawdopodobnie (i to jest argument, który akceptuję), jest to sprzeczne z intencją standardu, ponieważ jeśli sprzęt nie rozpozna adresów jako mapowanych w pamięci IO i nie zablokuje jakiejkolwiek zmiany kolejności itp., Nie możesz nawet użyć volatile dla mapowanych w pamięci IO, przynajmniej na architekturach Sparc lub Intel. Niemniej jednak żaden z kompilatorów, na które patrzyłem (Sun CC, g ++ i MSC), nie wyświetla żadnych instrukcji dotyczących ogrodzenia lub membara. (Mniej więcej w czasie, gdy Microsoft zaproponował rozszerzenie reguł dla
volatile
, myślę, że niektórzy z ich kompilatorów zaimplementowali swoją propozycję i wydali instrukcje ogrodzenia dla niestabilnych dostępów. Nie sprawdziłem, co robią najnowsze kompilatory, ale nie zdziwiłbym się, gdyby to zależało na niektórych opcjach kompilatora. Wersja, którą sprawdziłem - myślę, że to VS6.0 - nie emitowała jednak barier).źródło
volatile
semantyka jest całkowicie odpowiednia. Zazwyczaj takie urządzenia peryferyjne zgłaszają swoje obszary pamięci jako niebuforowalne, co pomaga w zmianie kolejności na poziomie sprzętowym.ASI_REAL_IO
części przestrzeni adresowej, myślę, że powinno być dobrze. (Altera NIOS używa podobnej techniki, z wysokimi bitami obejścia MMU kontrolującego adres; jestem pewien, że są też inne)Nie musi. Zmienna nie jest prymitywem synchronizacji. Po prostu wyłącza optymalizacje, tj. Otrzymujesz przewidywalną sekwencję odczytów i zapisów w wątku w tej samej kolejności, jaką określa maszyna abstrakcyjna. Ale czyta i pisze w różnych wątkach przede wszystkim nie ma porządku, nie ma sensu mówić o zachowaniu lub nie zachowaniu ich porządku. Porządek między teadami można ustalić za pomocą prymitywów synchronizacji, bez nich otrzymujesz UB.
Trochę wyjaśnienia odnośnie barier pamięci. Typowy procesor ma kilka poziomów dostępu do pamięci. Jest potok pamięci, kilka poziomów pamięci podręcznej, a następnie pamięć RAM itp.
Instrukcje Membar przepłukują rurociąg. Nie zmieniają kolejności wykonywania odczytów i zapisów, tylko wymusza wykonanie w danym momencie wybitnych. Jest to przydatne w programach wielowątkowych, ale poza tym niewiele.
Pamięci podręczne są zwykle automatycznie spójne między procesorami. Jeśli ktoś chce się upewnić, że pamięć podręczna jest zsynchronizowana z pamięcią RAM, konieczne jest opróżnienie pamięci podręcznej. Bardzo różni się od membara.
źródło
volatile
po prostu wyłącza optymalizację kompilatora? To nie ma żadnego sensu. Każda optymalizacja, którą może wykonać kompilator, może, przynajmniej w zasadzie, równie dobrze zostać wykonana przez procesor. Więc jeśli standard mówi, że po prostu wyłącza optymalizacje kompilatora, oznaczałoby to, że nie zapewnia żadnego zachowania, na którym można by polegać w kodzie przenośnym. Ale to oczywiście nieprawda, ponieważ kod przenośny może polegać na swoim zachowaniu w odniesieniu dosetjmp
sygnałów i.Kompilator musi wprowadzić barierę pamięci wokół
volatile
dostępów wtedy i tylko wtedy, gdy jest to konieczne do wykonania zastosowańvolatile
określonych w standardowej pracy (setjmp
programy obsługi sygnałów itp.) Na tej konkretnej platformie.Zauważ, że niektóre kompilatory wykraczają daleko poza to, co jest wymagane przez standard C ++, aby uczynić
volatile
bardziej wydajnymi lub użytecznymi na tych platformach. Przenośny kod nie powinien polegać navolatile
robieniu niczego poza tym, co określono w standardzie C ++.źródło
Zawsze używam volatile w procedurach obsługi przerwań, np. ISR (często kod asemblera) modyfikuje jakąś lokalizację w pamięci, a kod wyższego poziomu, który działa poza kontekstem przerwania, uzyskuje dostęp do lokalizacji pamięci poprzez wskaźnik do ulotności.
Robię to dla pamięci RAM, a także dla operacji we / wy mapowanych w pamięci.
Na podstawie dyskusji tutaj wydaje się, że jest to nadal ważne użycie volatile, ale nie ma to nic wspólnego z wieloma wątkami lub procesorami. Jeśli kompilator mikrokontrolera „wie”, że nie może być innych dostępów (np. Wszystko jest na chipie, nie ma pamięci podręcznej i jest tylko jeden rdzeń), pomyślałbym, że w ogóle nie zakłada się ograniczenia pamięci, kompilator po prostu musi zapobiec pewnym optymalizacjom.
Kiedy wrzucamy więcej rzeczy do "systemu", który wykonuje kod wynikowy, prawie wszystkie zakłady są wyłączone, przynajmniej tak czytam tę dyskusję. Jak kompilator mógł kiedykolwiek objąć wszystkie bazy?
źródło
Myślę, że zamieszanie wokół zmienności i zmiany kolejności instrukcji wynika z dwóch pojęć dotyczących zmiany kolejności procesorów:
Volatile wpływa na sposób, w jaki kompilator generuje kod, zakładając wykonanie jednowątkowe (w tym przerwania). Nie oznacza to nic o instrukcjach bariery pamięci, ale raczej uniemożliwia kompilatorowi wykonywanie pewnych rodzajów optymalizacji związanych z dostępem do pamięci.
Typowym przykładem jest ponowne pobieranie wartości z pamięci, zamiast używania jednej z pamięci podręcznej w rejestrze.
Wykonanie poza kolejnością
Procesory mogą wykonywać instrukcje poza kolejnością / spekulatywnie, pod warunkiem, że wynik końcowy mógł mieć miejsce w oryginalnym kodzie. Procesory mogą wykonywać transformacje, które są niedozwolone w kompilatorach, ponieważ kompilatory mogą wykonywać tylko takie transformacje, które są poprawne we wszystkich okolicznościach. W przeciwieństwie do tego, procesory mogą sprawdzić poprawność tych optymalizacji i wycofać się z nich, jeśli okażą się niepoprawne.
Sekwencja odczytów / zapisów pamięci widziana przez inne procesory
Końcowy wynik sekwencji instrukcji, efektywna kolejność, musi zgadzać się z semantyką kodu generowanego przez kompilator. Jednak rzeczywista kolejność wykonywania wybrana przez procesor może być inna. Efektywna kolejność widoczna w innych procesorach (każdy procesor może mieć inny widok) może być ograniczona przez bariery pamięci.
Nie jestem pewien, jak bardzo efektywna i rzeczywista kolejność może się różnić, ponieważ nie wiem, w jakim stopniu bariery pamięci mogą uniemożliwić procesorom wykonywanie wykonywania poza kolejnością.
Źródła:
źródło
Podczas gdy pracowałem nad samouczkiem wideo do pobrania w trybie online, dotyczącym tworzenia grafiki 3D i silnika gier, pracowałem z nowoczesnym OpenGL. Użyliśmy
volatile
w ramach jednej z naszych zajęć. Witrynę z samouczkami można znaleźć tutaj, a film wideo działający zevolatile
słowem kluczowym znajduje się wShader Engine
serii wideo 98. Te prace nie są moje własne, ale są akredytowaneMarek A. Krzeminski, MASc
i jest to fragment ze strony pobierania wideo.A jeśli jesteś zapisany do swojej stronie internetowej i mieć dostęp do jego filmu w tym filmie on odwołuje się ten artykuł dotyczący wykorzystania
Volatile
zmultithreading
programowaniem.Oto artykuł z linku powyżej: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766
Ten artykuł może być trochę przestarzały, ale daje dobry wgląd w doskonałe wykorzystanie zmiennego modyfikatora w programowaniu wielowątkowym, aby pomóc zachować asynchroniczność wydarzeń, podczas gdy kompilator sprawdza dla nas warunki wyścigu. Może to nie odpowiadać bezpośrednio na pierwotne pytanie OP dotyczące tworzenia ogrodzenia pamięci, ale zdecydowałem się opublikować to jako odpowiedź dla innych jako doskonałe odniesienie do dobrego wykorzystania ulotności podczas pracy z aplikacjami wielowątkowymi.
źródło
Słowo kluczowe
volatile
zasadniczo oznacza, że odczyt i zapis obiektu powinien być wykonywany dokładnie tak, jak został napisany przez program, i nie powinien być w żaden sposób optymalizowany . Kod binarny powinien następować po kodzie C lub C ++: ładowanie, w którym jest czytany, sklep, w którym jest zapis.Oznacza to również, że żaden odczyt nie powinien dać przewidywalnej wartości: kompilator nie powinien zakładać niczego o odczycie, nawet bezpośrednio po zapisie do tego samego ulotnego obiektu:
volatile int i; i = 1; int j = i; if (j == 1) // not assumed to be true
volatile
może być najważniejszym narzędziem w zestawie narzędzi "C to język asemblera wysokiego poziomu" .To, czy zadeklarowanie obiektu jako ulotnego jest wystarczające do zapewnienia zachowania kodu, który obsługuje zmiany asynchroniczne, zależy od platformy: różne procesory zapewniają różne poziomy gwarantowanej synchronizacji dla normalnych odczytów i zapisów w pamięci. Prawdopodobnie nie powinieneś próbować pisać kodu wielowątkowego niskiego poziomu, chyba że jesteś ekspertem w tej dziedzinie.
Atomowe prymitywy zapewniają ładny, wyższy poziom widoku obiektów do wielowątkowości, co ułatwia wnioskowanie o kodzie. Prawie wszyscy programiści powinni używać albo atomowych prymitywów, albo prymitywów, które zapewniają wzajemne wykluczenia, takie jak muteksy, blokady odczytu i zapisu, semafory lub inne prymitywy blokujące.
źródło