Projektowanie oprogramowania systemów KONFIGURACJE OPROGRAMOWANIA; DEBUGGING, PROFILOWANIE, NARZĘDZIA ZAPEWNIANIA JAKOŚCI
plan Konfiguracje budowania i uruchamiania oprogramowania Debugowanie Funkcje debuggerów Zdalne debugowanie Analiza post-mortem Narzędzia Narzędzia profilowania i optymalizacji kodu Hotspoty w kodzie Narzędzia Zapewnianie jakości Metryki pokrycia kodu Śledzenie problemów z alokacją pamięci Narzędzia
Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. DONALD KNUTH w skrócie: przedwczesna optymalizacja jest źródłem wszelkiego zła ;)
konfiguracje budowania i uruchamiania oprogramowania Tworząc nowy projekt większość współczesnych środowisk IDE stworzy automatycznie 2 konfiguracje: Debug i Release Zgodnie z nazwą, konfiguracja Debug służy do debugowania (odpluskwiania, uzdatniania) kodu (a w zasadzie większości prac wdrożeniowych), zaś konfiguracja Release jest tym, co dostarczamy klientowi Uwaga więc na subtelne bugi, które pojawiają się tylko w konfiguracji Release! Debugowanie takiego kodu jest boleśnie trudne, ale czasami nieuniknione Z tego względu większość nietrywialnych systemów opiera się nie tylko na debugowaniu ale również implementuje pewnego rodzaju logowanie wykonania
konfiguracje Debug Optymalizacje wprowadzane przez kompilator są wyłączone Wyłączone jest rozwijanie funkcji inline Działają makra i funkcje debugowania (np. assert - asercje) Symbole debugowania są obecne w kodzie Kod działa dużo wolniej g++ -g O0 source.cpp Release Optymalizacje są włączone, przez co debugowanie jest nieprzewidywalne: kod ma zmienioną kolejność, zmienne są wyoptymalizowane z pamięci do rejestrów, pomijane są ramki stosu Makra debugowania nie są zdefiniowane lub nie robią nic Symbole debugowania są usunięte (stripped) z kodu lub przeniesione do osobnych plików Kod działa z pełną prędkością g++ -O2 DNDEBUG source.cpp
asercje Warunki, które programista zakłada, że zawsze muszą być spełnione Wprowadzane poprzez użycie makra assert (C/C++) lub analogicznych funkcji/słów kluczowych w innych językach W konfiguracji Release asercje są usuwane W konfiguracji Debug asercje spowodują zatrzymanie programu, wyświetlenie komunikatu debugującego, wywołanie funkcji abort(), która spowoduje crash (wywalenie się) programu sygnałem SIGABRT (Unix), a następnie zapisanie zrzutu pamięci (core dump) dla debugera post-mortem Używaj asercji dla weryfikacji warunków wstępnych i końcowych (pre- and post-conditions), nie dla regularnej obsługi błędów!
przełączanie konfiguracji
debugowanie W dalekiej przeszłości komputerów lampowych częstym źródłem błędów były owady które osiedlał się wewnątrz urządzeń powodując ich przegrzewanie, stąd nazwa odpluskwianie (debugging) Debugger jest narzędziem dynamicznej analizy innych programów, używanym do znajdowania i identyfikowania błędów programistycznych Większość środowisk IDE zawiera zintegrowany debugger, albo swój własny, albo front-end do GDB (GNU Debugger)
typowe funkcje debuggera Wykonanie krokowe Program jest wykonywany linia-po-linii albo instrukcja-po-instrukcji, umożliwiając zbadanie efektów każdego wywołania Tryby: step over, step into, step out, continue to line Ustawianie pułapek (breakpoints) Wstrzymuje wykonanie programu na określonej linii kodu albo kiedy określony warunek jest spełniony Call stack (podgląd stosu) Pokazuje stos wywołań funkcji umożliwiając wizualizację Jak się znaleźliśmy w tym miejscu?
typowe funkcje debugera Zmienne lokalne/obserwowanie (locals/watches) Badanie wartości lokalnych zmiennych w danej chwili czasu lub śledzenie jak wartość określonej zmiennej zmienia się w czasie Modyfikacja wartości zmiennych w czasie wykonania programu Wątki (threads) Badanie stanu wątków w programie, wymuszanie przełączenia wątków, badanie stosu każdego wątku, diagnozowanie deadlocków Pamięć (memory) Badanie i zmiana zawartości przestrzeni adresowej programu
typowe funkcje debugera Deasemblacja (disassembly) Pokazuje kod programu jako mnemoniki asemblera Krokowe wykonanie kodu asemblera Użyteczne do znajdowania bardzo tajemniczych bugów Kod asemblera jest przeplatany z odpowiadającym mu kodem wysokopoziomowym fajny sposób nauki asemblera ;>
typowe funkcje debugera Zdalne debugowanie Zainstaluj debugging stub na zdalnej maszynie Podłącz swój lokalny debuger do zdalnego komputera za pomocą sieci lub portu szeregowego Debuguj zdalny program tak jakby działał na maszynie lokalnej Użyteczne dla: Debugowania programów niskopoziomowych, np. jądra systemu operacyjnego, sterowników urządzeń Debugowania programów działających w ograniczonych środowiskach, np. na telefonie komórkowym, systemach osadzonych Debugowania problemów związanych z interfejsem użytkownika, kiedy interakcja z UI debugera sprawia, że nie można powtórzyć błędu
typowe funkcje debugera Podłączanie debugera do działającego programu Użyteczne dla debugowania programów działających w specyficznych środowiskach, nad którymi nie mamy pełnej kontroli, np. usług systemu operacyjnego/demonów, obiektów COM, kodu działającego wewnątrz serwerów aplikacji Debugowanie bibliotek współdzielonych ładowanych przez inne programy Modyfikuj ścieżkę wykonania programu grzebiąc w jego wewnętrznym stanie, wykrywaj luki bezpieczeństwa, projektuj i szerz wirusy!
typowe funkcje debugera Debugowanie post-mortem (pośmiertne) Badanie stanu padniętej aplikacji w momencie jej wywalenia się Na Uniksie badanie zrzutu pamięci - pliku core Na Windows możliwe za pomocą biblioteki dbghelp.dll, program musi zawierać jawne wywołanie kodu zapisującego pliki minidump Zwykle samo spojrzenie na stos programu w momencie crashu (jaka instrukcja była wywołana i jaki był jej kontekst) wystarcza aby wyśledzić przyczynę wywalenia się Ta informacja jest bezcenna! Po prostu załaduj zrzut do debugera i uruchom go jak normalny kod Wymuś zrzut pamięci poprzez wywołanie funkcji abort() lub niespełnioną asercję
tipsy Debuger jest prawdopodobnie pojedynczą najważniejszą przyczyną dla używania środowiska IDE zamiast ręcznego systemu budowania i ogólnych edytorów tekstu Naucz się i używaj skrótów klawiszowych debugera w Twoim środowisku IDE; niesamowicie podniesie to Twoją produktywność Jedynym powodem olbrzymiego sukcesu pierwszych wersji MS Visual Studio było to, że jego wizualny debuger był rewelacyjny, jako że kompilator zawsze był przestarzały i niezgodny ze standardami Większość (prawie wszystkie?) środowiska IDE nie pochodzące z Microsoftu używają GDB dla debugowania kodu C/C++; ma on mnóstwo potężnych funkcji ale jest generalnie trudny w obsłudze (interfejs z linii poleceń)
narzędzia Microsoft Visual Studio zintegrowany debuger, prawdopodobnie najlepszy jaki istnieje ;> GDB standardowy debuger na Linuksie/Uniksach, używalny także na Windows interfejs wiersza poleceń (używaj front-end!) Eclipse przyjemny GUI do GDB, zbliża się użytecznością do MSVC; zawiera własny, bardzo dobry debuger do Javy; również użyteczny do zdalnego debugowania aplikacji Androida Xcode IDE firmy Apple wraz z debuggerem; działa na Makach i pozwala również na zdalne debugowanie aplikacji ios WinDbg Microsoftowy debuger dla zdalnego debugowania systemu operacyjnego (jądro, sterowniki) DDD (Data Display Debugger) front-end do GDB i kilku innych debugerów uniksowych, lepszy niż nic Kdbg, xxdbg - patrz wyżej^
profilowanie i optymalizacja kodu programu Profilowanie dynamiczna analiza programu w celu zmierzenia czasu wykonania, użycia pamięci, wydajności Najprostsze/ad hoc podejście użyj wprost wywołań funkcji pomiaru czasu do mierzenia czasu wykonania fragmentu kodu Podejście systematyczne użyj profilera aby wykonać różne pomiary, m.in..: Instrumentacja wprowadza niewidoczny dla programisty kod mierzący czas wywołań do kodu binarnego dla każdej lub wybranych instrukcji Próbkowanie użycie specyficznych funkcji sprzętu (procesora) w celu próbkowania stosu wywołań programu z określoną częstotliwością
hot spoty Właściwe użycie profilera pozwala zidentyfikować hot spoty w kodzie programu Hot spot jest to fragment kodu, który jest wywoływany często lub program spędza wykonując go większość czasu wykonania Wpływ kodu znajdującego się poza hot-spotami na czas wykonania programu jest marginalny; nie ma sensu tracić czasu na ręczne optymalizacje takiego kodu Optymalizacja kodu hot spotów może w sposób znaczący wpłynąć na wydajność programu Decyzja o przeprowadzaniu optymalizacji bez znajomości hot spotów jest bez sensu
profilowanie innych wielkości Profilera można również użyć do mierzenia: Zajętości pamięci poprzez użycie specjalnych funkcji zastępujących standardowe narzędzia alokacji lub instrumentację kodu Wydajność I/O poprzez użycie odpowiednich usług systemu operacyjnego (counters) lub instrumentację funkcji I/O Niektóre języki zawierają wbudowane wsparcie dla takich pomiarów np. C#, Java (pamięć)
wady profilerów Instrumentacja i/lub proces przeprowadzania pomiarów przeprowadzany przez profiler może zafałszowywać gromadzone dane pomiarowe Zbyt drobnoziarnista instumentacja spowoduje, że kod mierzący stanie się właściwym hot spotem Profilowanie w rzeczywistym świecie podlega zasadzie nieoznaczoności (w mniejszym stopniu dotyczy to próbkowania, w większym instrumentacji)!
narzędzia profilowania Microsoft Visual Studio wbudowany profiler używający próbkowania CPU lub intrumentacji, również profiler pamięci w środowisku.net Intel VTune rozbudowany profiler Intela z funkcjami optymalizacji kodu wielowątkowego (thread contention, deadlock detection) AMD CodeAnalyst podobny do VTune (free!) gprof GNU Profiler, opiera się na instrumentacji wspomaganej przez kompilator (gcc), używaj z gcov dla śledzenia pogrycia kodu Valgrid profiler wykrywający również wycieki pamięci i mierzący użycie pamięci (Unix)
narzędzia zapewnienia jakości Statyczna lub dynamiczna analiza kodu w celu wykrycia problemów poza tymi standardowo wykrywanymi przez kompilator Przykłady Wykrywanie wycieków pamięci w czasie wykonania Sprawdzanie pokrycia kodu, wykrywanie martwego kodu
wykrywanie wycieków pamięci Wycieki pamięci to powszechny problem w językach nie posiadających odśmiecacza (Garbage Collector) np. C/C++ każdy zaalokowany fragment pamięci musi być ręcznie zwolniony, inaczej jest bezpowrotnie tracony Długo działający program z wyciekami ostatecznie pożre całą pamięć wirtualną powodując spadek wydajności a potem wywalenie się Narzędzia wykrywania wycieków przeprowadzają instrumentację funkcji alokacji pamięci w celu śledzenia każdego fragmentu pamięci i określenia, które fragmenty wyciekły Wycieki są też możliwe w językach z odśmiecaczem, ale są trudniejsze do wywołania (i wykrycia) ;> Wycieki mogą dotyczyć nie tylko pamięci, ale również innych zasobów (uchwyty plików, gniazda sieciowe, )
narzędzia wykrywania i zapobiegania wyciekom Visual Leak Detector biblioteka dla Visual C++ zastępująca standardowe mechanizmy alokacji pamięci Valgrind narzędzie wykrywania wycieków i profiler dla Uniksa ale również używaj smart-pointerów dla zarządzania czasem życia obiektów alokowanych na stercie!
narzędzia analizy i śledzenia pokrycia kodu Wykrywaj niedostępny (unreachable) kod martwy kod jest symptomem pomyłek programisty Pokrycie kodu jest miarą określającą jak duży procent kodu jest objęty procedurami testowymi Kod, który nigdy nie został uruchomiony dla przetestowania, nigdy nie może być nazwany działającym Narzędzia: profilery (MSVC, VTune, CodeAnalyst) gcov wykrywają również mnóstwo innych problemów
wymagania względem Waszych projektów Zademonstruj biegłość w używaniu funkcji wybranego debugera Zidentyfikuj hotspoty w swoim kodzie Używaj technik zapobiegania wyciekom pamięci i innych zasobów systemowych