Czy „niestabilność” gwarantuje cokolwiek w przenośnym kodzie C dla systemów wielordzeniowych?

12

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:

  1. Wydaje się, że zapewnia różne gwarancje w zależności od sprzętu i kompilatora.
  2. 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.

Matt
źródło
3
Sygnały istnieją w przenośnym C; co ze zmienną globalną aktualizowaną przez moduł obsługi sygnału? Musi to volatileinformować program, że może on zmieniać się asynchronicznie.
Nate Eldredge
2
@NateEldredge Global, choć sama zmienność, nie jest wystarczająco dobra. Musi być również atomowy.
Eugene Sh.
@EugeneSh .: Tak, oczywiście. Ale omawiane pytanie dotyczy volatilekonkretnie, które moim zdaniem jest konieczne.
Nate Eldredge
podczas gdy koordynacja z pamięcią podręczną L1 nie gwarantuje nic więcej odnośnie koordynacji z innymi wątkami ” Gdzie „koordynacja z pamięcią podręczną L1” nie jest wystarczająca do komunikacji z innymi wątkami?
ciekawy,
1
Być może istotna, propozycja C ++, aby przestała być niestabilna , propozycja dotyczy wielu poruszonych tu obaw, a być może jej wynik będzie miał wpływ na komisję C
MM

Odpowiedzi:

1

Podsumowując problem, wydaje się (po dużym przeczytaniu), ż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 .

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:

  • możesz użyć 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);
  • podczas gdy wątek wykonania zatrzymanego, możesz odczytać wartość wszystkich obiektów lotnych, ponieważ mają one reprezentację kanoniczną (zgodnie z ABI dla ich odpowiedniego typu); nieulotna zmienna lokalna może mieć nietypową reprezentację, np. przesunięta reprezentacja: zmienną używaną do indeksowania tablicy można pomnożyć przez rozmiar poszczególnych obiektów, aby ułatwić indeksowanie; lub może zostać zastąpiony wskaźnikiem do elementu tablicy (o ile wszystkie zastosowania zmiennej są podobnie przekonwertowane) (pomyśl o zmianie dx na du w całce);
  • możesz także modyfikować te obiekty (o ile pozwalają na to odwzorowania pamięci), ponieważ obiekt lotny o stałym czasie życia, który ma stałą kwalifikację, może znajdować się w zakresie pamięci mapowanym tylko do odczytu).

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-xmożna ją uprościć dla nieulotnej liczby całkowitej, xale 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 getpidnie 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 longarchitektury jest atomowy, wówczas oczekuje się, że odczyt lub zapis a volatile longbę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 :

  • volatile nie mówi nic o tym, które operacje pamięci osiągają główną pamięć RAM (możesz ustawić określone typy buforowania pamięci za pomocą instrukcji montażu lub wywołań systemowych, aby uzyskać te gwarancje);
  • volatile nie daje żadnej gwarancji, kiedy operacje pamięci zostaną przypisane do dowolnego poziomu pamięci podręcznej (nawet L1) .

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.

ciekawy
źródło
6

Nie jestem ekspertem, ale cppreference.com ma coś, co wydaje mi się dość dobrą informacjąvolatile . Oto jego sedno:

Każdy dostęp (zarówno do odczytu, jak i zapisu) uzyskany za pomocą wyrażenia wartości typu lotnego kwalifikowanego jest uważany za możliwy do zaobserwowania efekt uboczny w celu optymalizacji i jest oceniany ściśle zgodnie z regułami maszyny abstrakcyjnej (tzn. Wszystkie zapisy są wykonywane w jakiś czas przed następnym punktem sekwencji). Oznacza to, że w ramach jednego wątku wykonania nie można zoptymalizować lub zmienić kolejności dostępu lotnego w stosunku do innego widocznego efektu ubocznego oddzielonego punktem sekwencyjnym od dostępu lotnego.

Daje również pewne zastosowania:

Zastosowania lotnych

1) statyczne obiekty lotne modelują porty we / wy mapowane w pamięci oraz statyczne stałe obiekty lotne modelują porty wejściowe mapowane w pamięci, takie jak zegar czasu rzeczywistego

2) statyczne lotne obiekty typu sig_atomic_t służą do komunikacji z modułami obsługi sygnałów.

3) zmienne zmienne lokalne dla funkcji zawierającej wywołanie makra setjmp są jedynymi zmiennymi lokalnymi, które zachowają swoje wartości po powrocie longjmp.

4) Dodatkowo, zmienne zmienne mogą być użyte do wyłączenia niektórych form optymalizacji, np. Do wyłączenia eliminacji martwego magazynu lub ciągłego fałdowania dla mikrodruków.

I oczywiście wspomina, że volatilenie jest przydatne do synchronizacji wątków:

Zauważ, że zmienne zmienne nie są odpowiednie do komunikacji między wątkami; nie oferują atomizacji, synchronizacji ani porządkowania pamięci. Odczyt ze zmiennej zmiennej, która jest modyfikowana przez inny wątek bez synchronizacji lub jednoczesnej modyfikacji z dwóch niezsynchronizowanych wątków, jest niezdefiniowanym zachowaniem z powodu wyścigu danych.

Fred Larson
źródło
2
W szczególności (2) i (3) dotyczą kodu przenośnego.
Nate Eldredge
2
@TED ​​Pomimo nazwy domeny link prowadzi do informacji o C, a nie C ++
David Brown
@NateEldredge Rzadko można używać longjmpw kodzie C ++.
ciekawy
@DavidBrown C i C ++ mają tę samą definicję obserwowalnego SE i zasadniczo te same prymitywy wątków.
ciekawy,
4

Po pierwsze, historycznie występowały różne czkawki dotyczące różnych interpretacji znaczenia volatiledostę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 volatilejest 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żywanie volatilejako bariery pamięci z pewnością nie jest przenośne.

To, czy język C gwarantuje zachowanie pamięci, czy nie, volatilejest 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:

Dostęp do volatileobiektu, modyfikacja obiektu, modyfikacja pliku lub wywołanie funkcji wykonującej którąkolwiek z tych operacji to wszystkie skutki uboczne , które są zmianami w stanie środowiska wykonawczego.

Norma definiuje termin sekwencjonowania jako sposób określania kolejności oceny (wykonania). Definicja jest formalna i uciążliwa:

Sekwencjonowana wcześniej jest asymetryczna, przechodnia, parowa relacja między ocenami wykonywanymi przez pojedynczy wątek, która indukuje częściowy porządek między tymi ocenami. Biorąc pod uwagę dowolne dwie oceny A i B, jeśli A jest sekwencjonowane przed B, wówczas wykonanie A poprzedza wykonanie B (odwrotnie, jeśli A jest sekwencjonowane przed B, to B jest sekwencjonowane po A.) Jeśli A nie jest sekwencjonowane przed lub po B, wówczas a i B są unsequenced . Oceny A i B są sekwencjonowane w nieokreślony sposób, gdy A jest sekwencjonowane przed lub po B, ale nie jest określone, które 13) Obecność punktu sekwencji między oceną wyrażeń A i B oznacza, że ​​każde obliczenie wartości i efekt uboczny związany z A jest sekwencjonowane przed każdym obliczeniem wartości i efektem ubocznym związanym z B. (Podsumowanie punktów sekwencji podano w załączniku C.)

TL; DR powyższego jest zasadniczo takie, że w przypadku gdy mamy wyrażenie Azawierające skutki uboczne, należy to wykonać wykonując przed innym wyrażeniem B, w przypadku, gdy Bjest ono sekwencjonowane później A.

Optymalizacja kodu C jest możliwa dzięki tej części:

W maszynie abstrakcyjnej wszystkie wyrażenia są obliczane zgodnie z semantyką. Rzeczywista implementacja nie musi oceniać części wyrażenia, jeśli można wywnioskować, że jego wartość nie jest używana i że nie powstają żadne potrzebne skutki uboczne (w tym wszelkie wywołane wywołaniem funkcji lub dostępem do lotnego obiektu).

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 * xnie musi oceniać xi po prostu zamienia wyrażenie na 0.

Chyba że dostęp do zmiennej jest efektem ubocznym. Co oznacza, że sprawa xjest volatile, to musi ocenić (wykonanie) 0 * x, mimo że wynik zawsze będzie 0. Optymalizacja nie jest dozwolone.

Ponadto standard mówi o obserwowalnym zachowaniu:

Najmniejsze wymagania dotyczące zgodnej implementacji to:

  • Dostęp do obiektów lotnych ocenia się ściśle według zasad maszyny abstrakcyjnej.
    / - / To jest możliwe do zaobserwowania zachowanie programu.

Biorąc pod uwagę wszystkie powyższe, zgodna implementacja (kompilator + system bazowy) może nie wykonywać dostępu do volatileobiektów w niesekwencjonowanej kolejności, w przypadku, gdy semantyka zapisanego źródła C mówi inaczej.

Oznacza to, że w tym przykładzie

volatile int x;
volatile int y;
z = x;
z = y;

Oba wyrażenia przypisania muszą zostać ocenione i z = x; muszą zostać ocenione wcześniej z = 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 volatiledziałanie jako bariera pamięci jest nieprzenośne. Stało się to kwestią jakości wdrożenia.

Lundin
źródło
@curiousguy Nieważne.
Lundin,
@curiousguy Nie ma znaczenia, o ile jest to rodzaj liczb całkowitych z kwalifikatorami lub bez nich.
Lundin,
Jeśli jest to prosta nieulotna liczba całkowita, dlaczego zbędne zapisy zbyłyby naprawdę wykonywane? (jak z = x; z = y;) Wartość zostanie usunięta w następnej instrukcji.
ciekawy,
@curiousguy Ponieważ odczyty zmiennych nietrwałych muszą być wykonywane bez względu na to, w określonej kolejności.
Lundin,
Czy tak znaprawdę jest przydzielany dwukrotnie? Skąd wiesz, że „odczyty są wykonywane”?
ciekawy