Po patrząc na kiść z innymi pytaniami i ich odpowiedzi , mam wrażenie, że nie ma powszechnej zgody na to, co „lotny” słowo kluczowe w C oznacza dokładnie.
Nawet sam standard nie wydaje się wystarczająco jasny, aby wszyscy mogli się zgodzić co to znaczy .
Wśród innych problemów:
- Wydaje się, że zapewnia różne gwarancje w zależności od sprzętu i kompilatora.
- Wpływa to na optymalizacje kompilatora, ale nie na optymalizacje sprzętowe, więc na zaawansowanym procesorze, który wykonuje własne optymalizacje w czasie wykonywania, nie jest nawet jasne, czy kompilator może zapobiec jakiejkolwiek optymalizacji, której chcesz zapobiec. (Niektóre kompilatory generują instrukcje, aby zapobiec niektórym optymalizacjom sprzętowym w niektórych systemach, ale nie wydaje się to być w żaden sposób ustandaryzowane).
Podsumowując problem, wydaje się (po przeczytaniu dużo), że „niestabilność” gwarantuje coś w rodzaju: Wartość zostanie odczytana / zapisana nie tylko z / do rejestru, ale przynajmniej do pamięci podręcznej L1 rdzenia, w tej samej kolejności, w jakiej odczyty / zapisy pojawiają się w kodzie. Ale wydaje się to bezużyteczne, ponieważ odczyt / zapis z / do rejestru jest już wystarczający w tym samym wątku, podczas gdy koordynacja z pamięcią podręczną L1 nie gwarantuje nic więcej odnośnie koordynacji z innymi wątkami. Nie mogę sobie wyobrazić, kiedy synchronizacja może być ważna tylko z pamięcią podręczną L1.
WYKORZYSTANIE 1
Jedynym powszechnie uzgodnionym zastosowaniem lotnych wydaje się być w przypadku starych lub wbudowanych systemów, w których niektóre lokalizacje pamięci są mapowane sprzętowo na funkcje we / wy, np. W pamięci, która kontroluje (bezpośrednio, w sprzęcie) światło lub trochę pamięci, która informuje, czy klawisz klawiatury jest wciśnięty, czy nie (ponieważ jest podłączony bezpośrednio przez sprzęt do klawisza).
Wydaje się, że „use 1” nie występuje w przenośnym kodzie, którego celem są systemy wielordzeniowe.
USE 2
Nie różni się zbytnio od „use 1” to pamięć, która może być w dowolnym momencie odczytana lub zapisana przez program obsługi przerwań (który może kontrolować światło lub przechowywać informacje z klucza). Ale już w tym przypadku mamy problem polegający na tym, że w zależności od systemu moduł obsługi przerwań może działać na innym rdzeniu z własną pamięcią podręczną pamięci , a „niestabilność” nie gwarantuje spójności pamięci podręcznej we wszystkich systemach.
Więc „Zastosowanie 2” wydaje się być poza to, co „lotny” może dostarczyć.
WYKORZYSTANIE 3
Jedynym innym niekwestionowanym zastosowaniem, jakie widzę, jest zapobieganie błędnej optymalizacji dostępu poprzez różne zmienne wskazujące na tę samą pamięć, której kompilator nie zdaje sobie sprawy z tej samej pamięci. Ale jest to prawdopodobnie tylko niekwestionowane, ponieważ ludzie o tym nie mówią - widziałem tylko jedną wzmiankę o tym. Pomyślałem, że standard C już rozpoznał, że „różne” wskaźniki (jak różne argumenty funkcji) mogą wskazywać na ten sam element lub elementy w pobliżu, i już określiłem, że kompilator musi wygenerować kod, który działa nawet w takich przypadkach. Nie mogłem jednak szybko znaleźć tego tematu w najnowszym standardzie (500 stron!).
Więc „użyj 3” może w ogóle nie istnieje ?
Stąd moje pytanie:
Czy „niestabilność” w ogóle gwarantuje cokolwiek w przenośnym kodzie C dla systemów wielordzeniowych?
EDYCJA - aktualizacja
Po przejrzeniu najnowszego standardu wygląda na to, że odpowiedź jest co najmniej bardzo ograniczona tak:
1. Norma wielokrotnie określa specjalne traktowanie dla określonego typu „volatile sig_atomic_t”. Jednak standard mówi również, że użycie funkcji sygnału w programie wielowątkowym powoduje niezdefiniowane zachowanie. Zatem ten przypadek użycia wydaje się ograniczony do komunikacji między programem jednowątkowym i jego obsługą sygnału.
2. Norma określa również jasne znaczenie słowa „niestabilny” w stosunku do setjmp / longjmp. (Przykładowy kod, który ma znaczenie, podany jest w innych pytaniach i odpowiedziach ).
Tak więc bardziej precyzyjne pytanie brzmi:
czy „ulotność” w ogóle gwarantuje cokolwiek w przenośnym kodzie C dla systemów wielordzeniowych, oprócz (1) zezwalającego jednemu wątkowi programu na otrzymywanie informacji z jego procedury obsługi sygnału lub (2) zezwalającego na setjmp kod, aby zobaczyć zmienne zmienione między setjmp i longjmp?
To wciąż pytanie tak / nie.
Jeśli „tak”, byłoby świetnie, gdybyś mógł pokazać przykład bezbłędnego przenośnego kodu, który staje się błędny, jeśli pominięto „niestabilność”. Jeśli „nie”, to przypuszczam, że kompilator może zignorować „niestabilność” poza tymi dwoma bardzo szczególnymi przypadkami, dla celów wielordzeniowych.
volatile
informować program, że może on zmieniać się asynchronicznie.volatile
konkretnie, które moim zdaniem jest konieczne.Odpowiedzi:
Nie, absolutnie nie . To sprawia, że zmienność jest prawie bezużyteczna dla celów bezpiecznego kodu MT.
Gdyby tak było, zmienność byłaby całkiem dobra dla zmiennych współdzielonych przez wiele wątków, ponieważ porządkowanie zdarzeń w pamięci podręcznej L1 jest wszystkim, co musisz zrobić w typowym procesorze (czyli wielordzeniowym lub wieloprocesorowym na płycie głównej) zdolnym do współpracy w sposób, który umożliwia normalną implementację wielowątkowości C / C ++ lub Java przy typowych oczekiwanych kosztach (tj. nie jest to ogromny koszt większości operacji atomowych lub niezadowolonych muteksów).
Jednak lotność nie zapewnia żadnego gwarantowanego uporządkowania (ani „widoczności pamięci”) w pamięci podręcznej ani w teorii, ani w praktyce.
(Uwaga: poniższe informacje oparte są na solidnej interpretacji standardowych dokumentów, intencjach standardu, praktyce historycznej i głębokim zrozumieniu oczekiwań autorów kompilatorów. Takie podejście oparte jest na historii, rzeczywistych praktykach oraz oczekiwaniach i zrozumieniu prawdziwych osób w prawdziwy świat, który jest znacznie silniejszy i bardziej niezawodny niż parsowanie słów dokumentu, o którym nie wiadomo, że jest gwiezdnym pismem specyfikacji i który był wielokrotnie zmieniany).
W praktyce funkcja volatile gwarantuje zdolność ptrace, czyli możliwość korzystania z informacji debugowania dla uruchomionego programu, na dowolnym poziomie optymalizacji , oraz fakt, że informacje debugowania mają sens dla tych obiektów lotnych:
ptrace
(mechanizmu przypominającego ptrace), aby ustawić znaczące punkty przerwania w punktach sekwencji po operacjach z obiektami lotnymi: możesz naprawdę złamać dokładnie w tych punktach (pamiętaj, że działa to tylko wtedy, gdy chcesz ustawić wiele punktów przerwania jako dowolnych Instrukcja C / C ++ może zostać skompilowana do wielu różnych punktów początkowych i końcowych asemblera, np. W masowo rozwiniętej pętli);Lotna gwarancja w praktyce jest czymś więcej niż ścisłą interpretacją ptrace: gwarantuje także, że zmienne automatyczne zmienne mają adres na stosie, ponieważ nie są one przypisane do rejestru, alokacja rejestru, która uczyniłaby manipulacje ptrace bardziej delikatnymi (kompilator może wyświetla informacje o debugowaniu, aby wyjaśnić, w jaki sposób zmienne są przydzielane do rejestrów, ale odczytywanie i zmiana stanu rejestru jest nieco bardziej zaangażowana niż dostęp do adresów pamięci)
Należy pamiętać, że możliwość pełnego debugowania programu, która bierze pod uwagę wszystkie zmienne zmienne co najmniej w punktach sekwencji, zapewnia tryb kompilatora „optymalizacja zera”, tryb, który wciąż wykonuje trywialne optymalizacje, takie jak uproszczenia arytmetyczne (zwykle nie ma gwarancji, że nie optymalizacja we wszystkich trybach). Ale zmienność jest silniejsza niż brak optymalizacji:
x-x
można ją uprościć dla nieulotnej liczby całkowitej,x
ale nie dla lotnego obiektu.Tak niestabilne środki gwarantowane do kompilacji bez zmian, podobnie jak tłumaczenie ze źródła na plik binarny / asembler przez kompilator wywołania systemowego nie jest reinterpretacją, zmianą ani optymalizacją w jakikolwiek sposób przez kompilator. Pamiętaj, że wywołania biblioteczne mogą, ale nie muszą być wywołaniami systemowymi. Wiele oficjalnych funkcji systemowych jest w rzeczywistości funkcjami bibliotecznymi, które oferują cienką warstwę wstawiania i na ogół opóźniają jądro na końcu. (W szczególności
getpid
nie musi iść do jądra i może dobrze odczytać lokalizację pamięci dostarczoną przez system operacyjny zawierający informacje).Lotne interakcje to interakcje ze światem zewnętrznym prawdziwej maszyny , które muszą podążać za „maszyną abstrakcyjną”. Nie są to interakcje wewnętrzne części programu z innymi częściami programu. Kompilator może jedynie wnioskować o tym, co wie, czyli o wewnętrznych częściach programu.
Generowanie kodu dla niestabilnego dostępu powinno odbywać się zgodnie z najbardziej naturalną interakcją z tą lokalizacją pamięci: nie powinno być zaskoczeniem. Oznacza to, że oczekuje się, że niektóre niestabilne dostępy będą miały charakter atomowy : jeśli naturalny sposób odczytu lub zapisu reprezentacji
long
architektury jest atomowy, wówczas oczekuje się, że odczyt lub zapis avolatile long
będzie atomowy, ponieważ kompilator nie powinien generować na przykład głupi, nieefektywny kod dostępu do obiektów lotnych bajt po bajcie .Powinieneś być w stanie to ustalić, znając architekturę. Nie musisz nic wiedzieć o kompilatorze, ponieważ niestabilność oznacza, że kompilator powinien być przezroczysty .
Ale zmienna nie tylko wymusza emisję oczekiwanego zestawu dla najmniej zoptymalizowanych dla konkretnych przypadków operacji pamięciowej: lotna semantyka oznacza ogólną semantykę przypadków.
Ogólnym przypadkiem jest to, co robi kompilator, gdy nie ma żadnych informacji o konstrukcji: np. wywołanie funkcji wirtualnej na wartości za pomocą dynamicznej wysyłki to ogólny przypadek, wykonanie bezpośredniego wywołania nadrzędnego po określeniu w czasie kompilacji rodzaju obiektu oznaczonego przez wyrażenie jest szczególnym przypadkiem. Kompilator zawsze ma ogólną obsługę wszystkich konstrukcji i jest zgodny z ABI.
Volatile nie robi nic specjalnego w celu synchronizacji wątków lub zapewnienia „widoczności pamięci”: volatile zapewnia jedynie gwarancje na poziomie abstrakcyjnym widocznym z wnętrza wątku wykonującego się lub zatrzymanego, czyli z wnętrza rdzenia procesora :
Tylko drugi punkt oznacza, że niestabilność nie jest przydatna w większości problemów komunikacyjnych między wątkami; pierwszy punkt jest zasadniczo nieistotny w żadnym problemie programistycznym, który nie wymaga komunikacji ze składnikami sprzętowymi poza procesorami, ale nadal na szynie pamięci.
Właściwość niestabilnego zapewniania gwarantowanego zachowania z punktu widzenia rdzenia prowadzącego wątek oznacza, że sygnały asynchroniczne dostarczane do tego wątku, które są uruchamiane z punktu widzenia kolejności wykonywania tego wątku, patrz operacje w kolejności kodu źródłowego .
O ile nie planujesz wysyłać sygnałów do swoich wątków (niezwykle przydatne podejście do konsolidacji informacji o aktualnie działających wątkach bez wcześniej uzgodnionego punktu zatrzymania), niestabilność nie jest dla ciebie.
źródło
Nie jestem ekspertem, ale cppreference.com ma coś, co wydaje mi się dość dobrą informacją
volatile
. Oto jego sedno:Daje również pewne zastosowania:
I oczywiście wspomina, że
volatile
nie jest przydatne do synchronizacji wątków:źródło
longjmp
w kodzie C ++.Po pierwsze, historycznie występowały różne czkawki dotyczące różnych interpretacji znaczenia
volatile
dostępu i tym podobne. Zobacz to badanie: Składniki lotne są źle skompilowane i co z tym zrobić .Oprócz różnych zagadnień wymienionych w tym badaniu zachowanie
volatile
jest przenośne, z wyjątkiem jednego aspektu: kiedy działają jak bariery pamięci . Bariera pamięci jest pewnym mechanizmem, który ma zapobiegać równoczesnemu wykonywaniu kodu bez konsekwencji. Używanievolatile
jako bariery pamięci z pewnością nie jest przenośne.To, czy język C gwarantuje zachowanie pamięci, czy nie,
volatile
jest najwyraźniej sporne, chociaż osobiście uważam, że język jest jasny. Najpierw mamy formalną definicję skutków ubocznych, C17 5.1.2.3:Norma definiuje termin sekwencjonowania jako sposób określania kolejności oceny (wykonania). Definicja jest formalna i uciążliwa:
TL; DR powyższego jest zasadniczo takie, że w przypadku gdy mamy wyrażenie
A
zawierające skutki uboczne, należy to wykonać wykonując przed innym wyrażeniemB
, w przypadku, gdyB
jest ono sekwencjonowane późniejA
.Optymalizacja kodu C jest możliwa dzięki tej części:
Oznacza to, że program może oceniać (wykonywać) wyrażenia w kolejności zgodnej ze standardem w innym miejscu (kolejność oceny itp.). Ale nie musi oceniać (wykonywać) wartości, jeśli można wywnioskować, że nie jest ona używana. Na przykład operacja
0 * x
nie musi oceniaćx
i po prostu zamienia wyrażenie na0
.Chyba że dostęp do zmiennej jest efektem ubocznym. Co oznacza, że sprawa
x
jestvolatile
, to musi ocenić (wykonanie)0 * x
, mimo że wynik zawsze będzie 0. Optymalizacja nie jest dozwolone.Ponadto standard mówi o obserwowalnym zachowaniu:
Biorąc pod uwagę wszystkie powyższe, zgodna implementacja (kompilator + system bazowy) może nie wykonywać dostępu do
volatile
obiektów w niesekwencjonowanej kolejności, w przypadku, gdy semantyka zapisanego źródła C mówi inaczej.Oznacza to, że w tym przykładzie
Oba wyrażenia przypisania muszą zostać ocenione i
z = x;
muszą zostać ocenione wcześniejz = y;
. Implementacja wieloprocesorowa, która outsourcuje te dwie operacje do dwóch różnych rdzeni niesekwencyjnych, nie jest zgodna!Dylemat polega na tym, że kompilatory nie mogą wiele zrobić z takimi rzeczami, jak buforowanie pobierania z wyprzedzeniem i potokowanie instrukcji itp., Szczególnie nie w przypadku uruchamiania na systemie operacyjnym. I tak kompilatory przekazują ten problem programistom, mówiąc im, że bariery pamięci są teraz obowiązkiem programisty. Podczas gdy standard C wyraźnie stwierdza, że kompilator musi rozwiązać problem.
Kompilator niekoniecznie musi jednak rozwiązać problem, dlatego
volatile
działanie jako bariera pamięci jest nieprzenośne. Stało się to kwestią jakości wdrożenia.źródło
z
byłyby naprawdę wykonywane? (jakz = x; z = y;
) Wartość zostanie usunięta w następnej instrukcji.z
naprawdę jest przydzielany dwukrotnie? Skąd wiesz, że „odczyty są wykonywane”?