To jest błąd Clanga
... podczas wstawiania funkcji zawierającej nieskończoną pętlę. Zachowanie jest inne, gdy while(1);
pojawia się bezpośrednio w głównym, który pachnie dla mnie bardzo błędnie.
Zobacz odpowiedź @ Arnavion na podsumowanie i linki. Reszta tej odpowiedzi została napisana, zanim otrzymałem potwierdzenie, że był to błąd, a tym bardziej znany błąd.
Aby odpowiedzieć na pytanie tytułowe: Jak utworzyć nieskończoną pustą pętlę, która nie będzie zoptymalizowana? ? -
utworzyć die()
makro, a nie funkcję , aby obejść ten błąd w Clang 3.9 i nowszych. (Wcześniejsze wersje Clanga albo utrzymywały pętlę, albo wysyłały „a”call
do nieliniowej wersji funkcji z nieskończoną pętlą.) Wydaje się to bezpieczne, nawet jeśli print;while(1);print;
funkcja włącza się w jej wywołującym ( Godbolt ). -std=gnu11
vs. -std=gnu99
nic nie zmienia.
Jeśli zależy ci tylko na GNU C, P__J ____asm__("");
wewnątrz pętli również działa i nie powinno zaszkodzić optymalizacji żadnego otaczającego kodu dla żadnego kompilatora, który go rozumie. Podstawowe instrukcje asm GNU C Basic są domyślnievolatile
, więc liczy się to jako widoczny efekt uboczny, który musi „wykonać” tyle razy, ile by to miało miejsce w maszynie abstrakcyjnej C. (I tak, Clang implementuje dialekt GNU języka C, jak udokumentowano w instrukcji GCC.)
Niektórzy twierdzą, że optymalizacja pustej nieskończonej pętli może być legalna. Nie zgadzam się 1 , ale nawet jeśli to zaakceptujemy, Clang nie może również legalnie zakładać, że instrukcje po osiągnięciu pętli są nieosiągalne, i pozwolić , aby wykonanie spadło z końca funkcji do następnej lub do śmieci który dekoduje jako losowe instrukcje.
(Byłoby to zgodne ze standardami dla Clang ++ (ale nadal niezbyt przydatne); nieskończone pętle bez żadnych skutków ubocznych są UB w C ++, ale nie C.
Jest while (1); niezdefiniowane zachowanie w C? UB pozwala kompilatorowi emitować praktycznie wszystko dla kodu na ścieżce wykonania, która na pewno napotka UB. asm
Instrukcja w pętli unikałaby tego UB dla C ++. Ale w praktyce Clang kompiluje jako C ++ nie usuwa nieskończonych pustych pętli o stałym wyrażeniu, z wyjątkiem wstawiania, tak jak wtedy, gdy kompilacja jako C.)
Ręczne wstawianie while(1);
zmienia sposób kompilacji Clanga: nieskończona pętla obecna w asm. Tego oczekujemy od prawnika POV.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
W eksploratorze kompilatorów Godbolt Clang 9.0 -O3 kompiluje się jako C ( -xc
) dla x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
Ten sam kompilator z tymi samymi opcjami kompiluje najpierw ten , main
który wywołuje infloop() { while(1); }
ten sam puts
, ale potem przestaje wydawać instrukcje dla main
tego punktu. Tak jak powiedziałem, wykonanie po prostu spada z końca funkcji, do dowolnej funkcji, która będzie następna (ale przy stosie przesuniętym względem wejścia funkcji, więc nie jest to nawet poprawne wywołanie ogona).
Prawidłowe opcje to
- emitują
label: jmp label
nieskończoną pętlę
- lub (jeśli zaakceptujemy, że nieskończona pętla może zostać usunięta) wysyłamy kolejne wywołanie, aby wydrukować 2. ciąg, a następnie
return 0
z main
.
Awaria lub kontynuacja bez drukowania „nieosiągalnego” najwyraźniej nie jest odpowiednia dla implementacji C11, chyba że istnieje UB, którego nie zauważyłem.
Przypis 1:
Dla przypomnienia, zgadzam się z odpowiedzią @ Lundin, która przytacza standard dla dowodów, że C11 nie pozwala na założenie zakończenia dla nieskończonych pętli o stałym wyrażeniu, nawet gdy są one puste (brak we / wy, niestabilność, synchronizacja lub inne widoczne efekty uboczne).
Jest to zestaw warunków, które pozwoliłyby skompilować pętlę do pustej pętli asm dla normalnego procesora. (Nawet jeśli treść nie była pusta w źródle, przypisania do zmiennych nie mogą być widoczne dla innych wątków lub procedur obsługi sygnałów bez UB wyścigu danych, gdy pętla jest uruchomiona. Zatem zgodna implementacja mogłaby usunąć takie treści pętli, gdyby chciała To pozostawia pytanie, czy samą pętlę można usunąć. ISO C11 wyraźnie mówi nie.)
Biorąc pod uwagę, że C11 wyróżnia ten przypadek jako przypadek, w którym implementacja nie może zakładać, że pętla się kończy (i że nie jest to UB), wydaje się jasne, że zamierzają być obecni w pętli w czasie wykonywania. Implementacja ukierunkowana na procesory z modelem wykonania, który nie może wykonać nieskończonej ilości pracy w skończonym czasie, nie ma uzasadnienia dla usuwania pustej stałej pętli nieskończonej. Lub nawet ogólnie, dokładne sformułowanie dotyczy tego, czy można je „założyć, że wygasną”, czy nie. Jeśli pętla nie może się zakończyć, oznacza to, że późniejszy kod jest nieosiągalny, bez względu na argumenty dotyczące matematyki i nieskończoności oraz czas potrzebny na wykonanie nieskończonej ilości pracy na jakiejś hipotetycznej maszynie.
Co więcej, Clang nie jest jedynie DeathStation 9000 zgodnym z ISO C, ale ma być przydatny do programowania systemów niskopoziomowych w świecie rzeczywistym, w tym do jąder i osadzonych elementów. Niezależnie od tego, czy akceptujesz argumenty dotyczące C11 zezwalające na usunięcie while(1);
, nie ma sensu, aby Clang chciał to zrobić. Jeśli napiszesz while(1);
, to prawdopodobnie nie był wypadek. Usuwanie pętli, które przypadkowo kończą się nieskończonością (z wyrażeniami kontrolnymi zmiennych wykonawczych), może być przydatne i ma to sens dla kompilatorów.
Rzadko zdarza się, że chcesz się kręcić do następnego przerwania, ale jeśli napiszesz to w C, to na pewno tego się spodziewasz. (I co dzieje się w GCC i Clang, z wyjątkiem Clanga, gdy nieskończona pętla znajduje się w funkcji otoki).
Na przykład w prymitywnym jądrze systemu operacyjnego, gdy program planujący nie ma zadań do uruchomienia, może uruchomić zadanie bezczynne. Pierwszą implementacją tego może być while(1);
.
Lub w przypadku sprzętu bez funkcji bezczynności oszczędzania energii, może to być jedyna implementacja. (Do wczesnych lat 2000., tak myślę, nie było to rzadkie na x86. Chociaż hlt
instrukcja istniała, IDK jeśli oszczędzał znaczącą ilość energii, dopóki procesory nie zaczęły mieć stanów bezczynności o niskiej mocy).