Patrząc na ten kod:
static int global_var = 0;
int update_three(int val)
{
global_var = val;
return 3;
}
int main()
{
int arr[5];
arr[global_var] = update_three(2);
}
Który wpis tablicy zostanie zaktualizowany? 0 czy 2?
Czy w specyfikacji C jest część wskazująca na pierwszeństwo działania w tym konkretnym przypadku?
c
language-lawyer
order-of-execution
Jiminion
źródło
źródło
clang
ten fragment kodu wywołał ostrzeżenie IMHO.Odpowiedzi:
Kolejność lewych i prawych argumentów
Aby wykonać przypisanie w
arr[global_var] = update_three(2)
, implementacja C musi ocenić operandy i, jako efekt uboczny, zaktualizować zapamiętaną wartość lewego operandu. C 2018 6.5.16 (który dotyczy przydziałów) paragraf 3 mówi nam, że nie ma sekwencjonowania w lewym i prawym operandzie:Oznacza to, że implementacja języka C może swobodnie obliczać wartość
arr[global_var]
najpierw pierwszą (obliczając wartość, rozumiemy, do czego odnosi się to wyrażenie), a następnie ocenićupdate_three(2)
, a na koniec przypisać wartość drugiej wartości temu pierwszemu; lubupdate_three(2)
najpierw ocenić , a następnie obliczyć wartość, a następnie przypisać pierwszą z nich do drugiej; lub w celu oceny wartości iupdate_three(2)
w pewien sposób zmieszany, a następnie przypisz odpowiednią wartość do lewej wartości.We wszystkich przypadkach przypisanie wartości do wartości musi być ostatnie, ponieważ 6.5.16 3 mówi również:
Naruszenie zasad sekwencjonowania
Niektórzy mogą zastanawiać się nad nieokreślonym zachowaniem z powodu używania go
global_var
i osobnej aktualizacji z naruszeniem 6.5 2, co mówi:Dla wielu praktyków języka C jest dość znane, że zachowanie wyrażeń, które
x + x++
nie jest zdefiniowane w standardzie C, ponieważ oba używają wartościx
i oddzielnie ją modyfikują w tym samym wyrażeniu bez sekwencjonowania. Jednak w tym przypadku mamy wywołanie funkcji, które zapewnia pewne sekwencjonowanie.global_var
jest używanyarr[global_var]
i jest aktualizowany w wywołaniu funkcjiupdate_three(2)
.6.5.2.2 10 mówi nam, że przed wywołaniem funkcji jest punkt sekwencji:
Wewnątrz funkcji
global_var = val;
znajduje się pełne wyrażenie , podobnie jak3
inreturn 3;
, na 6,8 4:Następnie między tymi dwoma wyrażeniami występuje punkt sekwencyjny, ponownie na 6,8 4:
Zatem implementacja C może
arr[global_var]
najpierw ocenić, a następnie wykonać wywołanie funkcji, w którym to przypadku między nimi znajduje się punkt sekwencji, ponieważ jest jeden przed wywołaniem funkcji, lub może ocenićglobal_var = val;
w wywołaniu funkcji, a następniearr[global_var]
, w którym to przypadku występuje punkt sekwencji między nimi, ponieważ jest jeden po pełnym wyrażeniu. Zachowanie jest więc nieokreślone - jedną z tych dwóch rzeczy może być najpierw ocenione - ale nie jest niezdefiniowane.źródło
Wynik tutaj jest nieokreślony .
Chociaż kolejność operacji w wyrażeniu, które określają sposób grupowania podwyrażeń, jest dobrze zdefiniowana, kolejność oceny nie jest określona. W takim przypadku oznacza to, że albo
global_var
można go przeczytać jako pierwszy, albo wezwanie doupdate_three
, ale nie ma sposobu, aby wiedzieć, który.Jest nie niezdefiniowane zachowanie się tutaj, ponieważ wywołanie funkcji wprowadza punkt sekwencji, tak jak każde oświadczenie w funkcji w tym jeden, który modyfikuje
global_var
.Aby wyjaśnić, norma C definiuje niezdefiniowane zachowanie w sekcji 3.4.3 jako:
i definiuje nieokreślone zachowanie w sekcji 3.4.4 jako:
Norma stwierdza, że kolejność oceny argumentów funkcji jest nieokreślona, co w tym przypadku oznacza, że albo
arr[0]
zostanie ustawiona na 3, alboarr[2]
na 3.źródło
Próbowałem i zaktualizowałem wpis 0.
Jednak zgodnie z tym pytaniem: czy wyrażenie po prawej stronie zawsze będzie oceniane jako pierwsze
Kolejność oceny jest nieokreślona i nie ma konsekwencji. Dlatego uważam, że takiego kodu należy unikać.
źródło
Ponieważ nie ma sensu wysyłać kodu dla przypisania, zanim będzie można przypisać wartość, większość kompilatorów C najpierw wyemituje kod wywołujący funkcję i zapisze gdzieś wynik (rejestr, stos itp.), A następnie wyemituje kod, który zapisuje tę wartość do ostatecznego miejsca docelowego i dlatego odczytują zmienną globalną po jej zmianie. Nazwijmy to „porządkiem naturalnym”, nieokreślonym żadnym standardem, lecz czystą logiką.
Jednak w trakcie optymalizacji kompilatory spróbują wyeliminować pośredni krok tymczasowego przechowywania wartości i spróbują zapisać wynik funkcji tak bezpośrednio, jak to możliwe, do ostatecznego miejsca docelowego, w takim przypadku często będą musieli najpierw przeczytać indeks , np. do rejestru, aby móc bezpośrednio przenieść wynik funkcji do tablicy. Może to spowodować odczyt globalnej zmiennej przed jej zmianą.
Jest to więc zasadniczo niezdefiniowane zachowanie z bardzo złą właściwością, że jest całkiem prawdopodobne, że wynik będzie inny, w zależności od tego, czy przeprowadzana jest optymalizacja i jak agresywna jest ta optymalizacja. Twoim zadaniem jako programisty jest rozwiązanie tego problemu poprzez zakodowanie:
lub kodowanie:
Ogólna zasada: jeśli zmienne globalne nie są
const
(lub nie są, ale wiesz, że żaden kod nigdy ich nie zmieni jako efekt uboczny), nigdy nie powinieneś ich używać bezpośrednio w kodzie, jak w środowisku wielowątkowym, nawet tego nie można zdefiniować:Ponieważ kompilator może go odczytać dwa razy, a inny wątek może zmienić wartość pomiędzy dwoma odczytami. Jednak optymalizacja z pewnością spowodowałaby, że kod przeczytałby go tylko raz, więc możesz ponownie uzyskać różne wyniki, które teraz zależą również od czasu innego wątku. W ten sposób będziesz mieć o wiele mniej bólu głowy, jeśli przed użyciem przechowujesz zmienne globalne w tymczasowej zmiennej stosu. Należy pamiętać, że jeśli kompilator uważa, że jest to bezpieczne, najprawdopodobniej zoptymalizuje nawet to, a zamiast tego bezpośrednio użyje zmiennej globalnej, więc w końcu może nie mieć różnicy w wydajności lub wykorzystaniu pamięci.
(Na wypadek, gdyby ktoś zapytał, dlaczego miałby to zrobić
x + 2 * x
zamiast3 * x
- na niektórych procesorach dodawanie jest ultraszybkie, podobnie jak mnożenie przez potęgę drugą, ponieważ kompilator zamieni je na przesunięcia bitów (2 * x == x << 1
), jednak mnożenie z dowolnymi liczbami może być bardzo wolne , dlatego zamiast mnożenia przez 3, uzyskujesz znacznie szybszy kod poprzez przesunięcie bitów x o 1 i dodanie x do wyniku - a nawet ten trik jest wykonywany przez nowoczesne kompilatory, jeśli pomnożysz przez 3 i włączysz agresywną optymalizację, chyba że jest to nowoczesny cel Procesor, w którym mnożenie jest równie szybkie jak dodawanie, ponieważ sztuczka spowolniłaby obliczenia).źródło
3 * x
w dwa odczyty x. Może odczytać x raz, a następnie wykonać metodę x + 2 * x na rejestrze, w którym wczytał x dolanguage-lawyer
, gdy dany język ma swoje własne „bardzo szczególne znaczenie” dla nieokreślonych , wprowadzisz zamieszanie, nie używając definicja języka.Globalna edycja: przepraszam chłopaki, wyrzuciłem wszystkich z pracy i napisałem wiele bzdur. Tylko stary geezer narzekający.
Chciałem wierzyć, że C zostało oszczędzone, ale niestety od C11 zostało wyrównane z C ++. Najwyraźniej wiedza o tym, co kompilator zrobi z efektami ubocznymi wyrażeń, wymaga teraz rozwiązania małej zagadki matematycznej polegającej na częściowym uporządkowaniu sekwencji kodu na podstawie „znajduje się przed punktem synchronizacji”.
Zdarzyło mi się zaprojektować i wdrożyć kilka krytycznych systemów osadzonych w czasie rzeczywistym w czasach K&R (w tym kontroler samochodu elektrycznego, który mógłby wysłać ludzi uderzających w najbliższą ścianę, gdyby silnik nie był kontrolowany, 10 ton przemysłowych robot, który mógłby zmiażdżyć ludzi na miazgę, jeśli nie byłby właściwie dowodzony, oraz warstwa systemowa, która, choć nieszkodliwa, miałaby kilkadziesiąt procesorów wysysających szynę danych z mniej niż 1% obciążenia systemowego).
Mogę być zbyt ostrożny lub głupi, aby dostrzec różnicę między niezdefiniowanym a nieokreślonym, ale myślę, że nadal mam całkiem niezłe pojęcie o tym, co oznacza równoczesne wykonywanie i dostęp do danych. W mojej, prawdopodobnie uzasadnionej opinii, ta obsesja na punkcie C ++, a teraz C facetów z ich językami domowymi przejmującymi problemy z synchronizacją, jest kosztownym marzeniem. Albo wiesz, co to jest równoczesne wykonywanie, i nie potrzebujesz żadnego z tych gadżetów, albo nie, i zrobiłbyś światu przysługę, nie próbując go zepsuć.
Cała ta ogromna ilość abstrakcyjnych barier pamięci jest po prostu spowodowana tymczasowym zestawem ograniczeń wieloprocesorowych systemów pamięci podręcznej, z których wszystkie można bezpiecznie zamknąć we wspólnych obiektach synchronizacji systemu operacyjnego, takich jak na przykład muteksy i zmienne warunkowe C ++ oferuje.
Koszt takiej enkapsulacji to zaledwie drobny spadek wydajności w porównaniu z tym, co może osiągnąć zastosowanie drobnoziarnistych instrukcji dla konkretnego procesora w niektórych przypadkach.
The
volatile
kluczowe (lub a#pragma dont-mess-with-that-variable
dla mnie, jako programisty systemu, opieka) byłaby wystarczająca, aby powiedzieć kompilatorowi, aby przestał zmieniać kolejność dostępu do pamięci. Optymalny kod może być łatwo wytworzony za pomocą bezpośrednich dyrektyw asm, aby posypać kod sterownika niskiego poziomu i kod systemu operacyjnego instrukcjami ad hoc CPU. Bez dogłębnej wiedzy o tym, jak działa podstawowy sprzęt (system pamięci podręcznej lub interfejs magistrali), i tak będziesz musiał pisać bezużyteczny, nieefektywny lub wadliwy kod.Drobna korekta
volatile
słowa kluczowego i Boba byłaby wszystkim, ale najbardziej wujek programistów niskiego poziomu. Zamiast tego zwykły gang maniaków matematyki w C ++ miał dzień w terenie, projektując jeszcze jedną niezrozumiałą abstrakcję, ulegając ich typowej tendencji do projektowania rozwiązań szukających nieistniejących problemów i mylących definicję języka programowania ze specyfikacjami kompilatora.Tylko tym razem zmiana wymagała zniszczenia podstawowego aspektu języka C, ponieważ te „bariery” musiały zostać wygenerowane nawet w kodzie niskiego poziomu, aby działały poprawnie. To między innymi spowodowało spustoszenie w definicji wyrażeń, bez żadnego wyjaśnienia ani uzasadnienia.
Podsumowując, fakt, że kompilator może wytworzyć spójny kod maszynowy z tego absurdalnego fragmentu C, jest tylko daleką konsekwencją sposobu, w jaki ludzie C ++ radzili sobie z potencjalnymi niespójnościami systemów pamięci podręcznej pod koniec 2000 roku.
To spowodowało straszny bałagan jednego podstawowego aspektu C (definicja wyrażenia), tak że ogromna większość programistów C - którzy nie dbają o systemy pamięci podręcznej i słusznie - jest teraz zmuszona polegać na guru, aby wyjaśnić różnica między
a = b() + c()
ia = b + c
.Zgadywanie, co stanie się z tym niefortunnym zestawem, jest i tak stratą czasu i wysiłku. Niezależnie od tego, co zrobi z niego kompilator, ten kod jest patologicznie niepoprawny. Jedyną odpowiedzialną rzeczą, jaką można z tym zrobić, jest wysłanie go do kosza.
Koncepcyjnie, skutki uboczne można zawsze usunąć z wyrażeń, przy trywialnym wysiłku wyraźnego zezwolenia na modyfikację przed lub po ocenie, w osobnym oświadczeniu.
Ten gówniany kod mógł być usprawiedliwiony w latach 80-tych, kiedy nie można było oczekiwać, że kompilator coś zoptymalizuje. Ale teraz, gdy kompilatory od dawna stają się mądrzejsze niż większość programistów, pozostaje tylko kawałek gównianego kodu.
Nie rozumiem również znaczenia tej nieokreślonej / nieokreślonej debaty. Albo możesz polegać na kompilatorze do generowania kodu o spójnym działaniu, albo nie możesz. To, czy nazywasz to niezdefiniowanym, czy nieokreślonym, wydaje się kwestią sporną.
Według mojej prawdopodobnie poinformowanej opinii C jest już wystarczająco niebezpieczny w swoim stanie K&R. Przydatną ewolucją byłoby dodanie zdrowych środków bezpieczeństwa. Na przykład, korzystając z tego zaawansowanego narzędzia do analizy kodu, specyfikacje zmuszają kompilator do wdrożenia przynajmniej generowania ostrzeżeń o kodzie bonkers, zamiast cichego generowania kodu potencjalnie niewiarygodnego.
Zamiast tego faceci postanowili na przykład zdefiniować stały porządek oceny w C ++ 17. Teraz każde imbecylowe oprogramowanie jest aktywnie zachęcane do celowego umieszczania efektów ubocznych w swoim kodzie, pławiąc się w przekonaniu, że nowe kompilatory chętnie zajmą się zaciemnianiem w deterministyczny sposób.
K&R był jednym z prawdziwych cudów świata komputerowego. Za dwadzieścia dolców masz pełną specyfikację języka (widziałem, jak pojedyncze osoby piszą kompletne kompilatory za pomocą tej książki), doskonałą instrukcję obsługi (spis treści zwykle wskazywałby na kilka stron odpowiedzi na twoje pytanie) pytanie) oraz podręcznik, który nauczy Cię rozsądnego posługiwania się językiem. Uzupełnij go uzasadnieniem, przykładami i mądrymi słowami ostrzegającymi o licznych sposobach nadużywania języka do robienia bardzo, bardzo głupich rzeczy.
Zniszczenie tego dziedzictwa za tak mały zysk wydaje mi się okrutnym marnotrawstwem. Ale znowu mogę nie rozumieć tego całkowicie. Może jakaś miła dusza mogłaby wskazać mi przykład nowego kodu C, który w znacznym stopniu wykorzystuje te skutki uboczne?
źródło
0,expr,0
.