Skąd mam wiedzieć, czy kompilator złamał mój kod i co zrobić, jeśli był to kompilator?

14

Raz na jakiś czas kod C ++ nie będzie działał, gdy zostanie skompilowany z pewnym poziomem optymalizacji. Może to być kompilator dokonujący optymalizacji, który łamie kod, lub może to być kod zawierający niezdefiniowane zachowanie, które pozwala kompilatorowi robić to, co czuje.

Załóżmy, że mam fragment kodu, który się psuje, gdy jest kompilowany tylko z wyższym poziomem optymalizacji. Skąd mam wiedzieć, czy to kod, czy kompilator i co mam zrobić, jeśli jest to kompilator?

sharptooth
źródło
43
Najprawdopodobniej to ty.
littleadv
9
@littleadv, nawet najnowsze wersje gcc i msvc są pełne błędów, więc nie byłbym tego taki pewien.
SK-logic,
3
Czy masz włączone wszystkie ostrzeżenia?
@ Thorbjørn Ravn Andersen: Tak, mam je włączone.
sharptooth
3
FWIW: 1) Staram się nie robić nic trudnego, co mogłoby kusić kompilator do zepsucia się, 2) jedynym miejscem znaczącym dla flag optymalizacji (dla prędkości) jest kod, w którym licznik programu spędza znaczną część swojego czasu. O ile nie piszesz ciasnych pętli procesora, w wielu aplikacjach komputer zasadniczo spędza cały czas głęboko w bibliotekach lub we / wy. W tego rodzaju aplikacjach przełączniki / O wcale nie pomagają.
Mike Dunlavey

Odpowiedzi:

19

Powiedziałbym, że jest to bezpieczny zakład, że w zdecydowanej większości przypadków uszkodzony jest twój kod, a nie kompilator. I nawet w wyjątkowym przypadku, gdy jest to kompilator, prawdopodobnie używasz jakiejś niejasnej funkcji językowej w niecodzienny sposób, dla której ten kompilator nie jest przygotowany; innymi słowy, najprawdopodobniej możesz zmienić kod na bardziej idiomatyczny i uniknąć słabego punktu kompilatora.

W każdym razie, jeśli możesz udowodnić , że znalazłeś błąd kompilatora (na podstawie specyfikacji języka), zgłoś go twórcom kompilatora, aby mogli go naprawić przez jakiś czas.

Péter Török
źródło
@ SK-logic, dość uczciwie, nie mam statystyk, aby to poprzeć. Opiera się na własnych doświadczeniach i przyznaję, że rzadko przekraczałem granice języka i / lub kompilatora - inni mogą to robić częściej.
Péter Török
(1) @ SK-Logic: Właśnie znalazłem błąd kompilatora C ++, ten sam kod, wypróbowałem na jednym kompilatorze i działa, wypróbowałem w innym, który popsuł.
umlcat
8
@umlcat: najprawdopodobniej był to Twój kod w zależności od nieokreślonego zachowania; na jednym kompilatorze spełnia twoje oczekiwania, na innym nie. to nie znaczy, że jest zepsute.
Javier
@Ritch Melton, czy kiedykolwiek używałeś LTO?
SK-logic
1
Zgadzam się z Crashworks, jeśli chodzi o konsole do gier. W tej konkretnej sytuacji nie jest niczym niezwykłym znalezienie ezoterycznych błędów kompilatora. Jeśli jednak celujesz w normalne komputery PC, używając mocno używanego kompilatora, jest bardzo mało prawdopodobne, że wpadniesz na błąd kompilatora, którego nikt wcześniej nie widział.
Trevor Powell
14

Tak jak zwykle, jak w przypadku innych błędów: wykonaj kontrolowany eksperyment. Zawęź podejrzany obszar, wyłącz optymalizacje dla wszystkiego innego i zacznij zmieniać optymalizacje zastosowane do tego fragmentu kodu. Gdy uzyskasz 100% odtwarzalność, zacznij zmieniać kod, wprowadzając rzeczy, które mogą złamać pewne optymalizacje (np. Wprowadzić możliwe aliasing wskaźnika, wstawić połączenia zewnętrzne z potencjalnymi skutkami ubocznymi itp.). Pomocne może być również sprawdzenie kodu asemblera w debuggerze.

Logika SK
źródło
może pomóc w czym? Jeśli jest to błąd kompilatora - co z tego?
littleadv
2
@littleadv, jeśli jest to błąd kompilatora, możesz spróbować go naprawić (lub po prostu odpowiednio go zgłosić, szczegółowo), lub możesz dowiedzieć się, jak go uniknąć w przyszłości, jeśli jesteś skazany na dalsze używanie tego wersja twojego kompilatora na chwilę. Jeśli jest to coś z twoim kodem, jednym z licznych problemów z granicami C ++, ten rodzaj kontroli pomaga również w naprawie błędu i uniknięciu tego rodzaju w przyszłości.
SK-logic,
Tak więc, jak powiedziałem w mojej odpowiedzi - poza zgłaszaniem, nie ma dużej różnicy w traktowaniu, niezależnie od tego, czyja to wina.
littleadv
3
@littleadv, nie rozumiejąc natury problemu, prawdopodobnie napotkasz go wielokrotnie. I często istnieje możliwość samodzielnego naprawienia kompilatora. I tak, nie jest wcale „mało prawdopodobne” znalezienie błędu w kompilatorze C ++, niestety.
SK-logic,
10

Sprawdź wynikowy kod asemblera i sprawdź, czy działa on zgodnie z żądaniami twojego źródła. Pamiętaj, że szanse są bardzo duże, że to naprawdę twój kod zawinił w nieoczywisty sposób.

Loren Pechtel
źródło
1
To naprawdę jedyna odpowiedź na to pytanie. W tym przypadku zadaniem kompilatorów jest przejście od C ++ do języka asemblera. Myślisz, że to jest kompilator ... sprawdź działanie kompilatorów. To takie proste.
old_timer
7

W ciągu ponad 30 lat programowania liczba znalezionych przeze mnie błędów oryginalnego kompilatora (generowania kodu) wynosiła tylko ~ 10. Liczba błędów własnych (i innych osób), które znalazłem i naprawiłem w tym samym okresie, jest prawdopodobnie > 10 000. Moja „ogólna zasada” polega zatem na tym, że prawdopodobieństwo wystąpienia dowolnego błędu wynikającego z kompilatora wynosi <0,001.

Paul R.
źródło
1
Jesteś szczęściarzem. Moja średnia to około 1 naprawdę zły błąd miesięcznie, a drobne problemy z pogranicza zdarzają się znacznie częściej. Im wyższy poziom optymalizacji, którego używasz, tym większe są szanse na błędy kompilatora. Jeśli próbujesz użyć -O3 i LTO, będziesz miał szczęście, że nie znajdziesz kilku z nich w krótkim czasie. I tutaj liczę tylko błędy w wersjach wydanych - jako twórca kompilatorów napotykam o wiele więcej tego rodzaju problemów w mojej pracy, ale to się nie liczy. Wiem tylko, jak łatwo jest spieprzyć kompilator.
SK-logic
2
25 lat i widziałem też bardzo wiele. Kompilatory pogarszają się z każdym rokiem.
old_timer
5

Zacząłem pisać komentarz, a potem zdecydowałem, że jest za długi i za bardzo do rzeczy.

Twierdziłbym, że to twój kod jest uszkodzony. W mało prawdopodobnym przypadku, gdy odkryłeś błąd w kompilatorze - powinieneś zgłosić go twórcom kompilatora, ale na tym kończy się różnica.

Rozwiązaniem jest zidentyfikowanie szkodliwego konstruktu i przefaktoryzowanie go w taki sposób, aby działał w ten sam sposób inaczej. To najprawdopodobniej rozwiązałoby problem, niezależnie od tego, czy błąd jest po twojej stronie, czy w kompilatorze.

littleadv
źródło
5
  1. Ponownie przeczytaj swój kod. Upewnij się, że nie robisz rzeczy z efektami ubocznymi w ASSERT lub innych instrukcjach specyficznych dla debugowania (lub, bardziej ogólnie, konfiguracji). Pamiętaj również, że w debugowaniu kompilacja pamięci jest inicjalizowana w różny sposób - znaczące wartości wskaźnika można sprawdzić tutaj: Debugowanie - reprezentacje alokacji pamięci . Podczas uruchamiania z poziomu Visual Studio prawie zawsze używasz stosu debugowania (nawet w trybie wydania), chyba że za pomocą zmiennej środowiskowej wyraźnie określisz, że nie jest to pożądane.
  2. Sprawdź swoją wersję. Często zdarzają się problemy ze złożonymi kompilacjami w innych miejscach niż rzeczywisty kompilator - przyczyną często są zależności. Wiem, że „czy próbowałeś całkowicie przebudować” jest prawie tak irytującą odpowiedzią, jak „czy próbowałeś ponownie zainstalować system Windows”, ale często pomaga. Spróbuj: a) Ponowne uruchomienie. b) RĘCZNIE usuwając wszystkie pliki pośrednie i wyjściowe oraz odbudowując.
  3. Przejrzyj swój kod, aby sprawdzić potencjalne lokalizacje, w których możesz wywoływać niezdefiniowane zachowanie. Jeśli pracujesz przez jakiś czas w C ++, będziesz wiedział, że są miejsca, w których myślisz: „Nie jestem CAŁKOWICIE pewien, że wolno mi zakładać, że ...” - przejrzyj go lub zapytaj tutaj o ten konkretny typ kodu, aby zobaczyć, czy jest to niezdefiniowane zachowanie, czy nie.
  4. Jeśli nadal tak nie jest, wygeneruj wstępnie przetworzone dane wyjściowe dla pliku, który powoduje problemy. Nieoczekiwana ekspansja makr może powodować wiele radości (przypomina mi się, że kolega zdecydował, że makro o nazwie H będzie dobrym pomysłem ...). Sprawdź wstępnie przetworzone dane wyjściowe pod kątem nieoczekiwanych zmian między konfiguracjami projektu.
  5. W ostateczności - teraz naprawdę jesteś w krainie błędów kompilatora - spójrz na dane wyjściowe zestawu. Może to zająć trochę kopania i walki, aby zrozumieć, co właściwie robi zgromadzenie, ale w rzeczywistości jest dość pouczające. Możesz wykorzystać nabyte tutaj umiejętności do oceny mikrooptymalizacji, aby nie stracić wszystkiego.
Joris Timmermans
źródło
+1 za „niezdefiniowane zachowanie”. Ukąsił mnie ten. Napisałem trochę kodu, który zależał od int + intprzepełnienia dokładnie tak, jakby skompilował się do instrukcji sprzętowej ADD. Działa dobrze po kompilacji ze starszą wersją GCC, ale nie po kompilacji z nowszym kompilatorem. Najwyraźniej mili ludzie z GCC zdecydowali, że skoro wynik przepełnienia liczb całkowitych jest nieokreślony, ich optymalizator może działać przy założeniu, że to się nigdy nie zdarza. Zoptymalizował ważną gałąź bezpośrednio z kodu.
Solomon Slow
2

Jeśli chcesz wiedzieć, czy jest to twój kod, czy kompilator, musisz doskonale znać specyfikację C ++.

Jeśli wątpliwości się utrzymują, musisz doskonale znać zestawienie x86.

Jeśli nie masz ochoty uczyć się zarówno do perfekcji, to prawie na pewno jest to nieokreślone zachowanie, które kompilator rozwiązuje inaczej w zależności od poziomu optymalizacji.

mouviciel
źródło
(+1) @mouviciel: Zależy to również od tego, czy funkcja jest obsługiwana przez kompilator, a nawet jeśli jest podana w specyfikacji. Mam dziwny błąd w gcc. Deklaruję „zwykłą strukturę c” z „wskaźnikiem funkcji”, który jest dozwolony w specyfikacji, ale działa w niektórych sytuacjach, a nie w innych.
umlcat
1

Otrzymanie błędu kompilacji standardowego kodu lub wewnętrznego błędu kompilacji jest bardziej prawdopodobne niż błędy optymalizatorów. Ale słyszałem o kompilatorach optymalizujących pętle niepoprawnie zapominających o niektórych skutkach ubocznych spowodowanych przez metodę.

Nie mam sugestii, jak się dowiedzieć, czy to ty, czy kompilator. Możesz wypróbować inny kompilator.

Pewnego dnia zastanawiałem się, czy to mój kod, czy nie i ktoś zasugerował mi valgrind. Spędziłem 5 lub 10 minut, aby uruchomić z nim mój program (myślę, valgrind --leak-check=yes myprog arg1 arg2że to zrobiłem, ale grałem z innymi opcjami) i od razu pokazało mi JEDNĄ linię uruchomioną w jednym konkretnym przypadku, który był problemem. Potem moja aplikacja działała płynnie, bez dziwnych awarii, błędów i dziwnego zachowania. valgrind lub inne podobne narzędzie to dobry sposób na sprawdzenie, czy jest to Twój kod.

Uwaga dodatkowa: Kiedyś zastanawiałem się, dlaczego wydajność mojej aplikacji jest do dupy. Okazało się, że wszystkie moje problemy z wydajnością były w jednej linii. Pisałem for(int i=0; i<strlen(sz); ++i) {. Sz było kilka MB. Z jakiegoś powodu kompilator działał strlen za każdym razem, nawet po optymalizacji. Jedna linia może być wielką sprawą. Od występów po awarie


źródło
1

Coraz częstszą sytuacją jest to, że kompilatory łamią kod napisany dla dialektów języka C, które wspierały zachowania nie wymagane przez standard, i pozwalały, by kod kierujący te dialekty był bardziej wydajny niż kod ściśle zgodny. W takim przypadku niesprawiedliwe byłoby opisanie jako „zepsuty” kod, który byłby w 100% wiarygodny w kompilatorach, które zaimplementowały dialekt docelowy, lub opisanie jako „zepsuty” kompilator, który przetwarza dialekt, który nie obsługuje wymaganej semantyki . Zamiast tego problemy wynikają po prostu z faktu, że język przetwarzany przez nowoczesne kompilatory z włączonymi optymalizacjami odbiega od dialektów, które były popularne (i nadal są przetwarzane przez wiele kompilatorów z wyłączonymi optymalizacjami lub przez niektóre nawet z włączonymi optymalizacjami).

Na przykład wiele kodu jest napisanych dla dialektów, które uznają za uzasadnione wiele wzorców aliasingu wskaźników, które nie są wymagane przez interpretację standardu przez gcc, i wykorzystuje takie wzorce, aby umożliwić proste tłumaczenie kodu, aby było bardziej czytelne i wydajne byłoby to możliwe przy interpretacji standardu C przez gcc. Taki kod może nie być zgodny z gcc, ale nie oznacza to, że jest uszkodzony. Po prostu polega na rozszerzeniach, które gcc obsługuje tylko przy wyłączonych optymalizacjach.

supercat
źródło
Cóż, na pewno nie ma nic złego w kodowaniu C standardowych rozszerzeń X + Y i Z, o ile daje to znaczące korzyści, wiesz, że to zrobiłeś i dokładnie to udokumentowałeś. Niestety, wszystkie trzy warunki zwykle nie są spełnione , dlatego można powiedzieć, że kod jest uszkodzony.
Deduplicator
@Deduplicator: Kompilatory C89 zostały promowane jako kompatybilne w górę ze swoimi poprzednikami, podobnie jak w przypadku C99 itp. Podczas gdy C89 nie nakłada żadnych wymagań na zachowania, które zostały wcześniej zdefiniowane na niektórych platformach, ale nie na innych, kompatybilność w górę sugerowałaby, że kompilatory C89 dla platformy, które uznały zdefiniowane zachowania, powinny nadal to robić; uzasadnienie promocji krótkich typów bez znaku na znaki sugerowałoby, że autorzy Standardu oczekiwali, że kompilatory będą zachowywać się w ten sposób, niezależnie od tego, czy Norma to nakazuje. Dalej ...
supercat
... ścisła interpretacja reguł aliasingu wyrzuciłaby przez okno kompatybilność w górę i uniemożliwiłaby wiele rodzajów kodu, ale kilka drobnych poprawek (np. identyfikacja niektórych wzorców, w których należy się spodziewać aliasingu krzyżowego, a zatem dopuszczalnych) rozwiązałoby oba problemy . Ogólnym celem tej reguły było uniknięcie wymagania od kompilatorów dokonywania „pesymistycznych” założeń aliasingowych, ale biorąc pod uwagę „zmiennoprzecinkowe x”, gdyby założyć, że „foo ((int *) i x)” może modyfikować x, nawet jeśli „foo” nie robi „nie pisać do żadnych wskaźników typu„ float * ”lub„ char * ”uznać za„ pesymistyczne ”lub„ oczywiste ”?
supercat
0

Wyodrębnij problematyczne miejsce i porównaj zaobserwowane zachowanie z tym, co powinno się stać zgodnie ze specyfikacją języka. Zdecydowanie nie jest to łatwe, ale właśnie to musisz zrobić, aby wiedzieć (i nie tylko zakładać ).

Prawdopodobnie nie byłbym tak drobiazgowy. Chciałbym raczej zapytać forum pomocy / listę mailingową producenta kompilatora. Jeśli to naprawdę błąd w kompilatorze, mogą go naprawić. Prawdopodobnie i tak byłby to mój kod. Na przykład specyfikacje języka dotyczące widoczności pamięci w wątkach mogą być sprzeczne z intuicją i mogą stać się widoczne tylko przy użyciu określonych flag optymalizacji na określonym sprzęcie (!). Niektóre zachowanie może być niezdefiniowane przez specyfikację, więc może działać z niektórymi kompilatorami / niektórymi flagami, a nie działać z innymi, itp.

Joonas Pulakka
źródło
0

Najprawdopodobniej twój kod ma pewne niezdefiniowane zachowanie (jak wyjaśnili inni, znacznie częściej masz błędy w kodzie niż w kompilatorze, nawet jeśli kompilatory C ++ są tak złożone, że mają błędy; nawet w specyfikacji C ++ występują błędy projektowe) . UB może być tutaj, nawet jeśli skompilowany plik wykonywalny działa (pech).

Powinieneś więc przeczytać blog Lattnera Co każdy programista C powinien wiedzieć o nieokreślonym zachowaniu (większość dotyczy również C ++ 11).

Valgrind narzędzie, a ostatnie -fsanitize= opcje oprzyrządowanie do GCC (lub Clang / LLVM ), powinny być również pomocne. I oczywiście włącz wszystkie ostrzeżenia:g++ -Wall -Wextra

Basile Starynkevitch
źródło