Zaktualizowano, patrz poniżej!
Słyszałem i przeczytałem, że C ++ 0x umożliwia kompilatorowi wydrukowanie „Hello” dla następującego fragmentu kodu
#include <iostream>
int main() {
while(1)
;
std::cout << "Hello" << std::endl;
}
Najwyraźniej ma to coś wspólnego z wątkami i możliwościami optymalizacji. Wydaje mi się jednak, że może to zaskoczyć wielu ludzi.
Czy ktoś ma dobre wyjaśnienie, dlaczego było to konieczne, aby zezwolić? Dla porównania, najnowsza wersja robocza C ++ 0x mówi o6.5/5
Pętla, która poza instrukcją for-init w przypadku instrukcji for,
- nie wywołuje funkcji biblioteki we / wy, a
- nie uzyskuje dostępu do obiektów ulotnych ani nie modyfikuje ich, oraz
- nie wykonuje operacji synchronizacji (1.10) ani operacji atomowych (klauzula 29)
może zostać uznane przez implementację za zakończone. [Uwaga: ma to na celu umożliwienie transformacji kompilatora, takich jak usuwanie pustych pętli, nawet jeśli nie można udowodnić zakończenia. - notatka końcowa]
Edytować:
Ten wnikliwy artykuł mówi o tym tekście Standardów
Niestety, słowa „niezdefiniowane zachowanie” nie są używane. Jednak za każdym razem, gdy standard mówi, że „kompilator może założyć P”, zakłada się, że program, który ma właściwość not-P, ma niezdefiniowaną semantykę.
Czy to prawda i czy kompilator może wydrukować „Do widzenia” dla powyższego programu?
Jest tu jeszcze bardziej wnikliwy wątek , który dotyczy analogicznej zmiany do C, zapoczątkowanej przez Guy'a wykonującego powyższy linkowany artykuł. Wśród innych przydatnych faktów przedstawiają rozwiązanie, które wydaje się mieć również zastosowanie do C ++ 0x ( Aktualizacja : To już nie zadziała z n3225 - patrz poniżej!)
endless:
goto endless;
Wydaje się, że kompilator nie może tego zoptymalizować, ponieważ nie jest to pętla, ale skok. Inny facet podsumowuje proponowaną zmianę w C ++ 0x i C201X
Pisząc pętlę, programista stwierdza , że albo pętla robi coś z widocznym zachowaniem (wykonuje operacje we / wy, uzyskuje dostęp do obiektów ulotnych, przeprowadza synchronizację lub operacje atomowe), albo że ostatecznie się kończy. Jeśli naruszę to założenie, pisząc nieskończoną pętlę bez skutków ubocznych, okłamuję kompilator, a zachowanie mojego programu jest nieokreślone. (Jeśli mam szczęście, kompilator może mnie o tym ostrzec.) Język nie zapewnia (już nie zapewnia?) Sposobu na wyrażenie nieskończonej pętli bez widocznego zachowania.
Aktualizacja z 3.1.2011 z n3225: Komisja przeniosła tekst do 1.10 / 24 i powiedz
Implementacja może zakładać, że dowolny wątek w końcu wykona jedną z następujących czynności:
- zakończyć,
- wywołanie funkcji wejścia / wyjścia biblioteki,
- uzyskać dostęp do zmiennego obiektu lub zmodyfikować go, lub
- wykonać operację synchronizacji lub operację atomową.
Ta goto
sztuczka już nie zadziała!
źródło
while(1) { MyMysteriousFunction(); }
muszą być niezależnie kompilowalne bez znajomości definicji tej tajemniczej funkcji, prawda? Jak więc możemy określić, czy wywołuje ona dowolne funkcje we / wy biblioteki? Innymi słowy: z pewnością ten pierwszy punktor mógłby zostać sformułowany w żaden sposób nie wywołuje funkcji .int x = 1; for(int i = 0; i < 10; ++i) do_something(&i); x++;
nafor(int i = 0; i < 10; ++i) do_something(&i); int x = 2;
? Lub być może w drugą stronę, zx
inicjalizacją2
przed pętlą. Potrafi stwierdzić,do_something
że nie dba o wartośćx
, więc jest to całkowicie bezpieczna optymalizacja, jeślido_something
nie powoduje zmiany wartości wi
taki sposób, że kończy się w nieskończonej pętli.main() { start_daemon_thread(); while(1) { sleep(1000); } }
może natychmiast zakończyć działanie, zamiast uruchamiać demona w wątku w tle?Odpowiedzi:
Tak, Hans Boehm uzasadnia to w N1528: Dlaczego niezdefiniowane zachowanie dla nieskończonych pętli? chociaż jest to dokument WG14, uzasadnienie dotyczy również C ++ i dokument odnosi się zarówno do WG14, jak i WG21:
Jedną z głównych różnic w C jest to, że C11 zapewnia wyjątek dla kontrolowania wyrażeń, które są wyrażeniami stałymi, które różnią się od C ++ i sprawiają, że twój konkretny przykład jest dobrze zdefiniowany w C11.
źródło
do { x = slowFunctionWithNoSideEffects(x);} while(x != 23);
kod Hoisting po pętli, od którego nie byłby zależny,x
wydawałby się bezpieczny i rozsądny, ale zezwolenie kompilatorowi na założeniex==23
w takim kodzie wydaje się bardziej niebezpieczne niż przydatne.Dla mnie odpowiednim uzasadnieniem jest:
Prawdopodobnie dzieje się tak dlatego, że mechaniczne udowodnienie zakończenia jest trudne , a niemożność udowodnienia zakończenia utrudnia kompilatorom, które w innym przypadku mogłyby dokonać użytecznych transformacji, takich jak przenoszenie niezależnych operacji z przed pętlą do po lub odwrotnie, wykonywanie operacji po pętli w jednym wątku, podczas gdy pętla jest wykonywana w innej i tak dalej. Bez tych przekształceń pętla mogłaby zablokować wszystkie inne wątki podczas oczekiwania na zakończenie wspomnianej pętli przez jeden wątek. (Używam luźno terminu „wątek” na oznaczenie dowolnej formy przetwarzania równoległego, w tym oddzielnych strumieni instrukcji VLIW).
EDYCJA: głupi przykład:
W tym przypadku jeden wątek mógłby szybciej wykonać
complex_io_operation
drugą, podczas gdy drugi wykonuje wszystkie złożone obliczenia w pętli. Ale bez cytowanej klauzuli kompilator musi udowodnić dwie rzeczy, zanim będzie mógł dokonać optymalizacji: 1) któracomplex_io_operation()
nie zależy od wyników pętli oraz 2) że pętla się zakończy . Dowodzenie 1) jest dość łatwe, udowodnienie 2) jest problemem zatrzymania. Z klauzulą może założyć, że pętla się kończy i uzyskać wygraną z równoległością.Wyobrażam sobie również, że projektanci uważali, że przypadki, w których nieskończone pętle występują w kodzie produkcyjnym, są bardzo rzadkie i zwykle są to takie rzeczy jak pętle sterowane zdarzeniami, które w jakiś sposób uzyskują dostęp do I / O. W rezultacie pesymizowali rzadki przypadek (nieskończone pętle) na rzecz optymalizacji bardziej powszechnego przypadku (nieskończony, ale trudny do mechanicznego udowodnienia niedokończony, pętle).
Oznacza to jednak, że w rezultacie ucierpi nieskończone pętle używane w przykładach uczenia się, co spowoduje problemy w kodzie początkującego. Nie mogę powiedzieć, że to całkiem dobra rzecz.
EDYCJA: w odniesieniu do wnikliwego artykułu, który teraz łączysz, powiedziałbym, że „kompilator może założyć, że X na temat programu” jest logicznym odpowiednikiem „jeśli program nie spełnia wymagań X, zachowanie jest niezdefiniowane”. Możemy to pokazać w następujący sposób: załóżmy, że istnieje program, który nie spełnia właściwości X. Gdzie zdefiniowano by zachowanie tego programu? Standard definiuje tylko zachowanie przy założeniu, że właściwość X jest prawdziwa. Chociaż standard nie deklaruje wyraźnie zachowania jako niezdefiniowane, zadeklarował, że jest ono niezdefiniowane przez pominięcie.
Rozważmy podobny argument: „kompilator może założyć, że zmienna x jest przypisana najwyżej raz między punktami sekwencji” jest równoważne z „przypisaniem do x więcej niż raz między punktami sekwencji jest niezdefiniowane”.
źródło
complex_io_operation
.Myślę, że właściwą interpretacją jest ta z twojej edycji: puste nieskończone pętle to niezdefiniowane zachowanie.
Nie powiedziałbym, że jest to szczególnie intuicyjne zachowanie, ale ta interpretacja ma więcej sensu niż alternatywna, że kompilator może arbitralnie ignorować nieskończone pętle bez wywoływania UB.
Jeśli nieskończone pętle to UB, oznacza to po prostu, że programy nie kończące się nie są uważane za sensowne: zgodnie z C ++ 0x, nie mają semantyki.
To też ma sens. Są to szczególny przypadek, w którym wiele efektów ubocznych po prostu już nie występuje (na przykład nic nie jest zwracane
main
), a wiele optymalizacji kompilatora jest utrudnionych przez konieczność zachowania nieskończonych pętli. Na przykład przenoszenie obliczeń w pętli jest całkowicie poprawne, jeśli pętla nie ma skutków ubocznych, ponieważ ostatecznie obliczenia zostaną wykonane w każdym przypadku. Ale jeśli pętla nigdy nie kończy, nie możemy kod bezpiecznie przestawiać poprzek niego, bo może być po prostu zmieniając których operacje faktycznie zostanie wykonany przed zawiesza programowych. Chyba że potraktujemy zawieszony program jako UB.źródło
while(1){...}
. Nawet rutynowo używająwhile(1);
do wywoływania resetowania wspomaganego przez watchdog.Myślę, że jest to podobne do tego typu pytania , które odwołuje się do innego wątku . Optymalizacja może czasami usuwać puste pętle.
źródło
X
i wzorca bitowegox
kompilator może wykazać, że pętla nie kończy się bezX
zatrzymania wzorca bitowegox
. To bezmyślnie prawda.X
Można więc uznać, że zawiera każdy wzór bitu, a to jest tak samo złe jak UB w tym sensie, że za złoX
ix
szybko spowoduje pewne. Dlatego uważam, że musisz być bardziej precyzyjny w swoim standardowym języku. Trudno jest mówić o tym, co dzieje się „na końcu” nieskończonej pętli i pokazywać to jako odpowiednik jakiejś skończonej operacji.Istotną kwestią jest to, że kompilator może zmienić kolejność kodu, którego skutki uboczne nie powodują konfliktów. Zaskakująca kolejność wykonywania mogłaby wystąpić nawet wtedy, gdyby kompilator wygenerował niezakończony kod maszynowy dla nieskończonej pętli.
Uważam, że to właściwe podejście. Specyfikacja języka określa sposoby egzekwowania porządku wykonania. Jeśli chcesz mieć nieskończoną pętlę, której nie można zmienić w kolejności, napisz:
źródło
unsigned int dummy; while(1){dummy++;} fprintf(stderror,"Hey\r\n"); fprintf(stderror,"Result was %u\r\n",dummy);
, pierwszyfprintf
mógł zostać wykonany, ale drugi nie (kompilator mógł przenieść obliczeniadummy
między nimifprintf
, ale nie poza to, które drukuje jego wartość).Wydaje mi się, że najlepiej byłoby określić ten problem, ponieważ „Jeśli późniejszy fragment kodu nie zależy od wcześniejszego fragmentu kodu, a wcześniejszy fragment kodu nie ma skutków ubocznych w żadnej innej części systemu, dane wyjściowe kompilatora może wykonać późniejszy fragment kodu przed, po lub zmieszany z wykonaniem pierwszego, nawet jeśli ten pierwszy zawiera pętle, bez względu na to, kiedy i czy poprzedni kod faktycznie się zakończy . Na przykład kompilator może przepisać:
tak jak
Generalnie nie jest to nierozsądne, chociaż mogę się martwić, że:
stanie się
Dzięki temu, że jeden procesor obsługuje obliczenia, a inny obsługuje aktualizacje paska postępu, przepisanie poprawiłoby wydajność. Niestety, aktualizacje paska postępu byłyby raczej mniej przydatne niż powinny.
źródło
Nie jest rozstrzygalne dla kompilatora w nietrywialnych przypadkach, jeśli w ogóle jest to nieskończona pętla.
W różnych przypadkach może się zdarzyć, że optymalizator osiągnie lepszą klasę złożoności dla twojego kodu (np. Było to O (n ^ 2) i po optymalizacji otrzymasz O (n) lub O (1)).
Tak więc włączenie takiej reguły, która zabrania usuwania nieskończonej pętli w standardzie C ++, uniemożliwiłoby wiele optymalizacji. A większość ludzi tego nie chce. Myślę, że to całkiem odpowiada na twoje pytanie.
Inna sprawa: nigdy nie widziałem żadnego ważnego przykładu, w którym potrzebna jest nieskończona pętla, która nic nie robi.
Jedynym przykładem, o którym słyszałem, był brzydki hack, który naprawdę powinien zostać rozwiązany w przeciwnym razie: dotyczyło to systemów wbudowanych, w których jedynym sposobem na wyzwolenie resetu było zamrożenie urządzenia, aby strażnik automatycznie uruchomił je ponownie.
Jeśli znasz jakiś ważny / dobry przykład, w którym potrzebujesz nieskończonej pętli, która nic nie robi, powiedz mi.
źródło
Myślę, że warto wskazać, że pętle, które byłyby nieskończone, gdyby nie fakt, że współdziałają z innymi wątkami za pośrednictwem nieulotnych, niezsynchronizowanych zmiennych, mogą teraz dawać nieprawidłowe zachowanie z nowym kompilatorem.
Innymi słowy, sprawiaj, że twoje globals będą ulotne - podobnie jak argumenty przekazywane do takiej pętli za pośrednictwem wskaźnika / referencji.
źródło
volatile
nie jest ani konieczne, ani wystarczające, a to dramatycznie szkodzi wydajności.