Czy to naprawdę dobra praktyka wyłączania optymalizacji podczas fazy programowania i debugowania?


15

Przeczytałem Programowanie 16-bitowych mikrokontrolerów PIC w C i taka książka znajduje się w książce:

Jednak podczas fazy projektowania i debugowania projektu zawsze dobrą praktyką jest wyłączanie wszystkich optymalizacji, ponieważ mogą one modyfikować strukturę analizowanego kodu i powodować problemy z wprowadzaniem pojedynczych kroków i umieszczaniem punktu przerwania.

Przyznaję, że byłem trochę zdezorientowany. Nie rozumiałem, czy autor powiedział to z powodu okresu oceny C30, czy też jest to naprawdę dobra praktyka.

Chciałbym wiedzieć, czy faktycznie korzystasz z tej praktyki i dlaczego?

Odpowiedzi:


16

Jest to dość standard w inżynierii oprogramowania jako całości - podczas optymalizacji kodu kompilator może zmieniać aranżację elementów w dowolny sposób, pod warunkiem, że nie zauważysz żadnej różnicy w działaniu. Na przykład, jeśli inicjujesz zmienną w każdej iteracji pętli i nigdy nie zmieniasz zmiennej w pętli, optymalizator może przenieść tę inicjalizację poza pętlę, abyś nie marnował z nią czasu.

Może również zdawać sobie sprawę, że obliczasz liczbę, z którą nie robisz nic przed nadpisaniem. W takim przypadku może to wyeliminować niepotrzebne obliczenia.

Problem z optymalizacją polega na tym, że będziesz chciał umieścić punkt przerwania na jakimś fragmencie kodu, który optymalizator przesunął lub wyeliminował. W takim przypadku debugger nie może zrobić tego, co chcesz (generalnie umieści punkt przerwania gdzieś blisko). Aby wygenerowany kod był bardziej podobny do tego, co napisałeś, wyłączasz optymalizacje podczas debugowania - gwarantuje to, że kod, który chcesz złamać, naprawdę istnieje.

Musisz jednak zachować ostrożność, ponieważ w zależności od kodu optymalizacja może popsuć! Zasadniczo kod, który jest łamany przez poprawnie działający optymalizator, to po prostu błędny kod, który coś uchodzi na sucho, więc zwykle chcesz dowiedzieć się, dlaczego optymalizator go psuje.


5
Kontrapunktem dla tego argumentu jest to, że optymalizator prawdopodobnie zmniejszy i / lub przyspieszy sprawę, a jeśli masz kod ograniczający czas lub rozmiar, możesz coś zepsuć, wyłączając optymalizację i marnować czas na debugowanie problemu, który nie powoduje naprawdę istnieją. Oczywiście debugger może spowolnić i powiększyć kod.
Kevin Vermeer

Gdzie mogę dowiedzieć się więcej na ten temat?
Daniel Grillo,

Nie pracowałem z kompilatorem C30, ale dla kompilatora C18 istniała notka / instrukcja aplikacji dla kompilatora, która obejmowała obsługiwane przez niego optymalizacje.
Mark,

@O Engenheiro: Sprawdź w dokumentach kompilatora, jakie optymalizacje obsługuje. Optymalizacja różni się bardzo w zależności od kompilatora, bibliotek i architektury docelowej.
Michael Kohne,

Ponownie, nie dla kompilatora C30, ale gcc publikuje LONG listę różnych optymalizacji, które można zastosować. Możesz także użyć tej listy, aby uzyskać szczegółową optymalizację, na wypadek, gdybyś miał określoną strukturę sterowania, którą chcesz zachować. Lista jest tutaj: gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
Kevin Vermeer

7

Wysłałem to pytanie do Jacka Ganssle'a, na co odpowiedział mi:

Daniel,

Wolę debugować przy użyciu wszelkich optymalizacji zawartych w wydanym kodzie. NASA mówi: „przetestuj, co latasz, a co przetestuj”. Innymi słowy, nie rób testów, a następnie zmieniaj kod!

Czasami jednak trzeba wyłączyć optymalizacje, aby debugger działał. Próbuję je wyłączyć tylko w modułach, nad którymi pracuję. Z tego powodu wierzę w utrzymywanie małych plików, powiedzmy kilkaset wierszy kodu.

Wszystkiego najlepszego, Jack


Myślę, że w tej odpowiedzi istnieje nieokreślone rozróżnienie między dwoma akapitami. Testowanie odnosi się do procedury, która ma wykazać, że miękkie, twarde i / lub twarde wyroby działają poprawnie. Debugowanie to proces, w którym kod przechodzi krok po kroku przez instrukcję, aby zobaczyć, dlaczego nie działa [jeszcze].
Kevin Vermeer

Miło mieć wybór. Testy mogą więc obejmować więcej odmian / permutacji z optymalizacją i bez.

@reemrevnivek, kiedy debugujesz, czy też nie testujesz?
Daniel Grillo,

@O Engenheiro - Nie. Debuguję tylko, jeśli test się nie powiedzie.
Kevin Vermeer,

6

Zależy, i to ogólnie dotyczy wszystkich narzędzi, nie tylko C30.

Optymalizacje często usuwają i / lub zmieniają kod na różne sposoby. Instrukcja switch może zostać ponownie zaimplementowana za pomocą konstrukcji if / else lub w niektórych przypadkach może zostać usunięta razem. y = x * 16 może zostać zastąpione szeregiem przesunięć w lewo itp., chociaż ten ostatni rodzaj optymalizacji można zwykle przeforsować, przeważnie jest to zmiana instrukcji sterującej.

Może to uniemożliwić przejście debuggera przez kod C, ponieważ struktury zdefiniowane w C już nie istnieją, zostały zastąpione lub ponownie uporządkowane przez kompilator w coś, co zdaniem kompilatora będzie szybsze lub zajmie mniej miejsca. Może również uniemożliwić ustawienie punktów przerwania z listy C, ponieważ instrukcja, na której się łamałeś, może już nie istnieć. Na przykład możesz spróbować ustawić punkt przerwania w instrukcji if, ale kompilator mógł go usunąć. Możesz spróbować ustawić punkt przerwania w pętli forsa lub for, ale kompilator postanowił rozwinąć tę pętlę, aby już nie istniała.

Z tego powodu, jeśli możesz debugować przy wyłączonych optymalizacjach, zwykle jest to łatwiejsze. Zawsze powinieneś ponownie testować z włączonymi optymalizacjami. Jest to jedyny sposób, w jaki dowiesz się, że przegapiłeś coś ważnego volatilei powoduje to sporadyczne awarie (lub inną dziwność).

W przypadku programowania wbudowanego należy zachować ostrożność przy optymalizacji. W szczególności w częściach kodu, które mają krytyczne znaczenie dla czasu, na przykład niektóre przerywają. W takich przypadkach należy albo zakodować krytyczne bity w zestawie, albo skorzystać z dyrektyw kompilatora, aby upewnić się, że te sekcje nie są zoptymalizowane, aby wiedzieć, że mają one ustalony czas wykonania lub ustalony najgorszy czas działania.

Innym gotcha może być dopasowanie kodu do sterownika, możesz potrzebować optymalizacji gęstości kodu, aby po prostu dopasować kod do układu. Jest to jeden z powodów, dla których zwykle dobrym pomysłem jest zacząć od największej pojemności pamięci ROM uC i wybrać tylko mniejszą do produkcji, po zablokowaniu kodu.


5

Generalnie debugowałbym przy użyciu ustawień, które planowałem wydać. Gdybym zamierzał wydać zoptymalizowany kod, debugowałbym przy pomocy zoptymalizowanego kodu. Gdybym zamierzał wydać niezoptymalizowany kod, debugowałbym z niezoptymalizowanym kodem. Robię to z dwóch powodów. Po pierwsze, optymalizatory mogą wprowadzić znaczne różnice czasowe, aby produkt końcowy zachowywał się inaczej niż niezoptymalizowany kod. Po drugie, mimo że większość z nich jest całkiem niezła, producenci kompilatorów popełniają błędy, a zoptymalizowany kod może dawać różne wyniki od niezoptymalizowanego kodu. W rezultacie lubię mieć jak najwięcej czasu na testowanie przy dowolnym ustawieniu, które zamierzam wydać.

Biorąc to pod uwagę, optymalizatory mogą utrudniać debugowanie, jak zauważono w poprzednich odpowiedziach. Jeśli znajdę określoną sekcję kodu, która jest trudna do debugowania, tymczasowo wyłączę optymalizator, przeprowadzę debugowanie, aby kod działał, a następnie ponownie go uruchomię i przetestuję.


1
Jednak pojedynczy krok kodu może być prawie niemożliwy przy włączonych optymalizacjach. Debuguj z wyłączonymi optymalizacjami i uruchom testy jednostkowe z kodem wersji.
Rocketmagnet

3

Moją normalną strategią jest rozwijanie z ostateczną optymalizacją (maksimum dla rozmiaru lub prędkości odpowiednio), ale tymczasowo wyłącz optymalizację, jeśli muszę coś debugować lub śledzić. Zmniejsza to ryzyko pojawienia się błędów w wyniku zmiany poziomów optymalizacji.

Typowym trybem awarii jest sytuacja, w której zwiększenie optymalizacji powoduje pojawienie się na powierzchni wcześniej niewidocznych błędów z powodu braku zadeklarowania zmiennych jako zmiennych w razie potrzeby - jest to niezbędne, aby poinformować kompilator, które rzeczy nie powinny być „optymalizowane”.


2

Używaj dowolnej formy, którą zamierzasz wydać, debuggerów i kompilacji do debugowania ukryj wiele (DUŻO) błędów, których nie widzisz, dopóki nie skompilujesz do wydania. Do tego czasu znacznie trudniej jest znaleźć te błędy, a nie debugowanie w trakcie pracy. 20 lat temu i nigdy nie miałem zastosowania do gdb lub innego typu debuggera, nie trzeba oglądać zmiennych ani jednego kroku. Setki do tysięcy linii dziennie. Więc jest to możliwe, nie daj się myśleć inaczej.

Kompilacja do debugowania, a następnie kompilacja do wydania może i zajmie dwa razy więcej niż dwa razy więcej wysiłku. Jeśli dostaniesz się do wiązania i będziesz musiał użyć narzędzia takiego jak debugger, to skompiluj, aby debugger przeszedł przez konkretny problem, a następnie powróć do normalnej pracy.

Inne problemy są również prawdziwe, ponieważ optymalizator przyspiesza kod, więc w szczególności w przypadku osadzania zmian czasu z opcjami kompilatora i które mogą wpływać na funkcjonalność programu, ponownie skorzystaj z opcji kompilacji dostarczanej podczas całej fazy. Kompilatory są również programami i zawierają błędy, a optymalizatory popełniają błędy, a niektóre nie wierzą w to. Jeśli tak jest, nie ma nic złego w kompilacji bez optymalizacji, po prostu rób to cały czas. Preferowaną przeze mnie ścieżką jest kompilacja w celu optymalizacji, a jeśli podejrzewam problem z kompilatorem, wyłącz optymalizację, jeśli to naprawia, zwykle w obie strony czasami analizuje dane wyjściowe asemblera, aby dowiedzieć się, dlaczego.


1
+1 tylko po to, by rozwinąć twoją dobrą odpowiedź: często kompilacja w trybie „debugowania” spowoduje wypełnienie stosu / sterty wokół zmiennych nieprzydzielonym miejscem, aby złagodzić niewielkie błędy zapisu i formatowania. Częściej dostaniesz dobrą awarię środowiska wykonawczego, jeśli skompilujesz w wydaniu.
Morten Jensen

2

Zawsze rozwijam kod z -O0 (opcja gcc, aby wyłączyć optymalizację). Kiedy czuję, że jestem w punkcie, w którym chcę zacząć pozwalać, by sprawy zmierzały w kierunku wydania, zacznę od -Os (optymalizacja pod kątem rozmiaru), ponieważ im więcej kodu możesz przechowywać w pamięci podręcznej, tym lepiej, nawet jeśli nie jest zoptymalizowany pod kątem superduperowania.

Uważam, że gdb działa znacznie lepiej z kodem -O0 i jest o wiele łatwiejsze do naśladowania, jeśli musisz wejść do zestawu. Przełączanie między opcjami -O0 i -Os pozwala również zobaczyć, co kompilator robi z twoim kodem. Czasami jest to dość interesująca edukacja, a także może wykrywać błędy kompilatora ... te paskudne rzeczy, które zmuszają cię do wyciągania włosów, próbując dowiedzieć się, co jest nie tak z twoim kodem!

Jeśli naprawdę muszę, zacznę dodawać sekcje -fdata-section i -fcode-section z sekcjami --gc, które pozwalają linkerowi usunąć całe funkcje i segmenty danych, które nie są faktycznie używane. Jest wiele drobiazgów, z którymi możesz majstrować, aby spróbować je zmniejszyć lub przyspieszyć, ale ogólnie rzecz biorąc, są to jedyne sztuczki, których używam, i wszystko, co musi być mniejsze lub szybsze, podam -montować.


2

Tak, wyłączanie optymalizacji podczas debugowania jest od dłuższego czasu najlepszą praktyką z trzech powodów:

  • (a) jeśli zamierzasz wykonać jednoetapowy program za pomocą debugera wysokiego poziomu, jest to nieco mniej mylące.
  • (a) (przestarzałe) jeśli zamierzasz debugować program w jednym kroku za pomocą debugera w języku asemblera, jest to mniej skomplikowane. (Ale dlaczego miałbyś się tym przejmować, skoro możesz używać debugera wysokiego poziomu?)
  • (b) (dawno przestarzały) prawdopodobnie uruchomisz ten konkretny plik wykonywalny tylko raz, a następnie wprowadzisz zmiany i ponownie skompilujesz. To marnowanie czasu na czekanie dodatkowych 10 minut, podczas gdy kompilator „optymalizuje” ten konkretny plik wykonywalny, gdy pozwoli to zaoszczędzić mniej niż 10 minut czasu wykonywania. (Nie ma to już znaczenia w przypadku nowoczesnych komputerów PC, które potrafią skompilować typowy plik wykonywalny mikrokontrolera, z pełną optymalizacją, w mniej niż 2 sekundy).

Wiele osób idzie jeszcze dalej w tym kierunku i wysyła z włączonymi twierdzeniami .


Jednoczesne przechodzenie przez kod asemblera może być bardzo przydatne do diagnozowania przypadków, w których kod źródłowy faktycznie określa coś innego niż to, jak wygląda (np. „Longvar1 & = ~ 0x40000000; longvar2 & = ~ 0x80000000;”) lub gdzie kompilator generuje błędny kod . Wyśledziłem niektóre problemy za pomocą debugerów kodu maszynowego, których tak naprawdę nie sądzę, żebym mógł wyśledzić w jakikolwiek inny sposób.
supercat

2

Proste: optymalizacje są czasochłonne i mogą być bezużyteczne, jeśli trzeba później zmienić ten fragment kodu. Mogą więc być stratą czasu i pieniędzy.
Są one jednak przydatne w gotowych modułach; części kodu, które najprawdopodobniej nie będą już wymagać zmian.


2

Z pewnością ma to sens w przypadku punktów przerwania ... ponieważ kompilator może usunąć wiele instrukcji, które nie wpływają na pamięć.

rozważ coś takiego:

int i =0;

for (int j=0; j < 10; j++)
{
 i+=j;
}
return 0;

może być całkowicie zoptymalizowany (ponieważ inigdy nie jest czytany). z punktu widzenia punktu przerwania wyglądałoby na to, że pominął cały ten kod, gdy w zasadzie go po prostu nie było ... Przypuszczam, że dlatego w funkcjach typu uśpienia często można zobaczyć coś takiego:

for (int j=delay; j != 0; j--)
{
    asm( " nop " );
    asm( " nop " );
}
return 0;

1

Jeśli używasz debugera, wyłączę optymalizacje i włączę debugowanie.

Osobiście uważam, że debugger PIC powoduje więcej problemów niż pomaga mi to naprawić.
Po prostu używam printf () do USART do debugowania moich programów napisanych w C18.


1

Większość argumentów przeciwko włączeniu optymalizacji w kompilacji sprowadza się do:

  1. problemy z debugowaniem (łączność JTAG, punkty przerwania itp.)
  2. niewłaściwe taktowanie oprogramowania
  3. sh * t przestaje działać poprawnie

IMHO pierwsze dwa są legalne, trzeci nie tyle. Często oznacza to, że masz zły kod lub polegasz na niebezpiecznym wykorzystaniu języka / implementacji, a może autor jest po prostu fanem dobrego starego Uncle Undefined Behavior.

Blog Embedded in Academia ma coś do powiedzenia na temat nieokreślonego zachowania, a ten post dotyczy wykorzystania kompilatorów: http://blog.regehr.org/archives/761


Innym możliwym powodem jest to, że kompilator może być denerwująco wolny po włączeniu optymalizacji.
supercat
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.