Rok akademicki 2012/2013 Politechnika Warszawska Wydział Elektroniki i Technik Informacyjnych Instytut Informatyki

PRACA DYPLOMOWA INŻYNIERSKA

Piotr Piasek

Profilowanie aplikacji w programowej symulacji błędów na przykładzie AMD CodeAnalyst

Opiekun pracy dr inż. Piotr Gawkowski

Ocena: ......

...... Podpis Przewodniczącego Komisji Egzaminu Dyplomowego Specjalność: Inżynieria Systemów Informatycznych

Data urodzenia: 1989.08.17

Data rozpoczęcia studiów: 2008.10.01

Życiorys

Urodziłem się 17 sierpnia 1989 roku w Kozienicach w województwie mazowieckim. Naukę rozpocząłem w Publicznej Szkole Podstawowej nr. 1 im. Urszuli Kochanowskiej w Kozienicach. Następnie uczęszczałem do Publicznego Gimnazjum nr. 1 oraz do I Liceum Ogólnokształcącego im. Stefana Czarnieckiego, również w Kozienicach. Studia na wydziale Elektroniki i Technik Informacyjnych rozpocząłem w październiku 2008 roku na kierunku Informatyka, wybierając później specjalność ”Inżynieria Systemów Informatycznych”. Podczas studiów odbyłem praktyki w OLPP sp. z o.o., a od stycznia 2012 roku pracuję jako Specjalista w Biurze Informatyki w Towarzystwie Ubezpieczeń na Życie „Polisa-Życie” S.A.

...... Podpis studenta

EGZAMIN DYPLOMOWY

Złożył egzamin dyplomowy w dniu ...... 20__ r z wynikiem ......

Ogólny wynik studiów: ......

Dodatkowe wnioski i uwagi Komisji: ......

......

......

2

STRESZCZENIE

Głównym celem pracy było zwiększenie funkcjonalności symulatora błędów FITS w kierunku lepszego pokrycia testami, zwiększenia wydajności pułapkowania oraz obsługi aplikacji wielowątkowych. Wykorzystano przy tym mechanizmy profilujące systemu AMD CodeAnalyst do rejestrowania instrukcji wykonywanych przez testowane aplikacje oraz pułapkowanie aplikacji przed wykonaniem zadanej instrukcji. W pracy poruszono temat optymalizacji oprogramowania i wykorzystania w tym celu aplikacji profilujących. Przedstawiony został opis systemu AMD CodeAnalyst. Zaprezentowany został również projekt oraz implementacja mająca na celu realizację postawionego zadania. W pracy zawarto też informacje o sposobie używania tak rozszerzonej wersji symulatora FITS. Zmodyfikowana aplikacja została przetestowana pod kątem poprawności oraz wydajności. Testy przeprowadzano na aplikacjach zarówno jedno- jak i wielowątkowych.

Słowa kluczowe: programowa symulacja błędów, profilowanie aplikacji, AMD CodeAnalyst, optymalizacja oprogramowania, wydajność oprogramowania, wiarygodność

Application profiling in software implemented fault injection with AMD CodeAnalyst profiler example.

The main purpose of this thesis was extension of functionality of software fault injector FITS in order to improve test coverage, more effective fault triggering and support towards multithreaded applications. In order to achieve that the profiling mechanisms of AMD CodeAnalyst profiler was used to collect executed instructions of the tested applications as well as for better fault triggering. This paper contains also information about software optimization and using profiling application to achieve more optimized software without unnecessary efforts. Description of the AMD CodeAnalyst application was also attached. Further part of the thesis also presents the project and the description of implementation which led to the goal. This thesis contains also guidelines on how to use the enhanced version of FITS simulator. Designed solutions were tested for correctness and performance. Tests were based on single-, as well as on multithreaded applications.

Keywords: software implemented fault injection, application profiling, AMD CodeAnalyst, software optimization, software performance, dependability

3

Składam podziękowania panu dr. inż. Piotrowi Gawkowskiemu za cenne wskazówki, zaangażowanie i pomoc przy pisaniu niniejszej pracy.

4

SPIS TREŚCI

SPIS TREŚCI ...... 5

1 Wstęp ...... 7 1.1 Wprowadzenie ...... 7 1.2 Cele pracy ...... 7 1.3 Układ treści ...... 7

2 Opis symulatora błędów FITS ...... 9 2.1 Próby zwiększenia wydajności testów ...... 10 2.2 Próby obsługi aplikacji wielowątkowych ...... 11

3 Aplikacje profilujące ...... 12 3.1 Profilowanie statyczne ...... 12 3.2 Metody profilowania dynamicznego ...... 13 3.2.1 TBP – próbkowanie czasowe ...... 14 3.2.2 EBP - próbkowanie zdarzeniowe ...... 14 3.2.3 IBS – próbkowanie przepływu instrukcji ...... 15 3.2.4 Instrumentacja kodu ...... 16 3.3 Funkcjonalności dodatkowe narzędzi profilujących ...... 17 3.4 Ilościowe porównanie metod statystycznych i modyfikujących kod aplikacji ...... 17 3.5 Wady i ograniczenia aplikacji profilujących ...... 20 3.6 Wybrane systemy profilujące i pomocnicze ...... 21 3.6.1 Program profilujący GNU gprof ...... 21 3.6.2 Performance Inspector ...... 24

4 AMD CodeAnalyst ...... 26 4.1 Typy analizy ...... 27 4.1.1 Próbkowanie czasowe ...... 28 4.1.2 Próbkowanie zdarzeniowe ...... 28 4.1.3 Próbkowanie po instrukcjach ...... 29 4.2 Funkcjonalności dodatkowe...... 30 4.2.1 Przeglądanie stosu wywołań ...... 30 4.2.2 Wykres współbieżności ...... 30 4.2.3 Wykorzystanie zasobów systemowych ...... 30 4.3 Wpływ profilowania na wydajność aplikacji ...... 30 4.4 Informacje dostarczane przez CodeAnalyst ...... 33 4.4.1 Przepustowość instrukcji ...... 33 4.4.2 Przepustowość pamięci systemowej ...... 34 4.4.3 Charakterystyka dostępu do pamięci danych ...... 34 4.4.4 Charakterystyka dostępu do pamięci instrukcji ...... 35 4.4.5 Wydajność pamięci wirtualnej ...... 36 4.4.6 Zdarzenia związane z przepływem instrukcji ...... 37 4.5 Implementacja metod profilowania w mikroarchitekturze AMD ...... 38 4.5.1 Kontroler APIC ...... 38

5

4.5.2 Liczniki wydajnościowe ...... 38 4.5.3 Konstrukcja potoku ...... 39

5 Integracja AMD CodeAnalyst z systemem FITS ...... 41 5.1 Interfejs programowy AMD CodeAnalyst ...... 42 5.1.1 Integracja CA API z aplikacją ...... 42 5.1.2 Zbieranie profilu ...... 43 5.1.3 Analiza zebranych danych ...... 45 5.1.4 Struktury danych ...... 46 5.2 Koncepcja rozwiązania i sposób jej realizacji ...... 47 5.2.1 Modyfikacja generacji przebiegu wzorcowego ...... 49 5.2.2 Struktura pliku wzorcowego wykonania ...... 52 5.2.3 Implementacja eksperymentu nadzorowanego przez CodeAnalyst ...... 53

6 Weryfikacja rozwiązania ...... 59 6.1.1 Wyniki testu porównawczego dla aplikacji jednowątkowej ...... 60 6.1.2 Wyniki testu porównawczego dla aplikacji wielowątkowej ...... 63

7 Podsumowanie pracy ...... 65 7.1 Osiągnięte cele ...... 65 7.2 Napotkane trudności ...... 65 7.3 Możliwości rozwoju ...... 65

Zawartość płyty CD ...... 66

Bibliografia ...... 67

6

1 Wstęp

1.1 Wprowadzenie

Bardzo istotną cechą współczesnych systemów komputerowych jest ich niezawodność oraz wydajność. Poprawie niezawodności służy przede wszystkim proces testowania. Dodatkowo wsparty on może być analizą efektów błędów wprowadzanych sztucznie do testowanego programu przez programy wstrzykujące błąd [13], [14], [15]. Natomiast w celu poprawy wydajności w szerokim stopniu wykorzystywane są programy profilujące [1]. Na kartach tej pracy inżynierskiej przedstawiono w pewnym sensie wykorzystanie tych dwóch zagadnień do osiągnięcia wspólnego celu, jakim jest wydajne testowanie aplikacji w sytuacji błędu. Jednym słowem użyto aplikacji profilującej do wspomagania procesu wstrzykiwania błędów.

1.2 Cele pracy

Głównym celem tej pracy była rozbudowa systemu symulacji błędów FITS [7]. Zwiększenie funkcjonalności polegało na zaimplementowaniu obsługi aplikacji wielowątkowych oraz opracowaniu nowej metody pracy, aby uzyskać lepsze pokrycie testami oraz większą wydajność pułapkowania. Symulator ten jest autorską aplikacją promotora tej pracy. Wykorzystywany jest między innymi do przeprowadzania eksperymentów pozwalających na analizę wrażliwości na błędy sprzętowe różnych aplikacji programowych oraz doskonalenie programowych technik detekcji i obsługi błędów. Przy pracy nad nowymi funkcjonalnościami wykorzystano aplikację profilującą AMD CodeAnalyst [16], [17]. W opracowanym rozwiązaniu, sterownik tej aplikacji odpowiada za kontrolowanie momentu wstrzyknięcia błędów. Zwiększenie szybkości wykonywania eksperymentów umożliwiło testowanie na przykład złożonych aplikacji użytkowych takich jak pakiety biurowe, czego przykład zawarto w niniejszej pracy. Implementację postanowiono oprzeć akurat na tym produkcie, gdyż jest on bezpłatny oraz udostępnia interfejs programistyczny do języka C++, a więc takiego samego w jakim napisana jest aplikacja FITS.

1.3 Układ treści

W rozdziale drugim przedstawiono krótki opis symulatora błędów FITS. Zawiera on głównie informacje wyjaśniające sposób jego działania i realizacji wstrzykiwania błędów.

Rozdział trzeci przybliża metody oraz działanie aplikacji profilujących, które służą pomocą przy pracy nad zwiększeniem wydajności oprogramowania. Przedstawiony został podział aplikacji profilujących ze względu na metody budowania profilu. Zamieszczono również porównanie wpływu różnych metod zbierania danych: statystycznych (opartych na próbkowaniu stanu aplikacji) i wstrzykujących (opartych na modyfikacji badanego programu) na wydajność testowanej aplikacji. Na koniec opisano dwa nierozbudowane, ale za to bezpłatne i otwartoźródłowe pakiety oprogramowania profilującego.

W rozdziale czwartym przedstawiono projekt AMD CodeAnalyst. Wyjaśniono wszystkie dostępne tryby profilowania oraz zamieszczono porównanie narzutu na wydajność, jakie niesie użycie każdego z nich. Zawarto również informacje o dodatkowych funkcjonalnościach tego pakietu. Mimo mnogości rodzajów informacji, jakich dostarcza ten profiler, celem autora

7

było opisać metody wnioskowania na podstawie tych informacji. Pod koniec przybliżono sposób implementacji budowania profilu wynikający z konstrukcji mikroprocesorów.

Piąty rozdział zawiera opis zmian w implementacji FITS, których wymagało wprowadzenie nowych funkcjonalności. Znajduje się tam również opis nowopowstałego formatu pliku wynikowego FITS, który może posłużyć np. do opracowania statystyk wykonywanych instrukcji. Czytelnik zostanie też wprowadzony w sposób integracji API CodeAnalyst do własnego projektu.

W przedostatnim rozdziale zaprezentowano wyniki testów wykonanych za pomocą zmodyfikowanej aplikacji symulatora błędów. Znajdują się w nim zarówno testy porównawcze jak i wyniki weryfikacji nowych funkcjonalności, które nie mogły zostać porównane do oryginalnej wersji symulatora FITS.

Szósty rozdział to podsumowanie pracy, w skład którego wchodzi opis osiągniętych celów, refleksje odnośnie napotkanych trudności oraz sugerowane usprawnienia, a także wnioski płynące z wykonanej pracy.

8

2 Opis symulatora błędów FITS

Symulator błędów FITS powstał podczas badań promotora tej pracy, doktora Piotra Gawkowskiego [7]. Dla lepszego zrozumienia kolejnych rozdziałów niezbędny jest krótki opis działania tego symulatora oraz wyjaśnienie pochodzenia problemów, których poprawa była celem tej pracy. Oprócz tego poniżej zostały przytoczone również wyniki niezależnych prac nad aplikacją FITS.

FITS jest programowym symulatorem błędów opartym na interfejsie programistycznym Win32 – Debugging API. Wykonanie testu symulującego błąd w uproszczeniu składa się z wytypowania miejsca i momentu generacji błędu a następnie nadzorowania działania testowanej aplikacji, aż do osiągnięcia wybranego momentu generacji błędu. Wówczas następuje wstrzyknięcie błędu oraz dalsza obserwacja działania testowanej aplikacji. Wybranie momentu generacji błędu odbywa się na podstawie tak zwanego wzorcowego przebiegu (ang. Golden Run) testowanej aplikacji – wykonania niezakłóconego błędami. Zebranie zapisu wzorcowego wykonania polega na rejestrowaniu instrukcji maszynowych wykonywanych przez testowany program bez wprowadzonych modyfikacji. Do tego celu wykorzystywane jest krokowe wykonywanie instrukcji maszynowych. Powszechnie wiadomo, że jest to bardzo czasochłonna czynność spowodowana przede wszystkim ciągłym przekazywaniem sterowania pomiędzy testowaną aplikacją a symulatorem błędów [23], [24]. Do tego dochodzi narzut wydajnościowy spowodowany przez samą aplikację FITS, która zapisuje wykonanie każdej pojedynczej instrukcji.

Tą samą metodą realizowane jest nadzorowane wykonanie aplikacji podczas testu, aby móc ją zatrzymać na N-tym wykonaniu danej (wybranej jako wyzwalająca generację błędów) instrukcji i wstrzyknąć błąd. Podejście polegające na wstrzyknięciu błędu dopiero przy N-tym wykonaniu instrukcji spod określonego adresu jest wymagane, aby osiągnąć miarodajne wyniki testów (m.in. zapewnieniu powodzenia generacji błędów podczas testu przy żądanej strategii rozkładu błędów w czasie wykonywania się testowanej aplikacji i przestrzeni zasobów przez nią wykorzystywanych). W przeciwnym przypadku, jeśli błąd byłby wstrzykiwany dla przykładu przy starcie programu i nawet w każdą instrukcję po kolei to rozkład testów byłby bardzo ubogi. Spowodowane jest to cechą oprogramowania, opierającą się na tym, że przez większość czasu wykonania programu procesor wykonuje wielokrotnie ograniczony zbiór instrukcji. Eksperymenty symulacyjne wymagają wielokrotnych testów (niezależnych wykonań aplikacji testowanej), zatem uzyskanie statystycznie istotnych wyników jest możliwe tylko wówczas, gdy zostanie zapewniony określony rozkład momentów i miejsc generacji błędów. Wymagany jest więc wysoki poziom kontroli nad momentem generacji błędów a to wymaga korelacji momentu generacji z postępem wykonywania się aplikacji testowanej. W dotychczasowej wersji zapewnione było to poprzez szczegółowy profil dynamiczny zbierany w trybie pracy krokowej procesora. Szerzej o tym problemie zawarto w dalszej części pracy.

FITS oferuje kilka metod wyznaczania zbioru instrukcji, które mają zostać zarejestrowane i poddane późniejszym testom. Są to kolejno:

 Wszystkie instrukcje wykonywane od początku wykonywania testowanej aplikacji – jest to najbardziej czasochłonna metoda analizy ponieważ obejmuje analizą również kod startowy i końcowy aplikacji (pre- i post-ambułę aplikacji);

9

 Instrukcje znajdujące się w obszarze adresowym zdefiniowanym przez pierwszy i ostatni adres obszaru – stwarza to problem gdyż można zdefiniować tylko jeden taki obszar;  Zbiór instrukcji znajdujący się pomiędzy specjalnymi znacznikami, którymi jest sekwencja instrukcji int 3, nop, nop – jak dotąd jest to najwydajniejszy sposób działania symulatora, który umożliwia wygodne zdefiniowanie interesującego zakresu w kodzie źródłowym do analizy; głównym mankamentem takiego podejścia jest wymóg posiadania kodu źródłowego oprogramowania;

Jednak nawet mimo wykorzystywania ostatniej metody (oznaczania obszaru z testowanymi instrukcjami) narzut czasowy stwarzany przez symulator jest bardzo duży. Wyobrażeniu sobie wpływu krokowego wykonywania instrukcji mogą pomóc wykonane testy. Podczas budowania przebiegu wzorcowego niewielkiej aplikacji analizowanych było około 30 tysięcy instrukcji na sekundę, natomiast wydajność procesora, na którym przeprowadzony był test, według benchmarków to około 3 miliardy instrukcji na sekundę. Liczby te różnią się od siebie o dziesięć milionów procent. Nawet jeśli pomiary są obarczone dużym błędem to przy tak dużej różnicy nie trudno sobie wyobrazić, aby aplikacja w normalnych warunkach wykonująca się sekundę, w przypadku rejestrowania przebiegu wzorcowego wykonywała się ponad 1000 sekund. Samo przeprowadzanie testów jest mniej czasochłonne, gdyż wystarczy zliczanie ilości wejść do obszaru, w którym znajduje się instrukcja wyznaczona do wstrzyknięcia błędu. Jednakże i w tym przypadku czasochłonność przeprowadzania eksperymentu uniemożliwia pracę z rozbudowanymi aplikacjami. Oprócz narzutu wydajnościowego rejestracja tak dużej ilości instrukcji może przysporzyć problemy z dostępną pamięcią operacyjną potrzebną na zbierane dane. Podczas przeprowadzania testów nie było w praktyce możliwe zarejestrowanie przebiegu wzorcowego dla aplikacji wykonującej się w normalnych warunkach około dziesięciu sekund, objawiało się to awaryjnym zamknięciem procesu symulatora przez system Windows.

Kolejną niepożądaną cechą, którą postarano się wyeliminować podczas pisania tej pracy jest obsługa przez FITS jedynie aplikacji jednowątkowych. Jest to bardzo istotna wada, szczególnie obecnie, kiedy rozwój jednostek obliczeniowych podąża nie w kierunku wzrostu częstotliwości pracy a w kierunku wielowątkowości i posiadania kilku rdzeni. Wykorzystywanie wielowątkowości jest też coraz szerzej wykorzystywanie przez programistów, gdyż umożliwia logiczne odseparowanie elementów rozbudowanego systemu, a także wzrost jego wydajności.

Wysiłki w celu poprawy funkcjonalności symulatora FITS podjęte podczas pisania tej pracy nie są jedynymi. W dwóch następnych punktach pokrótce przedstawiono niektóre z dotychczasowych prób tych popraw. Opisano w nich przede wszystkim wykorzystane idee oraz osiągnięte wyniki.

2.1 Próby zwiększenia wydajności testów

Pierwsza z omawianych idei optymalizacji symulatora FITS polegała na zastosowaniu obszaru kodu pułapkującego (ang. Code Cave). Prace te zostały przeprowadzane w ramach pracy inżynierskiej Pana Grzegorza Smulki [23]. Pomysł bazował na przeniesieniu części mechanizmu sterującego eksperymentem do kontekstu testowanej aplikacji. Dzięki temu uniknięte zostało częste przekazywanie sterowania pomiędzy symulatorem a testowanym programem. Całość koncepcji opierała się, aby instrukcję wyzwalającą wyznaczoną do testów

10

zastąpić instrukcją skoku do dodatkowo przydzielonemu aplikacji testowanej obszaru pamięci z instrukcjami wstrzykniętymi przez FITS. Wspomniany obszar pamięci odpowiadał za zliczanie liczby wykonań instrukcji wyzwalającej, aby w momencie, gdy należało wstrzyknąć błąd móc ustawić właściwą pułapkę i dzięki temu przekazać ponownie sterowanie do symulatora [23], [24]. Przyjęcie takiego podejścia wiązało się z problemem obsługi szeregu przypadków, kiedy rozmiar instrukcji będącej obiektem testów jest mniejszy niż instrukcja skoku do zaalokowanego obszaru pamięci. Problemem okazało się również pułapkowanie w ten sposób niektórych instrukcji, w szczególności zawierających adresy względne w argumencie lub korzystające z adresów wyliczanych lub ładowanych z pamięci w trakcie działania programu. Pierwszy z problemów został rozwiązany dzięki wykorzystaniu czterech rejestrów sprzętowych DR0-DR3, które umożliwiają zakładanie pułapki pod zdefiniowanym adresem. Drugi z problemów nie został rozwiązany i instrukcje takie są pułapkowane w standardowy sposób, jednakże we wspomnianej pracy inżynierskiej zaproponowano możliwość poprawy tej sytuacji. Wyniki testów porównawczych są bardzo zadowalające, o czym świadczy nawet siedmiokrotny wzrost wydajności symulatora. Niestety zaproponowany mechanizm nie zwiększał wydajności rejestracji przebiegu wzorcowego.

2.2 Próby obsługi aplikacji wielowątkowych

Inną próbę rozszerzenia funkcjonalności symulatora FITS podjął w ramach pracy magisterskiej Paweł Włodawiec [22]. Wynikiem tych prac była rozszerzona wersja symulatora FITS o nazwie MTInjector. Testy zastosowanego rozwiązania nie są zadowalające i zostały dokładnie opisane w rozprawie doktorskiej Piotra Gawkowskiego [21]. Głównym mankamentem symulatora MTInjector był nierównomierny rozkład wstrzykniętych błędów. Cytując wspomnianą rozprawę doktorską „Najczęstsze momenty generacji błędu stanowiły jedynie 2,38% instrukcji całego programu, a stanowiły aż 89,9% wszystkich symulacji błędów”. Taki stan rzeczy spowodowany został najprawdopodobniej faktem, że zatrzymanie programu wielowątkowego za pomocą funkcji systemu operacyjnego SuspendThread (a nie jak to miało dotychczas miejsce przy wykorzystaniu pułapek na konkretnych instrukcjach aplikacji testowanej) nie powodowało jego zatrzymania w żądanych momentach, ale raczej na określonym zbiorze instrukcji. Powodowało to nieakceptowalne zakłócenie statystycznego rozkładu generowanych błędów.

11

3 Aplikacje profilujące

Profilowanie aplikacji jest formą dynamicznej analizy oprogramowania, która polega na uruchamianiu aplikacji będącej obiektem analizy. Oprogramowanie wspomagające taką analizę zwane jest aplikacjami profilującymi, analizatorami wydajności lub potocznie – profilerami. Pierwszym i podstawowym celem procesu profilowania jest wytypowanie fragmentów kodu, które pochłaniają najwięcej zasobów systemowych. Do mierzonych zasobów będzie należał najczęściej czas użycia jednostki obliczeniowej oraz wykorzystanie pamięci operacyjnej. Drugim, chociaż nie mniej ważnym celem a nie zawsze realizowanym przez oprogramowanie profilujące, jest zebranie informacji przydatnych w określaniu przyczyn spadku wydajności aplikacji. Oczywiście zastosowań analizatorów wydajności można znaleźć więcej. Przykładem jest szansa użycia profilera do sprawdzenia pokrycia kodu testami lub do porównania i analizy różnych mikroarchitektur mikroprocesorów.

Zaimplementowanie pierwszych narzędzi zbliżonych działaniem do współczesnych programów profilujących przypada na początek lat 70. ubiegłego wieku. Przeznaczone były na platformy IBM/360 oraz IBM/370. Analiza działania wydajności systemu polegała na okresowym generowaniu przerwania, podczas którego zapisywane były rejestry stanu procesora. Tak zebrane dane przeznaczone były do dalszej analizy w celu znalezienia najczęściej wykonywanych fragmentów kodu uruchamianych programów. Kolejnym krokiem w rozwoju była implementacja symulatorów procesorów (ang. Instruction Set Simulator), dzięki którym możliwe stało się dokładne monitorowanie przepływu instrukcji. W 1982 roku udostępniony został program profilujący gproof (krótko opisany pod koniec rozdziału), który dzięki ogromnej uniwersalności stosowany jest po dziś dzień. Od tamtej pory rozwój aplikacji profilujących związany jest głównie z implementacją nowych rozwiązań wbudowanych w mikroprocesory lub technik modyfikacji kodu analizowanej aplikacji.

Użytkownikami programów profilujących są głównie osoby zajmujące się dostarczaniem aplikacji i systemów informatycznych, a w szczególności programiści, stojący przed zadaniem poprawy wydajności. Głównymi dostarczycielami tych narzędzi są producenci zintegrowanych środowisk programistycznych np. Microsoft Visual Studio, a także z racji tego, że programy profilujące wykorzystują specjalnie do tego stworzone mechanizmy we współczesnych mikroprocesorach to wśród popularnych programów tego typu znajdują się również produkty AMD i .

3.1 Profilowanie statyczne

Pomimo, iż praktycznie wszystkie aktualnie dostępne aplikacje profilujące korzystają z dynamicznej analizy, co podkreślone jest w definicji profilowania oprogramowania, to możliwa i często cenna jest również analiza statyczna. Opiera się ona na analizie kodu maszynowego, źródłowego lub kodu obiektowego programu bez potrzeby jego uruchamiania. Mała popularność tej metody ma kilka przyczyn. Oprogramowanie realizuje zawsze pewien zaprogramowany algorytm, który zawsze sterowany jest zbiorem danych wejściowych. Dlatego bez sterowania programu rzeczywistymi i przygotowanymi danymi trudniej przewidzieć np. częstotliwość wykonywania określonych fragmentów kodu. Istotną wadą analizy statycznej jest też jej cena, metody służące takiej formie analizy oparte są najczęściej na modelach matematycznych, których obliczanie wymaga dużej mocy obliczeniowej i których poziom komplikacji wzrasta ze złożonością oprogramowania. Kolejną słabą stroną

12

takiego podejścia do profilowania jest fakt, że czasy opóźnień oraz przepustowość takich samych instrukcje z tych samych zbiorów (np. ), różnią się w zależności od implementacji w konkretnym mikroprocesorze. Oprócz tego na szybkość wykonywania instrukcji wpływ mają również inne elementy systemu komputerowego jak np. szybkość i pojemność pamięci operacyjnej, szybkość różnych magistrali i tym podobne cechy. W związku z tym wraz z statyczną analizą kodu wymagane byłoby stworzenie kompletnego modelu produkcyjnego sprzętu wykonawczego. Byłoby to zadanie trudne w szczególności, że istnieje bardzo dużo elementów wyróżniających sprzęt.

Podejście statyczne pozwala jednak na symulowaniu maksymalnego czasu potrzebnego na wykonanie zadania, budowanie grafu możliwych stanów systemu oraz poznaniu zbioru zasobów wykorzystywanych przez oprogramowanie. Statyczna analiza wykorzystywana jest często w systemach czasu rzeczywistego o krytycznych wymaganiach czasowych, takich jak oprogramowanie medyczne, sterowania procesami technologicznymi (np. w elektrowniach jądrowych). Prowadzone są m.in. prace nad modelowaniem systemów przy wykorzystaniu maszyn stanowych w celu wykrycia potencjalnie niebezpiecznych sytuacji (np. zakleszczeń) [18]. Wyznaczenie zbioru zasobów wykorzystywanych przez oprogramowanie pozwala natomiast na opracowanie dedykowanych procedur autodiagnostycznych zorientowanych jedynie na wykorzystywane zasoby sprzętowe (nie ma potrzeby testowania modułów niewykorzystywanych w systemie) – tzw. application driven testing [25].

Wartym wspomnienia narzędziem profilującym jest AQtime [2] firmy SmartBear. Profiler ten oprócz wykorzystania dynamicznej metody analizy używa również metody statycznej. Wyniki uzyskane tą metodą niestety nie identyfikują fragmentów kodu wymagających najwięcej zasobów systemowych. Mogą natomiast posłużyć do oceny przyczyny spadku wydajności, między innymi dzięki wyszukiwaniu ilości użytych instrukcji różnego typu jak SSE, stało- lub zmiennoprzecinkowych, a także generowaniu sekwencji wołań funkcji i powiązań między klasami.

Więcej informacji o metodach statycznej analizy, a w szczególności analizy przepływu programu można znaleźć w publikacjach [4], [8].

3.2 Metody profilowania dynamicznego

Wyróżnia się kilka metod działania narzędzi profilujących [6]. Większość profilerów ma zaimplementowaną co najmniej jedną z wymienionych w kolejnych podrozdziałach metod. Każda z nich ma swoje zalety i wady, a porównywanie wyników otrzymanych różnymi sposobami może prowadzić do wyciągnięcia nieprawdziwych hipotez. Głównymi czynnikami różniącymi każdą z metod jest wiarygodność otrzymanych danych, narzut na wydajność systemu komputerowego, na którym działa aplikacja profilująca oraz rodzaj zebranych danych. Każda z metod opiera się na jednym z dwóch całkowicie odrębnych podejść do sposobu zbierania danych o charakterystyce wykonania analizowanej aplikacji: instrumentacji kodu (wykonywalnego lub źródłowego, wykonywanego statycznie lub dynamicznie) lub dynamicznego próbkowania.

Pierwszy ze sposobów polega na modyfikowaniu monitorowanych aplikacji. Modyfikowanie to wykonywane może być przed ich wykonaniem, chociaż istnieją również programy profilujące, które potrafią modyfikować kod maszynowy ‘w locie’. Wiąże się to z długim przygotowywaniem sesji zbierania danych, a wielokrotne wprowadzanie zmian w

13

kodzie programu i powtórne budowanie profilu znacząco wydłuża całkowity czas spędzony nad identyfikacją miejsc krytycznych. Jego wadą jest również często nieco większy narzut na wydajność aplikacji profilowanej w stosunku do metody próbkowania, ale nie jest to regułą i różnica może być niezauważalna. Zaletą programów profilujących opierających się na modyfikowaniu aplikacji może być natomiast większa ilość zebranych danych (w kontekście wewnętrznych danych aplikacji – np. wartości zmiennych) oraz ich wiarygodność.

Inną koncepcją jest metoda dynamicznego próbkowania. Polega ona na generowaniu przerwań systemowych, co określony czas, ale mogą to być również przerwania generowane na podstawie określonych zdarzeń zachodzących np. na poziomie sprzętowym. Za obsługę takiego przerwania odpowiada program profilujący, który może w momencie takiego przerwania zebrać potrzebne dane o aktualnie wykonywanej instrukcji przez monitorowaną aplikację, a następnie przywrócić system do dalszego działania. Największą zaletą takiego podejścia jest minimalna degradacja wydajności obserwowanej aplikacji, chociaż jest to też ściśle uzależnione od częstotliwości przerwań. Pomimo, że zbieranych jest relatywnie mniej danych w porównaniu z instrumentacją to zebrane dane lepiej oddają charakter przepływu instrukcji w rzeczywistym systemie właśnie dlatego, że próbkowanie ma mniejszy wpływ na wydajność obiektu analizy. Wadą tego rozwiązania jest poleganie na statystyce, co wiąże się z tym, że pewne istotne fragmenty kodu mogą nie zostać wychwycone w zbudowanym profilu, chociażby z powodu specyficznego rozkładu instrukcji maszynowych.

Poniżej wyszczególniono cztery podstawowe metody pracy aplikacji profilujących. Podział ten nie jest sztywny i oparty jest głównie na podstawie analizy aplikacji AMD CodeAnalyst oraz Intel VTune. Z tego powodu w różnych produktach mogą zostać zastosowane wariacje tych metod.

3.2.1 TBP – próbkowanie czasowe

Metoda bazująca na upływie czasu (ang. Time Based Profiling; TBP) polega na generowaniu przerwań z określonym okresem czasowym i zapisywaniu jedynie adresu instrukcji znajdującego się w liczniku rozkazów przerwanego procesu. Jest to metoda oparta na statystyce, przez co informacje przez nią generowane mogą prowadzić do błędnych wniosków. Dużą wadą jest kolekcjonowanie małej ilości danych, przez co możliwe jest określenie co najwyżej miejsca spadku wydajności bez danych sugerujących jego przyczynę. Natomiast argumentem przemawiającym za jej wykorzystaniem jest minimalny wpływ na wydajność profilowanej aplikacji, chociaż ściśle powiązany z wartością interwału przerwań. Do jej implementacji najczęściej wykorzystywane są układy APIC.

3.2.2 EBP - próbkowanie zdarzeniowe

Metoda bazująca na zdarzeniach (ang. Event Based Sampling; EBP) oparta jest na przerwaniach generowanych co określoną liczbę zdarzeń spowodowanych przez wykonywane instrukcje. Rodzaj zdarzeń jak i ich interwał programowany jest w licznikach wydajnościowych (ang. performance counters) wbudowanych w mikroprocesor [19], [20]. Liczba fizycznych liczników jest ograniczona, przez co utrudniona staje się obserwacja różnych typów zdarzeń. W celu ominięcia tego ograniczenia niektóre analizatory wydajności stosują multipleksowanie takich liczników. Multipleksowanie realizowane jest przez przeprogramowanie co jakiś czas liczników w celu obserwacji innych zdarzeń. Zmniejsza to jednak zamierzoną liczbę otrzymanych próbek dla wybranych zdarzeń. Innym sposobem na

14

obejście tej wady jest kilkukrotne powtarzanie budowy profilu dla różnych zestawów zdarzeń - niestety, identyczne powtórzenie wykonania programu może okazać się bardzo trudne lub niemożliwe ze względu na jego specyfikę. Kolejną wadą tej metody jest spekulatywny i potokowy charakter wykonywania instrukcji przez współczesne mikroprocesory, który wiąże się z wykonywaniem kilku instrukcji na raz. Każda z instrukcji przebywająca w potoku może spowodować zdarzenie, które ma być rejestrowane w licznikach, natomiast w czasie ewentualnego przerwania wygenerowanego przez licznik odczytywany jest adres ostatnio zatwierdzonej instrukcji, która wcale nie musiała spowodować przypisanego jej zdarzenia.

Rodzaje programowalnych zdarzeń są niskopoziomowe, powiązane z konstrukcją współczesnych potoków w procesorach czy mechanizmem pamięci wirtualnej, albo ogólniej: ściśle zależą od rozwiązań zastosowanych w sprzęcie. Wśród nich można wymienić liczbę chybień w buforze L1, liczbę chybień w buforze L2, liczbę błędnie przewidzianych rozgałęzień, liczbę zatwierdzonych instrukcji oraz wiele innych. Dzięki skorelowaniu instrukcji kodu źródłowego z danym zdarzeniem możemy łatwiej rozpoznać przyczynę słabej wydajności. Temat ten został dogłębnie poruszony przy opisie liczników zaimplementowanych w procesorach AMD (patrz rozdział 4.4).

Typy dostępnych zdarzeń w systemie są uzależnione od zainstalowanego procesora, gdyż każdy producent implementuje liczniki obsługujące różne, choć zazwyczaj podobne, zdarzenia. Wraz z rozwojem mikroarchitektur procesorów, zmienia się znaczenie tych samych zdarzeń, dlatego przy czerpaniu wiedzy na ich podstawie obowiązkowa jest znajomość istoty zdarzenia w danej mikroarchitekturze. W każdym logicznym procesorze, bądź rdzeniu, implementowany jest autonomiczny zestaw liczników. Pierwszą, chociaż nieudokumentowaną implementacją takich liczników wprowadził Intel w swoim mikroprocesorze Pentium.

3.2.3 IBS – próbkowanie przepływu instrukcji

Metoda bazująca na przepływie instrukcji (ang. Instruction Based Sampling; IBS) [5] została po raz pierwszy zaimplementowana przez AMD w 2007 roku dla rodziny procesorów 10h w celu wyeliminowania wad związanych z przedstawioną wyżej metodą budowania profilu. Tak samo jak dwie wcześniej opisane, oparta jest na statystyce i dostarcza również danych o zajściu pewnych niskopoziomowych zdarzeń. Rodzaj zdarzeń uzależniony jest od rodzaju procesora. Interwałem określającym częstotliwość przerwań jest ilość instrukcji asemblerowych pobranych z pamięci do procesora w celu wykonania, lub ilość mikroinstrukcji wysłanych do jednostek wykonawczych procesora, takich jak jednostka całkowitoliczbowa, zmiennopozycyjna itp. Powodem oddzielenia tych dwóch zdarzeń jest natura współczesnych mikroprocesorów, w których można wyróżnić dwa oddzielne etapy, mianowicie:

 pobranie i przygotowanie instrukcji;  wykonanie instrukcji.

W przypadku, gdy któryś z liczników osiągnie wartość odpowiadającego mu interwału, pobrana instrukcja lub mikroinstrukcja jest oznaczana i obserwowana przy przejściu przez cały potok procesora. Każde zdarzenie spowodowane przez taką instrukcję jest zapisywane a dzięki temu powiązanie zdarzenia z instrukcją jest dokładne. Wyeliminowany został również problem z niedostateczną ilością liczników wydajnościowych, gdyż w tym przypadku

15

potrzebne są co najwyżej dwa (podczas gdy np. procesory z serii AMD Phenom II mają wbudowane cztery takie liczniki). Dla poprawy wyników otrzymanych tą metodą układy mikroprocesorów stosują również niewielkie zaburzenie przy ustawianiu wartości startowej liczników wydajnościowych. Dzięki temu rozkład przerwań jest lepiej rozłożony podczas wykonywania instrukcji, dzięki uniknięciu wpadnięcia w ewentualny cykl.

3.2.4 Instrumentacja kodu

W przypadku instrumentacji (ang. Instrumentation) budowanie profilu aplikacji polega na umieszczaniu dodatkowego kodu w aplikacji będącej obiektem profilowania. Zadaniem takiego kodu jest np. zliczanie liczby wywołań funkcji, czasu ich trwania oraz rejestrowania zależności pomiędzy wywołaniami funkcji. Z tego powodu dodatkowy kod powiązany jest z każdą wywoływaną funkcją. Istnieje kilka sposobów na umieszczanie dodatkowych instrukcji. Niektóre narzędzia dodają instrukcje już na poziomie kodu źródłowego, innym rozwiązaniem jest umieszczanie dodatkowego kodu przez kompilatory. Możliwe jest również modyfikowanie już skonsolidowanego pliku binarnego albo wstrzykiwanie kodu w czasie wykonania (ang. runtime injection).

Programy profilujące oparte na tej metodzie eliminują błędy jakie wiążą się z metodami statystycznymi dlatego wyniki otrzymane tym sposobem łatwiej powtórzyć oraz lepiej przedstawiają ogólną wydajność wszystkich funkcji. Jednak największą zaletą jest możliwość wygenerowanie bardzo dokładnego grafu zależności wywołań funkcji. Dzięki temu łatwe staje się wytypowanie do optymalizacji najczęściej wywoływanych lub najdłużej wykonywanych funkcji.

Istotną wadą instrumentacji jest większa ingerencja w przepływ instrukcji profilowanej aplikacji spowodowana koniecznością modyfikacji kodu. Może się to przekładać na zwiększony spadek wydajności analizowanej aplikacji w stosunku do wpływu trzech wyżej wymienionych metod. Niekiedy problematyczna staje się ilość zebranych danych, szczególnie przy intensywnej pracy aplikacji i długim czasie budowania profilu. Informacje wygenerowane tą metodą, w przeciwieństwie do otrzymanych na podstawie metod opartych o statystykę, najczęściej nie pozwalają na dokładne wskazanie instrukcji powodującej spadek wydajności. Niemożliwe jest również wskazanie spadków wydajności związanych z brakiem optymalizacji kodu w odniesieniu do mikroarchitektury (chybienia w pamięci podręcznej, nieudane przewidywanie skoków itp.).

Rynek aplikacji profilujących jest ciągle ‘żywy’ i ich projektanci starają się opracowywać i implementować kolejne sposoby zbierania danych. Nowe możliwości aplikacji profilujących powstają przede wszystkim dzięki inżynierom pracującym nad mikroarchitekturami procesorów, którzy implementują w nich mechanizmy pozwalające na zbieranie coraz dokładniejszych danych o wykonywanych instrukcjach.

16

3.3 Funkcjonalności dodatkowe narzędzi profilujących

Wymienione dotychczas sposoby zbierania danych do budowy profilu nie są jedynymi możliwościami, jakie oferują współczesne analizatory wydajności. Wiele z nich oferuje dodatkowe opcje, które nie są powiązane z konkretną metodą.

Jednym z takich udoskonaleń jest rozwijanie stosu (ang. stack unwinding) wykorzystywane przy użyciu metod statystycznych. Daje to możliwość budowy niepełnego grafu zależności pomiędzy wywołaniami funkcji. Realizowane jest to dzięki przeglądaniu stosu w trakcie obsługi przerwania związanego z aplikacją profilującą. Stos przeglądany jest aż do napotkania określonej przez użytkownika liczby adresów powrotu z funkcji – im jest ona większa tym graf jest bardziej dokładny. Z drugiej strony, rozwijanie stosu jest operacją kosztowną, przez co głębokość przeglądania jak i częstotliwość znacząco wpływa na wydajność profilowanej aplikacji (co przedstawiono w rozdz. 4.3).

Innymi udogodnieniami oferowanymi przez aplikacje profilujące jest zapis wykorzystania procesora oraz ilości zajętej pamięci przez analizowane procesy. W przypadku procesorów wielordzeniowych wiele narzędzi potrafi przedstawić sekwencję przeplotu wątków jak i przypisanie ich do konkretnego rdzenia.

3.4 Ilościowe porównanie metod statystycznych i modyfikujących kod aplikacji

Porównywanie metod statystycznych do metod opartych na modyfikacjach kodu profilowanej aplikacji jest zadaniem trudnym ze względu na pewną komplementarność dostarczanych przez nie informacji. Obie grupy metod dostarczają innych informacji, a wielkość spowodowanego przez nie spadku wydajności jest ściśle związana z konstrukcją analizowanej aplikacji. Ilość danych zebranych za pomocą modyfikacji kodu jest powiązana z ilością wywołań funkcji zawartych w profilowanym kodzie. Zależność ma charakter proporcjonalny, tj. przy dwukrotnym wzroście liczby wołanych funkcji, co najmniej dwukrotnie wzrasta ilość danych. Dodatkowa zaleta to także dostępność z poziomu kodu źródłowego do profili danych (np. wartości argumentów wywołania). Natomiast ilość informacji zebranych metodami statystycznymi skorelowana jest najczęściej z długością pracy aplikacji.

W próbie porównania metody modyfikującej kod aplikacji oraz metody bazującej na upływie czasu autor wykorzystał analizator wydajności dostarczany z pakietem programistycznym Microsoft Visual Studio 2010 Ultimate. Testy zostały przeprowadzone na dwóch wersjach aplikacji generującej dwie macierze (1500 wierszy na 1500 kolumn) liczb zmiennoprzecinkowych, a następnie przeprowadzającej ich mnożenie. Jedna z aplikacji wykonuje mnożenie w sposób nieoptymalny dla pamięci podręcznej procesora (wyniki zamieszczono na Rys. 1, optymalizacja kodu omówiona w dalszych akapitach), natomiast druga jest pod tym kątem zoptymalizowana (patrz Rys. 2). Otrzymane wyniki są uśrednione na podstawie pięciu prób. Procesor, na którym został uruchomiony test to AMD Phenom X2 955.

17

Czas trwania aplikacji dla wybranych metod analizy 49

48

47

46 Bez analizy 45 Oparta na upływie 44 czasu; wzrost o 1%

liczba liczba sekund[s] 43 Oparta na modyfikacji aplikacji; wzrost o 6% 42

41

40 Rodzaj analizy

Rys. 1 Wyniki otrzymane dla aplikacji niezoptymalizowanej

Ilość danych zapisanych na dysku zebranych metodą opartą na upływie czasu to 5,1 megabajta, podczas gdy modyfikowanie kodu aplikacji wygenerowało 317 megabajtów, czyli ponad 60 razy więcej niż poprzednia. Zauważalny jest też większy spadek wydajności analizowanej aplikacji spowodowany metodą opartą na modyfikacji aplikacji w porównaniu do profilowania metodą statystyczną.

Czas trwania aplikacji dla wybranych metod analizy 8

7

6

5 Bez analizy

4 Oparta na upływie 3 liczba liczba sekund[s] czasu; wzrost o 23%

2

1

0 Rodzaj analizy

Rys. 2 Wyniki otrzymane dla aplikacji zoptymalizowanej

18

W przypadku analizy aplikacji zoptymalizowanej pod kątem wykorzystania pamięci podręcznej różnice pomiędzy oboma metodami profilowania są jeszcze bardziej uwydatnione. Ilość danych zapisanych na dysku zebranych metodą opartą na upływie czasu to 0,6 megabajta, podczas gdy modyfikowanie kodu aplikacji wygenerowało tak samo jak w poprzednim teście 317 megabajtów, w tym przypadku różnica jest pięćsetkrotna.

Porównując czasy wykonania obu wersji aplikacji bez profilowania widać również znaczącą różnicę w wydajności (dziesięciokrotne skrócenie czasu wykonania). Reorganizacja kodu polegała na zamianie kolejności mnożenia poszczególnych komórek dwuwymiarowych tablic. Tablica taka, w przypadku języka C++, jest zaalokowana, jako ciągły obszar pamięci, a dane przechowywane są wiersz po wierszu tzn. najpierw wszystkie dane z pierwszego wiersza potem z drugiego itd. Jeśli tablice takie są stosunkowo duże – na przykład jeden wiersz danych nie mieści się w rozmiarze jednej strony pamięci wirtualnej to zły dostęp do poszczególnych komórek takiej tablicy może być bardzo czasochłonny. Gdy dostęp do tablicy zrealizowany jest w ten sposób, że dane są odczytywane kolumna po kolumnie, to zmiana indeksu tablicy powoduje, że każde odwołanie wskazuje na inną wirtualną stronę. Pociąga to za sobą, dużo błędów w procesie translacji stron, a tym samym zwiększa czas odczytu danej z pamięci. Dodatkowo niesekwencyjny odczyt pamięci fizycznej utrudnia efektywne wykorzystanie pamięci podręcznej procesora. Poniżej przedstawiono dwa fragmenty kodu źródłowego realizujące mnożenie macierzy.

Nieoptymalny kod mnożący macierze: for (int i = 0 ; i < ROW_COUNT ; i++) { for (int j = 0 ; j < COLUMN_COUNT ; j++) { for (int k = 0 ; k < COLUMN_COUNT ; k++) { resultMatrix[i][k] += firstMatrix[i][j] * secondMatrix[j][k]; } } }

Zoptymalizowany kod mnożący macierze:

for (int i = 0 ; i < ROW_COUNT ; i++) { for (int j = 0 ; j < COLUMN_COUNT ; j++) { for (int k = 0 ; k < COLUMN_COUNT ; k++) { resultMatrix[i][j] += firstMatrix[i][k] * secondMatrix[k][j]; } } }

Zaprezentowane wyniki sugerują, że lepszymi metodami profilowania są metody oparte na statystyce, jednak łatwo można znaleźć przypadek testowy, który da całkowicie przeciwne rezultaty. Z powodu, iż optymalizacja aplikacji polegała na zmianie definicji funkcji wywoływanej tylko raz i która przed ani po modyfikacji nie wołała żadnych innych funkcji to wielkość zebranych danych dla obydwu aplikacji w przypadku metody modyfikującej kod jest taka sama. Liczba „próbek” zgromadzonych tą metodą to około 4 500 000, co odpowiada liczbie instrukcji call. Natomiast, jako że liczba wygenerowanych próbek metodą statystyczną w przypadku aplikacji niezoptymalizowanej wynosiła 44 000 (1 próbka co milisekundę), w stosunku do 4 000 próbek po zoptymalizowaniu funkcji mnożącej macierze, to liczba zebranych danych spadła około dziewięciokrotnie. Warto też zauważyć, że w tym przypadku wzrost czasu działania aplikacji podczas analizy metodą statystyczną jest dużo większy niż

19

przy aplikacji niezoptymalizowanej. Najsensowniejszym wytłumaczeniem tego faktu jest pewien stały koszt narzutu na analizowaną aplikację.

Oczywiście obie metody wskazały prawidłowe miejsce dużego spadku wydajności. Należy jednak pamiętać, że metoda oparta na modyfikacji kodu aplikacji nie umożliwia identyfikacji sprzętowych miejsc spadku wydajności. Przykładowo, można stwierdzić, że aplikacja spędza dużo czasu w konkretnej funkcji, ale w żaden sposób nie można uzyskać odpowiedzi, czy długi czas działania tej funkcji jest spowodowany np. częstymi chybieniami w pamięci podręcznej procesora. Niemniej trzeba mieć na uwadze to, że narzuty na zasoby systemu powodowane przez metody statystyczne związane są najczęściej z długością pracy analizowanej aplikacji. Z kolei na narzuty wydajnościowe spowodowane metodami modyfikującymi kod wpływ ma głównie ilość skoków do funkcji w czasie wykonywania programu.

Na podstawie otrzymanych wyników można obliczyć przybliżoną liczbę zebranych danych na próbkę. Dla metody statystycznej jest to około 150 bajtów w porównaniu do 70 bajtów w przypadku drugiej z porównywanych metod. Warto również zwrócić uwagę na częstotliwość zapisywania stanu analizowanej aplikacji. Dla metody opartej na upływie czasu została ona skonfigurowana podczas przygotowania aplikacji do profilowania i ustawiona na tysiąc Hz. Częstotliwość otrzymana drugą metodą to w przypadku aplikacji niezoptymalizowanej około sto tysięcy Hz, a po optymalizacji MHz. Maksymalną częstotliwością w opisanym środowisku testowym dla analizy metodą statystyczną było szacunkowo dwadzieścia pięć tysięcy Hz. Visual Studio ostrzegał, że większa częstość przerwań może doprowadzić do niestabilności całego systemu. Dowodzi to, że koszt mierzony wykorzystaniem zasobów przy zebraniu pojedynczej próbki jest dużo większy w przypadku generowania sprzętowych przerwań aniżeli przy wykonywaniu dodatkowych instrukcji. W ostateczności, dla rozpatrywanego przypadku, większy wpływ na wydajność testowanej aplikacji miała metoda modyfikująca kod aplikacji, ze względu na całkowitą ilość zebranych próbek.

3.5 Wady i ograniczenia aplikacji profilujących

Niestety informacje dostarczane przez aplikacje profilujące mogą prowadzić do wyciągnięcia błędnych wniosków. Ich nierzetelność spowodowana jest głównie dużym stopniem skomplikowania dzisiejszych komputerów, kiedy to na wydajność wykonywanego programu ma wpływ bardzo dużo zasobów. Natomiast drugim powodem jest osiągnięcie kompromisu pomiędzy wpływem aplikacji profilujących na działanie analizowanego oprogramowania. Znajomość zagadnień wpływających na działanie profilerów pozwala na zaprojektowanie wiarygodnego środowiska testowego. Poniżej opisanych zostało kilka istotnych przypadków.

Korzystając z metod profilowania opierających się na próbkowaniu wykonywanych instrukcji istnieje możliwość nie zarejestrowania niektórych fragmentów kodu programu, chociaż zostały wykonane. Najprostszym przykładem jest cyklicznie wywoływana funkcja, której moment wykonania zbiega się z momentem zebrania próbki. Mimo, iż funkcja ta może w analizowanej aplikacji wykonywać się jedynie przez ułamek całkowitego czasu wywołania to jest ona rejestrowana w bardzo dużej ilości próbek, co wprowadza w błąd. Problem ten jest w dużym stopniu redukowany dzięki wprowadzeniu zaburzenia okresu próbkowania, który powoduje, że rzeczywisty okres próbkowania dla każdej próbki jest minimalnie inny,

20

natomiast uśredniona wartość wszystkich okresów przybliżona jest do okresu zadanego przez użytkownika.

Kolejnym problemem jest potrzeba działania analizowanych aplikacji. Wiąże się to przede wszystkim ze stratą czasu, która uwidacznia się szczególnie przy aplikacjach mających wiele ścieżek przetwarzania danych, gdyż wykonanie każdego fragmentu kodu może zająć znaczącą ilość czasu. Sytuację pogarsza fakt, iż profilowanie aplikacji jest pewnego rodzaju fazą testów, co wiąże się z częstym (iteratywnym i regresyjnym) wykonywaniem tej czynności. Każda zmiana mająca na celu zwiększenie wydajności powinna być przetestowana po wprowadzeniu. Mniejsze komplikacje związane są z programami charakteryzującymi się przetwarzaniem typowym dla aplikacji badawczych, naukowych czy różnego rodzaju koderów i dekoderów, których działanie polega najczęściej na intensywnym wykonywaniu relatywnie małego fragmentu kodu na dużym zbiorze danych wejściowych. Z wymogiem działania profilowanej aplikacji podczas analizy powiązana jest konieczność przygotowania reprezentatywnych danych wejściowych. Powinny być one jak najbardziej zbliżone do tych z rzeczywistego systemu a równocześnie pokrywać wykonanie jak największej liczby funkcji. Niekiedy zachodzi również potrzeba symulacji działań użytkownika i stosowaniu specjalnych narzędzi potrafiących nagrywać i odtwarzać akcje osoby pracującej z aplikacją. Z drugiej strony należy również pamiętać o tym, iż im dłużej działa profilowana aplikacja tym więcej danych zostaje zebranych, co może chociażby wyczerpać wolne miejsce na dyskach twardych albo zwyczajnie utrudnić ich analizę.

Z poprzedniego akapitu wynika również wymóg przygotowania konfiguracji sprzętowej systemu, na którym uruchamiane zostaną testy wydajności. Niejednokrotnie wiąże się to z dużymi kosztami lub potrzebą wykorzystania środowiska produkcyjnego. Natomiast w przypadku przygotowywania aplikacji dla odbiorców masowych, a co za tym idzie uruchamiania jej na różnych komputerach należy stanąć przed doborem referencyjnej konfiguracji sprzętowej, a być może przygotowania kilku środowisk testowych. Spowodowane jest to, pomimo, że procesory od różnych producentów są zgodne z jedną architekturą to ich mikroarchitektury mogą się znacząco różnić, co pociąga różnice w przepustowości i opóźnieniach podczas wykonywania konkretnych instrukcji.

3.6 Wybrane systemy profilujące i pomocnicze 3.6.1 Program profilujący GNU gprof

GNU gprof został napisany w 1982 roku przez Jay’a Fenlason’a [3]. Do budowania profilu wykorzystuje zarówno techniki statystyczne, jak i modyfikujące kod wykonywalny. Modyfikacja kodu źródłowego aplikacji służy do budowania grafu wywołań funkcji, natomiast do zbierania informacji na temat czasu ich trwania służy próbkowanie stanu aplikacji co określony interwał (najczęściej 100 razy na sekundę). Sposób użycia tego programu jest bardzo szybki i prosty. Można go podzielić na trzy etapy.

Pierwszą czynnością jest skompilowanie i konsolidacja profilowanej aplikacji za pomocą narzędzi z pakietu GNU z dodaną opcją ‘-pg’. Opcja ta odpowiada za skompilowanie aplikacji razem z funkcjami realizującymi zbieranie profilu. Są to między innymi funkcje alokujące przestrzeń na zebrane dane czy rejestrujące graf wywołań funkcji. Przykładowe polecenie kompilacji a następnie konsolidacji może wyglądać następująco:

21

cc -g -c main.c test.c -pg cc -o programTestowy main.o test.o –pg

Nic nie stoi na przeszkodzie, aby powyższe czynności wykonać jednym poleceniem np.: cc -o myprog myprog.c utils.c -g –pg

Dzięki temu aplikacja podczas pracy wygeneruje dodatkowe dane analizowane przez gprof. Wymóg kompilacji i konsolidacji implikuje potrzebę posiadania kodu źródłowego, co czasami może okazać się problemem, chociaż naturalnym wydaje się fakt, iż w celu poprawy aplikacji na podstawie zebranych danych pliki źródłowe i tak są niezbędne.

Tak przygotowaną aplikację trzeba uruchomić podając na wejście dane przygotowane do przetwarzania, które będą sterowały wykonaniem aplikacji. Dobry dobór danych sterujących jest bardzo ważny, gdyż od nich zależy uwypuklenie albo zatuszowanie fragmentów programu powodujących największy spadek wydajności. Zebrane dane zostaną zapisane w katalogu roboczym uruchomionej aplikacji w pliku o nazwie ‘gmon.out’. Podczas tego etapu trzeba mieć na uwadze dwie istotne kwestie. Plik z danymi o profilu zapisze się tylko w sytuacji normalnego zakończenia profilowanej aplikacji, czyli przy wyjściu z funkcji main lub wywołaniu funkcji exit(). Kolejną ważną rzeczą jest fakt pomijania czasu spędzonego na wywołaniach systemowych czy oczekiwaniu na przydzielenie zasobów. Przez to aplikacja, której wydajność spada z powodu zbyt częstego oczekiwania na realizację zadania przez system na wynikach z gproof może stwarzać pozory działającej bardzo szybko. Przykładem jest aplikacja intensywnie wykorzystująca pamięć operacyjną, co skutkuje bardzo częstym wymiataniem stron z fizycznej pamięci do pamięci dyskowej. Proces wymiatania, choć bardzo czasochłonny, nie będzie rejestrowany jako czas działania aplikacji.

Ostatnim krokiem jest uruchomienie gprof w celu analizy zebranych danych. Gprof rozpoznaje kilka opcji, które służą głównie do filtrowania danych, można m.in. pominąć analizę funkcji niezdefiniowanych globalnie lub odrzucić funkcje o konkretnej nazwie. Przykładem polecenia analizującego dane zebrane podczas wykonania programu programTestowy i zapisującego wyjście do pliku profil.txt jest: gprof programTestowy gmon.out > profil.txt

Po wykonaniu takiej komendy w pliku profil.txt znajduje się raport z analizy zebranych danych. Wynik analizy prezentowany jest dwiema metodami – płaskim profilem (ang. flat profile) oraz grafem wywołań (ang. call graph).

Przykładowy sformatowany płaski profil może wyglądać następująco:

Each sample counts as 0.01 seconds. % cumulative self self total s/call name time seconds seconds calls s/call 99.75 12.05 12.05 1 12.05 12.05 multiplyMatrices() 0.25 12.08 0.03 1 0.03 0.03 initializeMatrices() 0.00 12.08 0.00 1 0.00 0.00 saveElapsedTime()

22

Nagłówek tabeli informuje nas o interwale zbierania próbek. W tym przypadku kolejne dane zapisywane są, co 100 milisekund. Każdy wiersz raportu reprezentuje konkretną funkcję natomiast znaczenie poszczególnych kolumn jest następujące:

 % time – jaki procent całkowitego czasu wykonania program spędził w konkretnej funkcji;  cumulative seconds – suma czasów spędzonych w tej funkcji i znajdujących się w wierszach powyżej, z ostatniego wiersza możemy odczytać przybliżony czas pracy całego programu;  self seconds – liczba sekund spędzonych w odpowiadającej funkcji;  self calls – liczba wywołań funkcji;  self ms/call – średni czas spędzony w ciele funkcji z pominięciem czasu działania funkcji przez nią wywoływanych;  total ms/call – średni czas wykonania funkcji wraz z czasem wywołania funkcji przez nią wywoływanych;  name – nazwa funkcji, do której odnosi się analizowany wiersz tabeli;

Chociaż nie wszystkie wyniki profilowania będą tak łatwe do oceny, to na podstawie powyższego raportu od razu można określić, że funkcją wymagającą optymalizacji jest multiplyMatrices(). Czas w niej spędzony i tylko w niej (jak widać wartość kolumny total s/call równa się wartości w kolumnie self seconds) to aż ponad 99 procent czasu działania całej aplikacji.

Przykładowy sformatowany graf wywołań może wyglądać następująco:

index % time self children called name

[1] 100.0 0.00 12.08 main [1] 12.05 0.00 1/1 multiplyMatrices() [2] 0.03 0.00 1/1 initializeMatrices() [3] 0.00 0.00 1/1 saveElapsedTime() [5] 12.05 0.00 1/1 main [1] [2] 99.8 12.05 0.00 1 multiplyMatrices() [2] 0.03 0.00 1/1 main [1] [3] 0.02 0.03 0.00 1 initializeMatrices() [3] 0.00 0.00 1/1 main [1] [5] 0.0 0.00 0.00 1 saveElapsedTime() [5]

Poszczególne rekordy powyższej tabeli posortowane są według ilości czasu spędzonego w danej funkcji – od wartości największych do najmniejszych. Każda funkcja identyfikowana jest przez indeks z pierwszej kolumny. Nad wierszem, w którym występuje indeks wymienione są funkcje wywołujące, natomiast wiersze poniżej zawierają funkcje wywoływane. Dzięki temu przedstawiony został graf zależności rodzice-dzieci. Sposób ten jest bardzo prosty w zapisie, ale niestety w przypadku wielu zależności pomiędzy funkcjami, może stać się trudny do zinterpretowania. Druga z kolumn zawiera stosunek liczby czasu

23

spędzonego w odpowiadającej funkcji(wraz z funkcjami wołanymi) do całkowitego czasu. Kolumna o nagłówku ‘self’ informuje o liczbie sekund spędzonych na wykonywaniu funkcji, natomiast kolumna ‘children’ niesie informację o tym ile czasu zajęło wykonanie funkcji wywoływanych przez rozpatrywaną funkcję. Z ostatniej informacji można odczytać liczbę wywołań funkcji, w przypadku dwóch liczba oddzielonych ukośnikiem, pierwsza z nich informuje o ilości wywołań funkcji przez wymienionego rodzica, natomiast druga jest całkowitą liczbą wywołań. W przypadku rekursyjnego wykonania funkcji ukośnik będzie zastąpiony znakiem plus, który będzie oddzielał ilość wywołań nierekurencyjnych od rekurencyjnych.

Podsumowując gprof jest wartym uwagi programem profilującym. Do jego głównych zalet należy duża dostępność, brak opłat, mały narzut na wydajność profilowanej aplikacji. Podczas wykonanych pomiarów czas wykonania aplikacji skompilowanej z opcją ‘-pg’ wydłużał się zaledwie o około półtora procent w porównaniu do kompilacji bez tej opcji. Główną wadą tego narzędzia jest ubogi zbiór funkcji, a także sposób prezentacji wyników, który może być ciężki do analizy przez użytkownika.

3.6.2 Performance Inspector

Pakiet narzędziowy Performance Inspector składa się z kilkunastu aplikacji o dość wąskiej funkcjonalności. Jego strona domowa to http://perfinsp.sourceforge.net/index.html. Dostępny jest zarówno w wersji skompilowanej, a także w formie kodu źródłowego. Platformami przeznaczenia jest MS Windows, , AIX oraz ZOS. Jest on dostępny bezpłatnie, co jest jego ogromną zaletą. Głównym przeznaczeniem tego oprogramowania jest monitorowanie oraz diagnozowanie wydajności aplikacji napisanych w Javie, chociaż wiele z narzędzi tego pakietu umożliwia analizę aplikacji wykonanych w innych językach programowania. Pakiet ten jest najprawdopodobniej nierozwijany od około 2010 roku. Teza ta została wysnuta na podstawie najnowszych znalezionych zmian w kodzie źródłowym.

Poniżej przedstawiona została lista, wraz z krótkim opisem, kilku najciekawszych narzędzi z opisywanego pakietu. Oprócz bezpośredniego wykorzystania dostępnych narzędzi pakiet ten umożliwia współpracę za pośrednictwem interfejsu programistycznego. Fakt ten czyni go niezwykle uniwersalnym. Wybór był całkowicie subiektywny – nastawiony głównie na tematykę tej pracy.

 CPI – obliczanie liczby wykonanych instrukcji w czasie jednego cyklu zegarowego, może posłużyć, jako prosty benchmark całego systemu;  MPEVT – umożliwia odczyt i konfigurację sprzętowych liczników wydajnościowych, automatycznie rozpoznaje typ procesora w tym AMD Phenom 955, który miał premierę pod koniec marca 2008 roku;  MSR – przy użyciu tej aplikacji można odczytywać oraz zapisywać sprzętowe rejestry procesora, tak samo jak poprzednia aplikacje rozpoznaje ona dostępne rejestry w zależności od zainstalowanego modelu procesora;  PERFUTIL – jest to implementacja sterownika systemowego, na którym oparta jest większość programów z tego pakietu, udostępnia on również API w języku C oraz Java;  PTT – dostarcza informacji o czasie, jaki zajmowały wątki uruchomione w systemie;

24

 SWTRACE – program profilujący na podstawie próbek pobieranych co jednostkę czasu lub zdarzenie sprzętowe;

25

4 AMD CodeAnalyst

CodeAnalyst (w skrócie CA) został stworzony przez pracowników firmy AMD. Pierwsza wydanie ukazało się w 2003 roku. Przeznaczony jest do analizy instrukcji na maszyny oparte o architekturę x86 i jej rozszerzenia, a więc również jej 64 bitową wersję. Jest to aplikacja bardzo elastyczna, oferuje użytkownikowi kilka sposobów użycia. Są nimi współpraca przez interfejs graficzny, wykorzystanie linii komend, integracja z MS Visual Studio lub własne zaprogramowanie działania przy użyciu dostarczonego API. Bardzo istotną, a może najistotniejszą zaletą tego programu profilującego jest jego darmowe rozprowadzanie i użytkowanie. CodeAnalyst wykorzystuje elementy mikroarchitektur procesorów firmy AMD, z tego powodu jego możliwości na systemach opartych na procesorach takich producentów jak Intel czy VIA są ograniczone do możliwości korzystania z podstawowego trybu pracy. Dedykowanie CA do procesorów AMD jest działaniem marketingowym, mającym na celu zachęcenie twórców oprogramowania do wykorzystywania sprzętu firmy AMD. Dla porównania, koszt licencji komercyjnego profilera VTune firmy Intel wynosi około 900 dolarów.

Z CodeAnalyst można korzystać na komputerach z systemami operacyjnymi Windows lub Linuks. Interfejs użytkownika i sposób użycia w obu przypadkach jest praktycznie identyczny, tylko nieznacznie różnią się zbiory dostępnych funkcjonalności. Inaczej realizowany jest również dostęp do zasobów sprzętowych komputera: w systemach Windows wykorzystywany jest sterownik napisany przez programistów CodeAnalyst, natomiast pod Linuksem danych z warstwy sprzętowej dostarcza otwarto-źródłowe narzędzie Oprofile. Można pokusić się o stwierdzenie, że wersja dla systemów Linuks jest bardziej uboga i traktowana trochę po macoszemu.

Wśród najważniejszych funkcjonalności CodeAnalyst można wymienić dostęp do wielu trybów zbierania danych, jednak każdy z nich oparty jest na metodach statystycznych. Z tego względu przy pracy z tym oprogramowaniem trzeba mieć na uwadze wszystkie wady i zalety, jakie niosą za sobą profilery korzystające ze statystycznej analizy. Chociaż wszystkie zbierane dane są instrukcjami w języku assemblera i język, w jakim napisany jest kod źródłowy analizowanej aplikacji nie ma znaczenia dla możliwości ich kolekcji, to jest on istotny przy dalszej analizie zebranych danych. Wpieranymi przez CodeAnalyst językami programowania są C/C++, Fortran, Java, a także oparte o platformę .Net. Możliwa jest również analiza aplikacji wykorzystujących standard OpenCL, czyli przeznaczonych na systemy heterogeniczne. Takie systemy budowane są z wielu różnego rodzaju jednostek obliczeniowych takich jak CPU, GPU i inne. Popularność takich systemów ostatnimi czasy bardzo szybko wzrasta głównie dzięki ogromnym możliwościom potokowego i równoległego przetwarzania danych przez procesory graficzne. Do wydajnych zastosowań GPU należy zaliczyć takie problemy jak kryptografia, kompresja, symulacje fizyczne oraz obliczenia inżynierskie, a także sztuczna inteligencja i oczywiście efekty graficzne. Wykorzystanie w analizowanej aplikacji jednego ze wspieranych języków pozwala na użycie symboli debugowania do skojarzenia instrukcji assemblera z instrukcjami w kodzie źródłowym.

Zbieranie danych przez CodeAnalyst nie ogranicza się do jednej określonej aplikacji, ale może być wykonywane dla całego systemu, a więc zarówno innego konkurencyjnego oprogramowania i współdzielonych bibliotek, jak również całej przestrzeni jądra. Taki sposób zbierania danych może być niepożądany, głównie z powodu ilości zbieranych danych, dlatego

26

istnieje możliwość zawężenia go do wybranych procesów. Obserwowanie jednego procesu pociąga za sobą obserwację wszystkich uruchomionych przez niego wątków, a także wszystkich procesów potomnych.

Do jednych z najważniejszych zalet CodeAnalyst należy sposób prezentacji informacji w interfejsie użytkownika. Wyniki analizy można przeglądać na różnych poziomach filtrowania takich jak poszczególne procesy, bloki, funkcje, linie kodu źródłowego, czy najbardziej szczegółowy poziom, czyli instrukcje maszynowe. Praca z tą aplikacją oparta jest na sesjach, dzięki czemu dane są wygodnie uporządkowane i w szybki sposób można powtórzyć wcześniej wykonany test. Konfiguracja sesji pozwala również na zdefiniowanie momentu startu i zakończenia profilowania, jak również czasowego zatrzymania kolekcji danych. Niektóre z informacji prezentowane są w postaci wykresów i dzięki temu, wysunięcie wniosków przez użytkownika czasami sprowadza się do rzucenia kątem oka. Jednak największą i nieocenienie pomocną wartością interfejsu użytkownika jest powiązanie zebranych danych z liniami kodu źródłowego na podstawie symboli debugowania.

Oprócz analizy i agregacji danych przez interfejs użytkownika i prezentowania informacji na temat profilowanego systemu w postaci wykresów i tabel możliwy jest również eksport danych do innych formatów i wykorzystanie ich według potrzeb. Wśród tych formatów są pliki CSV, XML, czy struktury danych dostępne przez wbudowane API.

Programiści aplikacji działających na platformach wielordzeniowych lub wieloprocesorowych docenią wsparcie, jakie niesie ze sobą CodeAnalyst. Podczas konfiguracji sesji istnieje możliwość jawnego zdefiniowania numeru rdzenia, na którym ma się wykonywać profilowana aplikacja, co może ułatwić lepsze odwzorowanie docelowego środowiska pracy lub testowanie skalowalności. W czasie zbierania danych możliwy jest zapis stanu wykorzystania zasobów procesora i pamięci operacyjnej przez poszczególne procesy. Oprócz wspomnianych platform wieloprocesorowych CodeAnalyst ułatwia również analizę architektur NUMA, które to składają się z wielu procesorów korzystających ze wspólnej przestrzeni adresowej jednak podzielonej na lokalną i zdalną pamięć operacyjną.

Kolejną zaletą okazuje się prowadzone oficjalne forum wsparcia technicznego. Chociaż społeczność na tym forum jest stosunkowo niewielka to w przypadku problemów można dostać bezpośrednią odpowiedź od twórców CodeAnalyst. Na łamach tego forum zgłaszane są również błędy w oprogramowaniu, które są szybko poprawiane w następnych wydaniach. Do wykorzystanie opisywanego profilera zachęca też fakt ciągłego rozwoju i dość regularnego wydawania nowej wersji – zwykle, co 3, 4 miesiące. Dzięki tak częstym aktualizacjom wsparcie dla nowych rodzin procesorów jest bardzo szybkie. Jako plus trzeba traktować także wielkość zainstalowanego oprogramowania, które nie przekracza 100 megabajtów, i wykorzystanie biblioteki QT, co powoduje, że interfejs graficzny ma dużą responsywność, a jednocześnie używa stosunkowo niewiele zasobów systemowych.

4.1 Typy analizy

CodeAnalyst udostępnia trzy sposoby analizy aplikacji. Każdy z nich wykorzystuje inną technikę zbierania danych. Tryby te są niezamienne i wyniki uzyskane za pomocą różnych trybów nie powinny być porównywane, gdyż może to prowadzić do błędnych wniosków. Wszystkie tryby kolekcjonują (próbkują) dane co pewien interwał określony pewnymi zdarzeniami, których zbiór jest dla każdego trybu inny. Dostępność poszczególnych typów

27

analizy jak i zdarzeń z nimi związanych jest zależna od rodziny mikroprocesora, na którym przeprowadzane jest budowanie profilu.

4.1.1 Próbkowanie czasowe

Tryb TBP (ang. Time Based Profiling – próbkowanie oparte na interwałach czasowych) służy do podstawowej analizy aplikacji i znajdowaniu miejsc krytycznych. Przerwania generowane są na podstawie upływającego czasu, minimalny okres przerwania to 100 mikrosekund dla systemów wyposażonych w kontroler APIC. Do pracy zużywa minimum zasobów systemu, gdyż obsługa przerwania polega jedynie na zapisaniu adresu aktualnie wykonywanej instrukcji oraz identyfikatora procesu i wątku, do którego należy instrukcja. Obszary krytyczne aplikacji rozpoznawane są na podstawie liczby zebranych próbek im jest ich więcej tym bardziej prawdopodobne, że dana instrukcja ma duży wpływ na wydajność. Jest to jedyny tryb, który działa na procesorach niewyprodukowanych przez AMD.

4.1.2 Próbkowanie zdarzeniowe

W trybie EBP (ang. Event Based Profiling – próbkowanie oparte na zdarzeniach) aplikacja CodeAnalyst korzysta z liczników wydajnościowych wbudowanych w procesory. Przerwania generowane są co określoną liczbę konfigurowalnych zdarzeń. Najczęściej dostępne są cztery liczniki zdarzeń na każdy z rdzeni procesora. Do zapisywanych danych, oprócz identyfikatora procesu i adresu instrukcji, należy również nazwa zdarzenia, które wygenerowało przerwanie. Zdarzeniami, które mogą być rejestrowane przez liczniki są między innymi: cykle CPU, zatwierdzone instrukcje, trafienia pamięci podręcznej, chybienia pamięci podręcznej, błędne przewidzenie rozgałęzień itd. Dla rodziny procesorów AMD 10h dostępnych zdarzeń jest ponad 100 [9]. Jest to zarówno wada jak i zaleta. Początkujący użytkownik może czuć się przygnieciony przez problem wyboru 4 zdarzeń z ponad setki. Na szczęście wraz z instalacją konfigurowanych jest kilka przykładowych sesji, co ułatwia pierwsze kroki. Analiza EBP niesie za sobą większe narzuty na wydajność w stosunku do TBP, ale daje nam informację o przyczynie słabej wydajności w konkretnym miejscu aplikacji. Przykładowo, duża liczba chybień do bufora pamięci podręcznej może znaczyć o złym schemacie dostępu do danych.

Idea buforów pamięci podręcznej opiera się na zasadzie lokalności odwołań. Według tej zasady w danym momencie aplikacja wykorzystuje tylko określony, niewielki zbiór danych i instrukcji przechowywanych w pamięci operacyjnych. Dlatego jeśli zbiór ten w całości zmieści się w buforze pamięci podręcznej uzyskany jest wzrost wydajność dzięki wyeliminowaniu potrzeby odwołań do pamięci. Z powodu konstrukcji buforów podręcznych lepsze efekty daje wykorzystywanie struktur danych o liniowym rozkładzie w pamięci, takich jak tablice, niż losowo rozrzuconych w przestrzeni adresowej jak to jest w przypadku np. list. Implementacje buforów przewidują dostęp do kolejnych danych i starają się je sprowadzić z pamięci zanim dostaną żądanie od procesora. Jest to bardzo pożyteczna cecha, jednak na przykład dostęp do elementu tablicy od największego do najmniejszego indeksu uniemożliwia prawidłowe działanie tego mechanizmu, gdyż przewiduje on dostęp w przeciwnym kierunku. Istnieją grupy algorytmów, które koncentrują się na optymalnym wykorzystaniu pamięci cache, można ich szukać pod angielskimi hasłami cache-friendly algorithms oraz cache- oblivious algorithms.

Kolejna częsta przyczyna spadku wydajności aplikacji leży w częstym anulowaniu instrukcji wykonywanych spekulatywnie. Spowodowane jest to błędnym przewidzeniem

28

przez procesor ścieżki instrukcji, którą podąży aplikacja. Duża długość potoków procesora pociąga za sobą bardzo duże opóźnienia w przypadku zmiany przepływu programu. Zmiana przepływu programu to wszystkie instrukcje warunkowe takie jak if-then-else, switch oraz pętle. W momencie, gdy jednostka wykonawcza napotka w kodzie wykonalnym rozgałęzienie to, aby nie tracić czasu, obiera jedną ze ścieżek przepływu i wykonuje pobrane z niej instrukcje. Wybór ścieżki realizowany jest przez algorytmy predykcji rozgałęzień, których wynik oparty jest na charakterze wykonania poprzednich rozgałęzień. W przypadku, gdy ścieżka zostanie źle przewidziana to wszystkie wykonane instrukcje występujące logicznie po instrukcji warunkowej muszą zostać anulowane. Sytuacja taka prowadzi do unieważnienia całego potoku wykonawczego. Tego rodzaju opóźnienia w zależności od rodzaju procesora trwają od 15 do 30 cykli zegara systemowego. Takich kłopotów można uniknąć starając się jak najlepiej zaprojektować wszystkie instrukcje warunkowe w taki sposób, aby były jak najlepiej przewidywane przez wewnętrzne mechanizmy procesora. Inżynierowie implementują wiele algorytmów predykcji w mikroprocesorach, dzięki czemu można przyjąć następujące zasady:

 ścieżka, która zawsze podąża w tym samym kierunku jest dobrze przewidywana;  rozgałęzienie, które przez dłuższy okres podąża jednym torem jest dobrze przewidywane, oprócz sytuacji, kiedy są zmiany toru;  instrukcja warunkowa wielokrotnie wykonująca się w ten sam sposób, a następnie wielokrotnie dająca przeciwny rezultat jest dobrze przewidywana oprócz momentu zmiany kierunku;  również proste wzorce typu: raz w jednym kierunku dwa razy w przeciwnym, czy na przemian to w jednym to w drugim są dobrze przewidywane  najgorsze wyniki są przy spekulowaniu instrukcji, które nie charakteryzują się żadnym wzorcem, są wykonywane losowo, albo wzorzec jest zbyt skomplikowany, wtedy prawdopodobne jest, że za każdym razem przewidywania będą błędne;  słabo zgadywana jest również ścieżka wykonania zagnieżdżonych pętli lub pętli zawierających instrukcje warunkowe;

4.1.3 Próbkowanie po instrukcjach

Dane zbierane podczas analizy IBS (ang. Instruction Based Sampling – próbkowanie oparte na instrukcjach) są bardzo podobne do tych z trybu EBP, jednak sposób ich kolekcjonowania jest znacząco inny i wyniki uzyskane w tych dwóch typach analizy nie powinny być porównywane. Moment przerwania generowany jest na podstawie liczby instrukcji assemblera. Z powodu rozdzielenia potoku w procesorach AMD na fazę pobrania oraz wykonania, instrukcjami znakowanymi do śledzenia mogą być instrukcje pobrane do dekodera lub mikroinstrukcje trafiające do jednostki wykonawczej. Po oznaczeniu instrukcja jest dokładnie monitorowana i odnotowywane są wszystkie zdarzenia, które spowodowała. Niektóre z tych zdarzeń są dokładnie opisane w następnej części pracy, a kompletny opis można znaleźć w dokumencie AMD pt. BIOS and Kernel Developer Guide [9]. Wśród zdarzeń związanych z fazą pobrania należą m.in. chybienia w buforze translacji stron lub w buforze pamięci podręcznej instrukcji, natomiast dla fazy wykonania są to liczba cykli od wykonania do zatwierdzenia, czy liczba nieprzewidzianych rozgałęzień i inne (w przypadku

29

procesorów z rodziny 10h ponad 50 [9]). Analiza tego typu stanowić więc może bardzo wartościowe źródło informacji niedostępnych dla innych systemów profilujących.

4.2 Funkcjonalności dodatkowe.

Oprócz wspomnianych trybów analizy, które są niejako fundamentami opisywanego oprogramowania, użytkownik może wykorzystać, także dodatkowe opcje.

4.2.1 Przeglądanie stosu wywołań

Przeglądanie stosu (CSS – ang. Call Stack Sampling) – to opcja, którą można włączyć podczas zbierania próbek w każdym z uprzednio wymienionych trybów. Dostarcza ona statystycznych informacji o relacji pomiędzy funkcjami wołającymi i wołanymi. Dzięki temu można wyciągnąć wnioski, które z funkcji są najczęściej wykonywane. Zasada działania polega na przeglądaniu stosu podczas zbierania próbek. Ze stosu odczytywane są adresy funkcji powrotu do pewnej określonej liczby, zdefiniowanej jako głębokość rozwijania. W ramach konfiguracji ustawiany jest również interwał próbek, co który ma być zbierana informacja o stosie.

4.2.2 Wykres współbieżności

Dla aplikacji wielowątkowych istnieje możliwość wygenerowania wykresu ze sposobem szeregowania wątków przez system operacyjny (ang. Profile). Pozwala to na określenie poziomu wykorzystania dostępnej mocy obliczeniowej, a także na wykrycie ewentualnych problemów z długą synchronizacją. Istotne dla wydajności może być również wykonywanie wątku zawsze na tym samym rdzeniu, dzięki czemu lepsze będzie wykorzystanie pamięci podręcznej mikroprocesora oraz opóźnień synchronizacji międzywątkowej. W przypadku analizowania aplikacji uruchomionych na systemach w architekturach NUMA generowana jest również tabela z ilością odwołań do pamięci nielokalnej.

4.2.3 Wykorzystanie zasobów systemowych

CodeAnalyst umożliwia także monitorowanie wykorzystania czasu procesora oraz pamięci operacyjnej. Informacje prezentowane są w postaci wykresów czasowych dla całej sesji profilowania. Wspierana jest również analiza wykorzystania pamięci podręcznej procesora na podstawie ilości dokonanych odczytów lub zapisów linii buforu przed jego usunięciem. Poziom użycia buforów może być przeglądany w podziale na moduły, funkcje lub instrukcje.

4.3 Wpływ profilowania na wydajność aplikacji

Wykorzystanie aplikacji profilujących wiąże się z wprowadzeniem swoistego zaburzenia do działania testowanego systemu. Głównymi zasobami konsumowanymi przez CodeAnalyst jest czas procesora i narzut związany z przełączaniem wątków w momencie obsługi przerwania oraz operacje dyskowe, podczas których zapisywane są zbierane dane. Z racji tego, że każdy z trybów pracy opisywanej aplikacji profilującej wykorzystuje inne techniki kolekcjonowania danych, pociąga to za sobą różny współczynnik wpływu na wydajność.

30

Na poniższych wykresach przedstawiony został procentowy wzrost czasu wykonywania programu podczas sesji profilowania. Wyniki są uśrednione z trzech pomiarów. Aplikacja przeprowadza operację mnożenia macierzy metodą standardową i korzysta tylko z jednego wątku. Podczas testów zadbano, aby aplikacja zawsze wykonywała się na tym samym rdzeniu procesora. Test przeprowadzono na komputerze z konfiguracją:

 procesor - AMD Phenom X4 955 3,2 GHz,  pamięć operacyjna – Kingston 8 GB DDR3 HyperX 1600 MHz.

Dane prezentowane są dla czterech częstotliwości zbierania próbek (Rys. 3, Rys. 4, Rys. 5). Oznaczone są one literą f i należy je rozumieć następująco:

 0.5f – częstotliwość próbkowania zmniejszona o połowę w stosunku do domyślnie ustawionej w AMD CodeAnalyst;  f – częstotliwość próbkowania ustawiona na domyślną;  2f – częstotliwość próbkowania zwiększona dwukrotnie w odniesieniu do domyślnej;  4f – częstotliwość próbkowania zwiększona czterokrotnie w porównaniu do domyślnej;

Porównano wszystkie trzy dostępne tryby profilowania z różną konfiguracją opcji rozwijania stosu (odpowiednio z wyłączoną opcją CSS – Rys. 3, z włączoną opcją CSS o głębokości 10 i interwale 10 –Rys. 4 i włączonym CSS o głębokości 50 i interwale 1 - Rys. 5). Dla odniesienia, wykonanie testowanej aplikacji bez budowania profilu trwa około 15 sekund.

Wyłączona opcja CSS 7

6

5

4

3

2 Spadek wydajności[%] 1

0 0.5f f 2f 4f TBP 0,27 0,27 0,27 0,54 EBP 1,35 1,89 3,65 6,23 IBS 0,54 1,21 2,3 4,2

Rys. 3 Wykres przedstawiający procentowy wzrost czasu wykonywania programu podczas sesji profilujących bez korzystania z opcji Call Stack Sampling

31

CSS głębokość 10, interwał 10 30

25

20

15

10

Spadek wydajności[%] 5

0 0.5f f 2f 4f TBP 0,27 0,54 1,76 2,3 EBP 2,57 13,67 16,65 25,85 IBS 3,92 8,53 15,56 25,71

Rys. 4 Wykres przedstawiający procentowy wzrost czasu wykonywania programu podczas sesji profilujących z wykorzystaniem opcji Call Stack Sampling, rozwijającej stos do głębokości 10 wywołań funckji

CSS głębokość 50, interwał 1 160 140 120 100 80 60 40

Spadek wydajności[%] 20 0 0.5f f 2f 4f TBP 3,92 10,56 12,59 31,53 EBP 7,58 14,75 149,53 467,13 IBS 5,14 20,57 137,21 292,03

Rys. 5 Wykres przedstawiający procentowy wzrost czasu wykonywania programu podczas sesji profilujących z wykorzystaniem opcji Call Stack Sampling, rozwijającej stos do głębokości 50 wywołań funckji

Z wykresów na Rys. 3, Rys. 4 i Rys. 5 widoczna jest wyraźna degradacja wydajności z powodu źle dobranych okresów próbkowania. O ile w przypadku nie zbierania danych o stosie wywołań funkcji narzuty nie przekraczają 10% i są akceptowalne, to przy częstym rozwijaniu stosu do głębokości 50 funkcji aplikacja zwalnia drastycznie. Wyraźnie widać

32

również, że najmniejsze narzuty na wydajność generuje tryb TBP. Kwestię spadku wydajności należy mieć na uwadze z dwóch powodów. Po pierwsze, tak zebrane dane mogą źle obrazować rzeczywistą pracę aplikacji w sytuacji, gdy nie działa program profilujący. Kolejnym problemem jest to, iż w ekstremalnych przypadkach podczas tworzenia profilu możemy doprowadzić do całkowitego zaburzenia działania aplikacji doprowadzając do załamania całego systemu informatycznego. Jest to szczególnie ważne, gdy działamy na systemie produkcyjnym.

4.4 Informacje dostarczane przez CodeAnalyst

Ilość monitorowanych zdarzeń udostępniana przez interfejs CodeAnalyst jest bardzo duża. Przez taki natłok informacji czasami ciężko dokonać wyboru, którym aspektom bliżej się przyjrzeć tak, aby stosunek włożonego wysiłku do otrzymanych efektów był jak najlepszy. Oprócz tego brak lub mała znajomość budowy mikroprocesorów komplikuje wyciągnięcie z otrzymanych danych prawidłowej wiedzy. Poniżej opisano kilka, subiektywnie wybranych zdarzeń dla rodziny procesorów AMD 10h, a także zasugerowano, co może być przyczyną ich występowania. Przy wyciąganiu wniosków cały czas należy pamiętać, iż informacje prezentowane przez CodeAnalyst są zebrane metodami statystycznymi, przez co ilość zdarzeń jest jedynie przybliżeniem faktycznego stanu. Wiele z dostępnych zdarzeń posiada możliwość dodatkowej konfiguracji np. czy dane mają być zbierane tylko w przestrzeni użytkownika, jądra czy w obydwu przypadkach, a także wiele innych. Zdarzenia posegregowane zostały w grupy, których każda dotyczy innego elementu wykonawczego. Ma to na celu wprowadzenie większej przejrzystości. W nawiasach podano angielskie nazwy oraz numery zdarzeń w celu ich łatwiejszego odnalezienia w interfejsie CodeAnalyst i oficjalnej dokumentacji.

W trybie EBP istnieje ograniczenie na ilość rejestrowanych zdarzeń podczas jednego wykonania profilu. Spowodowane to jest skończoną ilością liczników wydajnościowych przypisanych dla każdego z rdzeni. Problem ten jest wyeliminowany w metodzie IBS. Natomiast zbiór zdarzeń zbieranych przez obydwie metody jest identyczny. Jedyną różnicą jest to, iż w trybie IBS domyślnie zbierane są wszystkie dostępne zdarzenia, natomiast przy wykorzystaniu metody EBP w gestii użytkownika leży wybranie interesujących go zdarzeń. Dokładnego opisu i sposobu konfiguracji wszystkich dostępnych zdarzeń należy szukać w dokumentacji AMD przeznaczonej dla twórców oprogramowania systemowego [9].

4.4.1 Przepustowość instrukcji

Do pomiaru przepustowości instrukcji (ang. Instruction throughput) służą wartości dwóch zdarzeń:

 Liczba cyklów zegara (0x76 - ang. CPU clocks not halted) – nie uwzględniane są cykle, w których procesor został wprowadzony przez system operacyjny w stan bezczynności np. za pomocą instrukcji HALT.  Liczba zatwierdzonych instrukcji (0xC0 - ang. retired instructions) - liczba instrukcji wykonanych i zatwierdzonych w ostatnim etapie potoku. Nie wliczają się w to instrukcje, których przetwarzanie zostało rozpoczęte, ale przerwane na przykład z powodu błędnego przewidzenia rozgałęzienia.

Obydwa zdarzenia są bardzo częste: na przykład dla procesora o częstotliwości 3GHz liczba zajść pierwszego zdarzenia to około 3 miliardy razy na sekundę. Z tego powodu należy

33

ustalić odpowiednio duży interwał. Stosunek dwóch liczb wymienionych powyżej nazwany jest liczbą instrukcji na cykl (ang. Instructions Per Cycle) i służy do ogólnej oceny wydajności funkcji, modułów lub większych fragmentów kodu oraz do obserwacji postępów w optymalizacji. Zasadą jest, że im większa jest wartość tego współczynnika tym większa jest wydajność systemu. Dla współczesnych systemów dobrym wynikiem jest jedna wykonana instrukcja na cykl zegara. Poza tym duże zagęszczenie zdarzeń związanych z liczbą cykli zegara systemowego dla konkretnego fragmentu kodu świadczy o długim czasie jego wykonania. Natomiast duża wartość liczby zatwierdzonych instrukcji wskazuje często wykonywane fragmenty kodu.

4.4.2 Przepustowość pamięci systemowej

Niżej opisane zdarzenia służą do oceny wykorzystania pamięci systemowej a w szczególności pozwalają na wskazanie miejsc intensywnej wymiany danych pomiędzy pamięcią operacyjną a pamięcią procesora.

 Liczba cyklów zegara (0x76 - ang. CPU clocks not halted) – zdarzenie opisane w poprzednim paragrafie 4.4.1.  Odczyty z pamięci operacyjnej (0x6C - ang. response from system on cache refills) – każde zajście zdarzenia to pobranie linii pamięci podręcznej procesora z pamięci operacyjnej. W przypadku procesorów z rodziny 10h są to 64 bajty. Mnożąc tę wartość przez interwał i rozmiar linii otrzymujemy ilość przesłanych danych z pamięci do procesora podczas czasu, który zmierzony został przez poprzednie zdarzenie.  Zapisy do pamięci operacyjnej (0x6D - ang. octwords written to system) – zdarzenie zapisu do pamięci operacyjnej 16 bajtów danych. Wykonując analogiczne obliczenia jak w poprzednim punkcie otrzymujemy ilość danych przesłanych z pamięci procesora do pamięci operacyjnej.  Liczba dostępów do pamięci operacyjnej (0xE0 - ang. DRAM accesses) – liczba ta może być większa od sumy poprzednich dwóch zdarzeń z tego względu, że zawiera również żądania kontrolera związane z ustawieniem dostępu do odpowiednich komórek pamięci.

4.4.3 Charakterystyka dostępu do pamięci danych

Z powodu dużej różnicy w czasie dostępu do danych w pamięci operacyjnej a szybkością przetwarzania tych danych w procesorze, niezwykle istotnym aspektem dla wydajności jest tzw. lokalność odwołań, a co za tym idzie, efektywność działania pamięci podręcznej procesora. Zasada lokalności odwołań zakłada, że w pewnym okresie czasu, program korzysta z ograniczonego niewielkiego zbioru danych. To, że zbiór ten jest określany jako niewielki zwiększa prawdopodobieństwo, że zmieści się on w pamięciach podręcznych mikroprocesora, które są wielokrotnie szybsze od pamięci operacyjnej. W celu określenia charakteru lokalności odwołań należy wykorzystać poniższe zdarzenia:

 Liczba zatwierdzonych instrukcji (0xC0 - ang. retired instructions) – zdarzenie opisane w paragrafie 4.4.1.

34

 Liczba dostępów do buforu danych pierwszego poziomu (0x40 - ang. data cache accesses) – licznik ten rejestruje każde odwołanie jednostek mikroprocesora do pamięci danych.  Liczba odwołań do buforów L2 (0x42 - ang. data cache refills from L2) – informuje, jaka ilość odwołań chybiła w pamięci danych pierwszego poziomu, ale trafiła w pamięci drugiego poziomu.  Liczba odwołań do pamięci operacyjnej (0x43 - ang. data cache refills from system) - liczba linii pamięci podręcznej, które zostały bezpośrednio pobrane z pamięci operacyjnej lub buforów poziomu L3.

Ogólna liczba chybień dostępu do pamięci w buforze L1 równa się sumie liczby dwóch ostatnich zdarzeń. Dzieląc liczbę dostępów do pamięci przez liczbę wykonanych instrukcji można wyciągnąć wniosek na temat zapotrzebowania aplikacji na dane. Bardziej pomocny jest stosunek ogólnej liczby chybień w buforze L1 do liczby wszystkich instrukcji oraz liczba chybień w buforze L1 do liczby żądań pobrania danych. Im uzyskana z tego dzielenia liczba jest mniejsza tym lepszą wydajność ma aplikacja. Sensowne jest też porównanie liczby żądań obsługiwanych przez pamięć operacyjną w stosunku do ogólnej liczby instrukcji. Zakładając, iż odwołanie do pamięci operacyjnej trwa około 100 cykli zegarowych, a przepustowość procesorów to około 1 instrukcji na cykl, to każde takie odwołanie to strata czasu, w którym można było wykonać ok. 100 instrukcji.

Mało wydajne wykorzystanie pamięci podręcznej procesora może być spowodowane głównie przez jej zbyt mały rozmiar w stosunku do lokalnego zbioru danych lub nachodzenia na siebie adresów, pod którymi składowane są dane. Należy również pamiętać o tym, że podczas pierwszego dostępu do pewnej danej również nastąpi chybienie w pamięci podręcznej. Stara się to być niwelowane przez mechanizmy mikroprocesora, które zakładają, że istnieje duże prawdopodobieństwo, iż celem żądania stanie się obszar pamięci znajdujący się obok poprzednio odczytywanego, a co za tym idzie sprowadzają one te dane przed ewentualnym odwołaniem.

4.4.4 Charakterystyka dostępu do pamięci instrukcji

Typ zdarzeń w tym punkcie jest bardzo podobny do poprzednio opisywanych i zasada oceny wydajności jest taka sama jak w paragrafie wyżej. Buforowanie instrukcji z pamięci operacyjnej jest niemniej ważne niż danych. Do pomiaru wydajności służą następujące zdarzenia:

 Liczba zatwierdzonych instrukcji (0xC0 - ang. retired instructions) – zdarzenie opisane w paragrafie 4.4.1.  Liczba dostępów do buforu instrukcji pierwszego poziomu (0x80 - ang. instruction cache fetches) – licznik ten rejestruje każde odwołanie jednostek mikroprocesora do pamięci instrukcji.  Liczba odwołań do buforów L2 (0x82 - ang. instruction cache refills from L2) – informuje, jaka ilość odwołań chybiła w pamięci instrukcji pierwszego poziomu, ale trafiła w pamięci drugiego poziomu.  Liczba odwołań do pamięci operacyjnej (0x83 - ang. instruction cache refills from system) - liczba linii pamięci podręcznej, które zostały bezpośrednio pobrane z pamięci operacyjnej lub buforów poziomu L3.

35

Powodów słabej wydajność buforów instrukcji należy szukać w złej organizacji kodu źródłowego, a w szczególności, w powiązanych funkcjach, które znajdują się w różnych fragmentach kodu lub całkowicie innych plikach źródłowych.

4.4.5 Wydajność pamięci wirtualnej

Do implementacji mechanizmu pamięci wirtualnej wymagane jest przechowywanie odwzorowania pomiędzy stronami adresu wirtualnego a adresu fizycznego. W celu przyśpieszenia translacji adresów, w buforze stron procesora (ang. Translation Lookaside Buffer) przechowywanych jest kilka ostatnio używanych odwzorowań. Struktura tych buforów jest dwupoziomowa oraz z rozróżnieniem na strony zawierające dane i instrukcje. Niska wydajność translacji stron spowodowana jest żądaniami spod adresów, które są rozrzucone po całej przestrzeni (swobodnym charakterem dostępu do pamięci). Do oceny wydajności pamięci wirtualnej służą kolejne zdarzenia wymienione poniżej.

Wydajność translacji stron danych:

 Liczba zatwierdzonych instrukcji (0xC0 - ang. retired instructions) – zdarzenie opisane w paragrafie 4.4.1.  Liczba dostępów do buforu danych pierwszego poziomu (0x40 - ang. data cache accesses) – licznik ten rejestruje każde odwołanie jednostek mikroprocesora do pamięci danych.  Liczba trafień w buforze translacji stron danych drugiego poziomu (0x45 - ang. L1 DTLB miss and L2 DTLB hit) – jest to również liczba chybień w buforze translacji stron danych pierwszego poziomu.  Liczba chybień w buforze translacji stron danych drugiego poziomu (0x46 - ang. L1 DTLB miss and L2 DTLB miss) – liczba ta oznacza ilość odwołań do pamięci operacyjnej tylko w celu pobrania adresu komórki, która jest żądana przez instrukcję procesora i która może spowodować kolejne chybienie w buforze pamięci danych.

Wydajność translacji stron instrukcji:

 Liczba zatwierdzonych instrukcji (0xC0 - ang. retired instructions) – zdarzenie opisane w paragrafie 4.4.1.  Liczba dostępów do buforu instrukcji pierwszego poziomu (0x80 - ang. instruction cache fetches) – licznik ten rejestruje każde odwołanie jednostek mikroprocesora do pamięci instrukcji.  Liczba trafień w buforze translacji stron instrukcji drugiego poziomu (0x84 - ang. L1 ITLB miss and L2 ITLB hit) – jest to również liczba chybień w buforze translacji stron instrukcji pierwszego poziomu.  Liczba chybień w buforze translacji stron instrukcji drugiego poziomu (0x85 - ang. L1 ITLB miss and L2 ITLB miss) – liczba ta oznacza ilość odwołań do pamięci operacyjnej tylko w celu pobrania adresu komórki, która jest żądana przez instrukcję procesora i która może spowodować kolejne chybienie w buforze pamięci instrukcji.

36

Z racji tego, że każde odwołanie do pamięci pociąga za sobą potrzebę translacji adresu wirtualnego na fizyczny, to liczba odwołań do pamięci równa jest liczbie dostępów do buforów odwzorowań adresów stron. Podobnie jak ma to miejsce w przypadku pamięci podręcznej danych i kodu, tak i dla TLB chybienia na poziomie pierwszym i trafienia na poziomie drugim są o wiele mniej kosztowne od chybień na poziomie drugim. O poprawie wydajności świadczył będzie fakt zmniejszania się stosunku chybień do ilości wykonanych instrukcji lub ilości żądań.

Poprawy wydajności translacji adresów wirtualnych najlepiej szukać w przeprojektowaniu pętli przetwarzania lub zmiany użytych struktur danych w taki sposób, aby dostęp do pamięci miał charakter jak najczęściej sekwencyjny. Dodatkowo pomóc może wyrównanie najczęściej żądanych danych do początku strony. Zasadą jest też, że zmniejszenie chybień w buforze instrukcji lub danych powoduje również poprawę procesu translacji stron.

4.4.6 Zdarzenia związane z przepływem instrukcji

Z powodu spekulatywnego wykonywania instrukcji współczesne procesory starają się przewidzieć, w jakiej kolejności powinny być wykonywane instrukcje. Pomyłka w takich przewidywaniach skutkuje tym, że cała praca wykonana pomiędzy rozgałęzieniem kodu, a wykryciem pomyłki staje się bezużyteczna. Jednak jeszcze większy wpływ na wydajność może mieć konieczność anulowania całego potoku i rozpoczęcia przetwarzania od innych instrukcji, które być może nie znajdują się w pamięci podręcznej lub korzystają z danych obecnych tylko w pamięci operacyjnej. Widać więc, że koszt błędnego przewidzenia ścieżki programu jest istotny. Do sprawdzenia wydajności mechanizmów spekulatywnego przetwarzania pomagają następujące zdarzenia:

 Liczba zatwierdzonych instrukcji (0xC0 - ang. retired instructions) – zdarzenie opisane w paragrafie 4.4.1.  Zatwierdzone instrukcje rozgałęzień (0xC2 - ang. retired branch instructions) – liczba wszystkich instrukcji odpowiadających za kontrolę przepływu programu, włączając również przerwania oraz obsługę wyjątków.  Błędnie przewidziany przepływ instrukcji (0xC3 - ang. retired misspredicted branch instructions) – liczba zdarzeń, kiedy przewidywania procesora, co do wykonywania kolejnych instrukcji okazały się błędne. Do tego grona zaliczają się też typy instrukcji, które nie są przewidywalne (przerwania, sytuacje wyjątkowe oraz instrukcje skoku do innych segmentów kodu – ang. far control transfers).  Prawidłowo przewidziane instrukcje rozgałęzień (0xC4 - ang. retired taken branch instructions) – liczba instrukcji prawidłowo przewidzianych.

Stosunek liczby instrukcji rozgałęzień do liczby zatwierdzonych instrukcji świadczy o rozbudowaniu logiki decyzyjnej w analizowanej aplikacji. Celem optymalizacji powinno być zmniejszenie liczby błędnie przewidzianych rozgałęzień w stosunku do liczby wszystkich instrukcji lub tylko instrukcji decyzyjnych. Poprawę można osiągnąć przede wszystkim przez zmniejszenie liczby instrukcji sterujących lub (co może być trudniejsze) przebudowanie ich w taki sposób, aby były lepiej przewidywane przez mikroprocesor.

37

4.5 Implementacja metod profilowania w mikroarchitekturze AMD

Każdy z trybów analizy opiera swoje działanie na innej zasadzie. Niektóre z nich wykorzystują standardowe elementy systemu komputerowego, natomiast część z nich korzysta z mechanizmów dedykowanych platformie AMD. Zrozumienie metod zbierania danych pozwala na lepszą ocenę ich wpływu na wydajność analizowanej aplikacji oraz na lepsze zrozumienie ich znaczenia.

4.5.1 Kontroler APIC

Zaawansowany programowalny kontroler przerwań APIC (ang. Advanced Programmable Interrupt Controller) jest układem opracowanym przez firmę Intel, ale na podstawie umowy licencyjnej wykorzystywany jest również w procesorach AMD. Kontroler zapewnia współpracę pomiędzy urządzeniami I/O, a procesorami systemu komputerowego. Zawiera on też wbudowane timer’y umożliwiające zaprogramowanie obsługi przerwania co określony interwał. Właściwość ta wykorzystywana jest przez sterownik CodeAnalyst (tylko wersje na Windows) do pracy w trybie TBP. W systemie Linuks CodeAnalyst do tego samego celu wykorzystuje aplikację Oprofile (do implementacji trybu TBP używa jednego z wbudowanych liczników wydajnościowych ustawionego na pomiar ilości cykli procesora).

4.5.2 Liczniki wydajnościowe

Do pomiaru zdarzeń w trybie EBP służą liczniki wydajnościowe. Są nimi specjalizowane rejestry procesora przeznaczone na zliczanie ilości zaprogramowanych zdarzeń. Zasoby tych rejestrów są ograniczone i w przypadku rodzin procesorów 10h firmy AMD są to cztery liczniki na fizyczny rdzeń procesora. Niektóre ze zdarzeń mogą być obsługiwane jedynie przez określone numery liczników co uniemożliwia czasami łączenie wykluczających się rodzajów zdarzeń.

W implementacjach AMD budowa tych liczników oparta jest na 64 bitowym rejestrze [9], w którym wydzielony jest 48 bitowy licznik (patrz Rys. 6). Dokładność zliczania zdarzeń przez licznik nie jest gwarantowana przez inżynierów AMD, co wprowadza pewien element niepewności przy ich wykorzystaniu i całkowicie wyklucza je z zastosowań wymagających dużej precyzji. Do odczytu stanu licznika służy instrukcja RDPMC. Pozostałe 16 bitów rejestru traktowane jest jako tzw. RAZ (Read As Zero) – czyli wartość tych bitów równa jest 0. Przepełnienie licznika wydajnościowego generuje przerwanie systemowe obsługiwane przez sterownik CodeAnalyst (w przypadku systemów Windows). Dlatego podczas konfiguracji CodeAnalyst inicjalizuje licznik jego maksymalną wartością pomniejszoną o ilość żądanych zdarzeń pomiędzy próbkami.

Rys. 6 Budowa rejestru licznika wydajnościowego

Z każdym licznikiem wydajnościowym skojarzony jest rejestr, który służy do jego konfiguracji. Zbudowany jest z 64 bitów, których znaczenie jest następujące (w nawiasach [] podano zakres bitów):

38

 [63:36] Reserved – zarezerowane,  [35:32] EventSelect – 4 najstarsze bity wyboru zdarzenia,  [31:24] CntMastk – wybór metody zliczania zdarzeń w trakcie jednego cyklu zegarowego,  [23] Inv – zmienia znaczenie bitów CntMask,  [22] En – aktywowanie licznika,  [21] Reserved – zarezerwowane,  [20] Int – aktywuje generację przerwań przy przepełnieniu licznika,  [19] Reserved – zarezerwowane,  [18] Edge – konfiguracja sposobu detekcji zdarzenia,  [17] OS – zliczanie zdarzeń z poziomu systemu operacyjnego,  [16] User – zliczanie zdarzeń z poziomu użytkownika,  [15:8] UnitMask – dodatkowa konfiguracja wybranego zdarzenia,  [7:0] EventSelect – 8 najmłodszych bitów wyboru zdarzenia.

Dokładny opis mechanizmów liczników wydajnościowych można znaleźć w publikacjach BIOS and Kernel Developer’s Guide przeznaczonych dla poszczególnych rodzin procesorów (np. [9]).

Największą zaletą rozwiązania opartego na tych rejestrach jest wprowadzanie przez nie małego narzutu wydajnościowego oraz dostarczanie niskopoziomowych danych. Z drugiej strony głównym ograniczeniem jest ich mała liczba w porównaniu do ilości dostępnych zdarzeń. Dodatkowo, instrukcja, która spowodowała wyzwolenie przerwania, nie musi odpowiadać instrukcji odczytanej z rejestru stanu procesora – powodem tego jest spekulatywny i potokowy charakter przetwarzania współczesnych mikroprocesorów. Wada ta została jest wyeliminowana przez metodę IBS, która opiera swoje działanie na konstrukcji potoku procesora.

4.5.3 Konstrukcja potoku

Powyżej wymienione wady liczników wydajnościowych wyeliminowane zostały w trybie IBS. Dostępne są w nim dwa zdarzenia definiujące częstotliwość pobierania próbek:

 Pobranie instrukcji przez procesor (ang. fetch sampling);  Wysłanie mikrooperacji do wykonania (ang. op sampling).

Podział na dwie grupy spowodowany jest rozdzieleniem logiki działania potoku w mikroprocesorze [10], [11]. Jednak próbki dla obu zdarzeń mogą być zbierane razem lub osobno. Upraszczając konstrukcję potoku procesorów o architekturze x86 można go przedstawić w postaci pięciu faz, pokazanych na Rys. 7.

Rys. 7 Schemat budowy potoku mikroprocesorów o architekturach x86

39

Granica między wspomnianymi zdarzeniami przebiega pomiędzy etapem szeregowania mikrooperacji a jej wykonaniem. Osiągnięcie przez któreś ze zdarzeń zaprogramowanej wartości granicznej powoduje oznaczenie instrukcji powodującej konkretne zdarzenie. Taka instrukcja jest obserwowana przez układy procesora w celu odnotowania czy spowodowała pewne zdarzenia. Obserwacja kończy się w fazie zatwierdzania, a w przypadku instrukcji pobranych również gdy zostaną anulowane (w skutek anulowania potoku). Oprócz flag informujących o wystąpieniu pewnych zdarzeń, zebrana próbka zawiera również znacznik czasowy, identyfikator przerwanego procesu i wirtualny adres instrukcji. Dzięki temu dokładnie znana jest instrukcja, która wywołała zarejestrowane zdarzenia.

Z próbkowaniem na podstawie liczby pobranych instrukcji związane są między innymi następujące informacje:

 Czy instrukcja została wykonana czy anulowana;  Czy pobranie instrukcji spowodowało chybienie w buforze procesora lub w buforze translacji stron;  Jaka była wielkość strony pamięci wirtualnej, z której została pobrana instrukcja;  Wielkość opóźnienia w fazie pobrania.

Natomiast z próbkowaniem opartym na liczbie mikrooperacji związane są informacje:

 Jak długo mikroinstrukcja była przetwarzana w procesorze zaczynając od momentu oznaczenia i kończąc na jej zatwierdzeniu;  Czy instrukcja spowodowała operację ładowania lub zapisu w pamięci operacyjnej;  Czy dostęp do danych żądanych przez instrukcję spowodowało chybienie w buforze procesora lub w buforze translacji stron;  Wielkość opóźnienia w przypadku chybienia pamięci podręcznej;  W przypadku instrukcji decyzyjnych – czy została prawidłowo przewidziana.

40

5 Integracja AMD CodeAnalyst z systemem FITS

Głównym celem tej pracy inżynierskiej było zastosowanie API CodeAnalyst do zwiększenia efektywności systemu programowej symulacji błędów FITS [7]. System FITS wykorzystuje interfejs programistyczny Win32 – Debugging API do testowania aplikacji działających w systemach z rodziny MS Windows. Posiada wygodny interfejs graficzny oparty o Microsoft Foundation Classes (MFC). Pracę z aplikacją można podzielić na cztery oddzielne czynności:

 Konfiguracja momentu oraz rodzaju wstrzykiwanych błędów;  Tworzenie pliku z wzorcowym przebiegiem testowanej aplikacji (tzw. Golden Run Log);  Konfiguracja eksperymentu;  Uruchomienie eksperymentu.

Zmiany w implementacji podczas pisania tej pracy dotyczyły głównie 2 oraz 3 i 4 punktu. Konfiguracja momentu wstrzykiwania błędów nie wymagała zmian, chociaż opisywane rozwiązanie nie umożliwia zrealizowania wszystkich ustawień dostępnych w konfiguracji. Dokładny opis procesu konfigurowania momentu wstrzyknięcia oraz rodzaju błędu znajduje się w [7].

Jak wskazano w [23] jednym z istotnych ograniczeń wydajnościowych dotychczasowej implementacji FITS była obsługa pułapkowania momentu wstrzykiwania błędu przez symulator błędów FITS realizowana poprzez pułapki zarówno sprzętowe jak i programowe. Przedstawiona w [23] oraz [24] oryginalna alternatywna metoda precyzyjnego wyzwalania błędów sterowanego adresem instrukcji wyzwalającej i numerem iteracji jej wykonania powoduje narzuty akceptowalne dla niektórych klas aplikacji [21], ale nadal wymagały wiedzy o profilu dynamicznym analizowanej aplikacji, a dokładniej wzorcowego wykonania umożliwiającego poznanie zbioru wykonywanych instrukcji (ich adresów) oraz liczby ich iteracji. Wyklucza to jednak w praktyce możliwość badania aplikacji długotrwałych i wielowątkowych (np. kodery multimediów, aplikacje biurowe). Warunkiem koniecznym jest możliwość kontroli nad rozkładem momentów generacji błędów w czasie całego okresu działania aplikacji testowanej a to możliwe jest jedynie w sytuacji, kiedy symulator błędów zna konkretny zbiór adresów instrukcji, który z dużym prawdopodobieństwem będzie wykonywany przez testowaną aplikację od momentu założenia pułapki na tejże instrukcji.

Taką funkcjonalność, obarczoną minimalnym narzutem czasowym, udostępniają dedykowane systemy profilujące. Dostępność darmowego narzędzia profilującego z otwartym API otworzyła więc nową możliwość rozbudowy systemu FITS. Koncepcja rozwiązania polega na zastąpieniu wzorcowego wykonania z dotychczasowej formy śledzenia instrukcji maszynowych w trybie pracy krokowej na zbieranie profilu analizowanej aplikacji w postaci próbek TBP. Następnie, zbiór momentów generacji błędów określony jest na podstawie zebranych próbek. Instrukcja z każdej próbki jest identyfikowana przez adres ładowania modułu, z którego została wykonana, jej adres względem początku modułu oraz kolejnym numerem próbki względem całego profilu. Rozwiązanie takie pozwoli na w praktyce pomijalny narzut podczas wzorcowego wykonania zapewniając jednocześnie wysoki poziom trafności doboru adresu instrukcji maszynowej oraz jej iteracji w czasie testów. Również proces dochodzenia do momentu generacji błędów może być zoptymalizowany poprzez

41

odwlekanie właściwego pułapkowania instrukcji wyzwalającej aż do zebrania liczby próbek zbliżonej do kolejnego numeru próbki, dla której planowane jest wstrzyknięcie błędu.

W dalszej części rozdziału omówiono interfejs programowy udostępniany przez AMD CodeAnalyst. Następnie przedstawiono koncepcję i implementację opracowanego rozwiązania..

5.1 Interfejs programowy AMD CodeAnalyst

API dostarczone przez programistów CodeAnalyst jest niezbyt rozbudowane. Składa się na nie około 40 różnych funkcji operujących na ośmiu różnych strukturach danych. Napisane jest w języku C++. Niestety interfejs programistyczny CA nie odwzorowuje jeden-do-jednego funkcjonalności aplikacji graficznej. Brak jest między innymi możliwości filtrowania próbek po trybie uprzywilejowania, w jakim zostały zebrane lub informacji dostarczanych przez rozwijanie stosu. API zaimplementowane jest w sposób strukturalny, co wydaje się bardziej naturalne, gdyż jest ono bezpośrednim interfejsem do sterownika używanego przez CodeAnalyst a każdy użytkownik w miarę potrzeb może użyć wzorca projektowego fasady do opracowania interfejsu obiektowego.

Dokumentacja dostarczona wraz z projektem API [12] jest wygenerowana za pomocą oprogramowania doxygen co ułatwia bardzo nawigację po dokumencie. Warto też wspomnieć, że do dokumentacji dołączone są przypadki użycia, dzięki interakcja między funkcjami jest dobrze wyjaśniona. W katalogu z plikami interfejsu programistycznego zawarty jest również plik źródłowy z przykładową aplikacją. Ponadto funkcje podzielone są na dwa główne zbiory odpowiedzialne za zbieranie profilu oraz za analizę zebranych danych.

Prawie każda funkcja zwraca enumerację CAPROFILECONTROL_API HRESULT, która informuje użytkownika o sukcesie (wartość zwrócona S_OK) lub napotkanych błędach podczas wykonania zleconego zadania. Natomiast dane zwracane przez funkcje przekazywane są poprzez modyfikację przekazanych argumentów. Do błędów związanych z wywołaniem funkcji można zaliczyć głównie:

 E_INVALIDARG – niepoprawna wartość przekazanych argumentów;  E_ACCESSDENIED – brak dostępu do żądanej akcji, spowodowana np. brakiem inicjalizacji sterownika;  E_PENDING – żądanie nie może zostać aktualnie przetworzone z powodu wykonywania innych czynności;  E_OUTOFMEMORY – struktura przekazana w argumencie funkcji jest za mała lub nie udało się zaalokować wymaganej ilości pamięci;  E_UNEXPECTED – nieznany błąd.

5.1.1 Integracja CA API z aplikacją

Rozpoczęcie używania interfejsu programistycznego CodeAnalyst jest stosunkowo proste i składa się z kilku kroków, które opisano poniżej. Przykład oparty jest na projekcie w Microsoft Visual Studio 2010 i dotyczy oprogramowania wykorzystywanego na systemach z rodziny Windows.

42

Rozpocząć należy od instalacji AMD CodeAnalyst. Proces ten kończy się zarejestrowaniem sterownika CodeAnalyst w systemie Windows, dzięki czemu może on działać w trybie uprzywilejowanym. W folderze instalacyjnym utworzone zostanie kilka katalogów, wśród nich warto wyróżnić:

 SampleCode – zawiera kod źródłowy przykładowej aplikacji;  docs – zawiera dokumenty pomocy w formacie .chm;  bin – zawiera binarne pliki CodeAnalyst;  API – zawiera pliki nagłówkowe oraz biblioteki potrzebne do rozpoczęcia pracy z CodeAnalyst API.

Kolejnym krokiem jest skopiowanie katalogu API/include do źródeł aplikacji, w której będziemy korzystać z API CA lub dodanie tego folderu, jako źródło plików nagłówkowych. W Visual Studio można to zrobić w ustawieniach projektu w zakładce „C/C++ -> General”, jako wartość „Additional Include Directories”. Taką samą operację należy przeprowadzić z katalogiem API/lib z tą różnicą, że powinien on zostać dodany jako źródło zewnętrznych bibliotek. To ustawienie dla Visual Studio znajduje się w zakładce „Linker -> General”, jako wartość „Additional Library Directories”. Po dodaniu w zakładce „Linker -> Input” w wartości „Additional Dependencies” następujących plików: CaProfileControl.lib oraz CaProfileDataAccess.lib, cały projekt powinien się poprawnie skompilować i skonsolidować. Ostatnim krokiem potrzebnym do poprawnego uruchomienia utworzonej aplikacji jest dodanie do zmiennej środowiskowej PATH ścieżki do folderu bin z katalogu instalacyjnego AMD CodeAnalyst.

5.1.2 Zbieranie profilu

W tabeli Tabela 1 zamieszczono krótki opis kilku wybranych funkcji, które odpowiadają za konfigurację oraz kontrolę nad sterownikiem CodeAnalyst odpowiedzialnym za zebranie danych. Funkcje te są oddzielone od funkcji analizujących zebrane dane z dwóch powodów. Po pierwsze dane można analizować w zupełnie innym środowisku – w tym również opartym na procesorze nieobsługiwanym przez profiler CodeAnalyst, Drugim powodem jest brak wymagania, aby analiza danych zebranych przez interfejs programistyczny była również wykonana przez ten interfejs. Zamiast tego można użyć aplikacji linii komend CADataAnalyze, która umożliwia również eksport zebranych danych do graficznej aplikacji CodeAnalyst. Dla przejrzystości opisu, jeśli funkcja przyjmuje jakiekolwiek argumenty w ich liście użyto „…”, należy też pamiętać, że prawie wszystkie dane zwracane są poprzez modyfikację argumentów przekazywanych najczęściej jako wskaźniki.

43

Nazwa funkcji Opis fnEnableProfiling() Inicjalizuje zasoby używane przez sterownik. fnReleaseProfiling() Zwalnia zasoby używane przez sterownik. fnGetEventCounters(…) Zwraca liczbę dostępnych liczników wydajnościowych. fnGetLastProfileError(…) Zwraca opis napotkanego błędu. fnGetProfilerState() Zwraca status sterownika. fnMakeProfileEvent(…) Tworzy konfigurację zdarzenia trybu EBP, która można następnie przekazać do sterownika. Na tym etapie następuje weryfikacja poprawności konfiguracji. fnSetEventConfiguration(...) Ustawia konfigurację trybu EBP stworzoną funkcją fnMakeProfileEvent. Do funkcji można przekazać dowolną ilość konfiguracji, które zostaną rozdysponowane pomiędzy dostępne liczniki lub zostanie użyte multipleksowanie zdarzeń. fnSetFilterProcesses(…) Umożliwia zbieranie danych dotyczących tylko wyspecyfikowanych procesów. Wliczają się w to również procesy potomne. fnStartProfiling(…) Uruchamia zbieranie danych. fnPauseProfiling(…) Wstrzymuje zbieranie danych. fnResumeProfiling(…) Wznawia zbieranie danych. fnStopProfiling() Zatrzymuje zbieranie danych. fnGetIbsAvailable(…) Informuje o tym czy procesor obsługuję profilowanie metodą IBS. fnSetIbsConfiguration(…) Konfiguracja sterownika do zbierania danych za pomocą metody IBS. fnSetProfileOutputFile(…) Definiuje ścieżkę do pliku z zebranymi danymi. Plik ten ma format binarny i należy go odczytać odpowiednimi funkcjami API lub za pomocą aplikacji CADataAnalyze, fnSetTimerConfiguration(…) Konfiguruje sterownik do profilowania systemu metodą TBP.

Tabela 1 Podzbiór funkcji API CodeAnalyst odpowiadających za zbieranie profilu

44

5.1.3 Analiza zebranych danych

W tym podpunkcie znajduje się krótki opis kilku wybranych funkcji, za pomocą których można odczytać zebrane dane. Praca z danymi może przebiegać według dwóch scenariuszy. Pierwszym z nich jest wstępna agregacja danych, dzięki której programista ma dostęp do informacji pogrupowanych według adresów instrukcji. Innym sposobem jest odczyt danych nieprzetworzonych – próbka po próbce. Dla przejrzystości opisu, jeśli funkcja przyjmuje jakiekolwiek argumenty w ich liście użyto „…”, należy też pamiętać, że prawie wszystkie dane zwracane są poprzez modyfikację argumentów przekazywanych najczęściej jako wskaźniki.

Nazwa funkcji Opis fnOpenProfile(…) Otwiera uchwyt do pliku dyskowego z danymi zebranymi podczas profilowania. fnCloseProfile(…) Zwalnia zasoby używane przez otwarty uchwyt do profilu. fnOpenAggregatedProfile(…) Otwiera uchwyt do pliku dyskowego z zagregowanymi danymi zebranymi podczas profilowania. fnAggregateDataSets(…) Agreguje dane zebrane przez sterownik. fnWriteSetToFile() Zapisuje zagregowany profil, aby można go było odczytać za pomocą funkcji fnOpenAggregatedProfile. fnSetRuleCoreData(…) Filtruje zebrane dane na podstawie przekazanej maski rdzeni. fnSetRuleForProcesses(...) Filtruje zebrane dane na podstawie przekazanej listy identyfikatorów procesów. fnSetRuleForThreads(…) Filtruje zebrane dane na podstawie przekazanej listy identyfikatorów wątków. fnGetDataEvents(…) Zwraca informację o rodzaju zdarzeń przechwyconych podczas zbierania profilu. fnGetProcessData(…) Zwraca tablicę informacji o wszystkich procesach, z których wykonane zostały instrukcje zapisane w zebranych próbkach. fnGetModuleData(…) Zwraca tablicę informacji o wszystkich modułach, z których wykonane zostały instrukcje zapisane w zebranych próbkach. fnGetInstructionData(…) Zwraca tablicę informacji o spróbkowanych instrukcjach w ramach przekazanego modułu.

45

Nazwa funkcji Opis fnGetFirstRawRecord(…) Zwraca strukturę danych zawierającą pierwszą nieprzetworzoną próbkę. fnGetNextRawRecord(…) Zwraca strukturę danych zawierającą kolejną nieprzetworzoną próbkę. Przed wywołaniem tej funkcji musi zostać wywołana funkcja fnGetFirstRawRecord.

Tabela 2 Podzbiór funkcji API CodeAnalyst odpowiadających za odczytywanie danych z zebranego profilu 5.1.4 Struktury danych

Podpunkt ten zawiera krótki opis struktury przechowującą niezagregowane informacje o każdej próbce z osobna, gdyż jej użycie daje największą swobodę dla programisty. Dodatkowo opisane zostały dwie struktury pomocnicze.

Struktura RawDataType:

Typ atrybutu Nazwa atrybutu Opis

FILETIME timeMark Czas zebrania próbki. wchar_t* Path Nazwa modułu, z którego wykonana została instrukcja uchwycona w odczytywanej próbce. unsigned __int64 loadAddress Adres ładowania modułu w przestrzeni adresowej procesu. unsigned __int64 moduleSize Rozmiar modułu podany w bajtach. unsigned __int64 Address Wirtualny adres instrukcji podany względem początku przestrzeni adresowej.

ModuleType type Enumeracja informująca o typie modułu. unsigned __int64 processId Identyfikator procesu, w którym została zebrana próbka. unsigned __int64 threadId Identyfikator wątku, w którym została zebrana próbka. wchar_t* pJitFunctionName Nazwa funkcji, z której kompilator JIT wygenerował instrukcję zapisaną w danej próbce. wchar_t* pJitDataFile Nazwa pliku nadana przez kompilator

46

Typ atrybutu Nazwa atrybutu Opis

JIT. unsigned __int64 ibsOpBranchAddress Docelowy adres instrukcji rozgałęzienia. Dostępny jedynie w trybie IBS. unsigned __int64 ibsOpDcLinearAddress Liniowy adres odwołania do pamięci podręcznej procesora. Dostępny jedynie w trybie IBS. unsigned __int64 ibsOpDcPhysicalAddress Fizyczny adres odwołania do pamięci podręcznej procesora. Dostępny jedynie w trybie IBS.

SampleData data Struktura danych z zarejestrowanymi zdarzeniami.

Tabela 3 Opis struktury RawDataType

Struktura SampleData:

Typ atrybutu Nazwa atrybutu Opis unisigned int count Ilość zdarzeń.

SampleDatumKey* keyArray Identyfikacja zdarzenia oraz rdzenia, na którym zostało zarejestrowane. unsigned __int64* dataArray Ilość próbek zebranych dla danego zdarzenia.

Tabela 4 Opis struktury SampleData

Struktura SampleDatumKey:

Typ atrybutu Nazwa atrybutu Opis int core Numer rdzenia, na którym zostało zarejestrowane zdarzenie. unsigned __int64 event Kod zarejestrowanego zdarzenia.

Tabela 5 Opis struktury SampleDatumKey 5.2 Koncepcja rozwiązania i sposób jej realizacji

Praca symulatora FITS w wersji 3.2 (użytej w projekcie) opiera się na obserwacji zaawansowania wykonania testowanej aplikacji i wstrzyknięcia błędu w momencie zarejestrowania, wcześniej wytypowanego, stanu oprogramowania. Jako zaawansowanie aplikacji lub stan aplikacji rozumiana jest ilość dotychczas wykonanych instrukcji. W celu

47

realizacji takiego sposobu symulacji błędów FITS w dotychczasowej wersji uruchamia testowaną aplikację w trybie debugowania, co umożliwia krokowe wykonanie instrukcji. Operacja ta jest bardzo czasochłonna, nawet w przypadku krokowego wykonania jedynie niewielkich fragmentów kodu, które są na przykład wykonywane w pętli. Dokładniejszy opis działania FITS zamieszczono w [7]. Pomysłem na zwiększenie wydajności symulatora było nadzorowanie wykonania testowanej aplikacji za pomocą profilera CodeAnalyst. Sposób ten opiera się na analizie liczby zebranych próbek w stosunku do liczby próbek zebranych podczas przebiegu wzorcowego. W ten sposób w każdym momencie wykonania testowanej aplikacji można z pewną dokładnością określić, jakie jeszcze instrukcje będą wykonywane, a tym samym, które z nich nadają się na miejsce wstrzyknięcia błędu. W dalszej części pracy znajduje się opis wdrożenia tego pomysłu do działającego symulatora. Podjęta została decyzja o niemodyfikowaniu dotychczasowego sposobu działania aplikacji FITS, a wykorzystanie profilera CodeAnalyst zostało zaimplementowane jako jego nowe funkcjonalności.

Niestety implementacja profilowania za pomocą CodeAnalyst niesie za sobą kilka wad, które zmniejszają funkcjonalność symulatora:

 Podczas rejestracji przebiegu wzorcowego nie mamy dostępu do stanu rejestrów procesora. Uniemożliwia to obserwowanie czasu utajenia błędów, a także obliczanie efektywnego adresu odwołania spróbkowanej instrukcji[21].  FITS nie umożliwia wstrzykiwania błędów w kod wykonywany na poziomie uprzywilejowania systemu operacyjnego natomiast API CA nie umożliwia rozpoznania poziomu uprzywilejowania, na którym została wykonana zrejestrowana przez niego instrukcja. Powoduje to, że nie uda się wstrzyknąć błędu w tak wytypowaną instrukcję, a tym samym zmniejsza się efektywność eksperymentu (w niektórych z testów nie dochodzi do wstrzyknięcia błędów).  Za pomocą krokowego wykonania instrukcji można otrzymać obraz wykonania analizowanej aplikacji zawierający nie tylko wszystkie wykonywane przez nią instrukcje, ale także liczby ich wykonania. Dzięki temu błąd można wstrzykiwać po dokładnie określony numer wykonania instrukcji, natomiast w przypadku metody CodeAnalyst można określić jedynie mniej więcej moment czasowy wykonania instrukcji, w którą wstrzykiwany jest błąd.

Podczas implementacji autor starał się jak najmniej ingerować w kod istniejących klas aplikacji FITS, niemniej nie można było uniknąć kilku niewielkich ingerencji. Spowodowane to było głównie wymogiem przystosowania symulatora do pracy z wielowątkowymi aplikacjami, a także dodanie kilku metod, które umożliwiały odczytywanie składowych klas. W celu odseparowania zmian od oryginalnej wersji oprogramowania utworzono katalog „ca”, w którym znajdują się wszystkie nowe klasy.

Dostęp do nowych funkcjonalności zrealizowany jest przez dwa nowe elementy menu widoczne na Rys. 8:

Rys. 8 Menu zmodyfikowanego symulatora FITS

48

Element pierwszy odpowiada za generację przebiegu wzorcowego, natomiast element drugi uruchamia eksperyment. Elementy te są nieaktywne, jeśli na danym systemie nie jest dostępne profilowanie przez CodeAnalyst. Sprawdzenie tego warunku jest dwuetapowe i następuje przy starcie aplikacji. Pierwszy etap wykorzystuje funkcję HMODULE WINAPI LoadLibrary(_In_ LPCTSTR lpFileName). Dzięki niej sprawdzane jest czy w systemie dostępne są biblioteki, które odpowiadają za pracę sterownika CA. Uniknięcie błędu automatycznego ładowania tych bibliotek, na systemie ich nie posiadających, rozwiązane zostało przez dodanie ich nazw w opcji „Delay Loaded DLLs” znajdującej się w konfiguracji projektu Visual Studio. Jeśli powiodło się ładowanie bibliotek za pomocą wyżej wspomnianej funkcji następuje sprawdzenie dostępności profilowania przez funkcję fnEnableProfiling() z API CodeAnalyst. Dopiero po jednoczesnym spełnieniu tych dwóch warunków udostępniane są te dwa elementy menu. W kolejnych punktach szczegółowo opisano sposób implementacji. Opis rozdzielono na dodane funkcjonalności tj.: rejestrowanie przebiegu wzorcowego oraz przeprowadzanie eksperymentu.

5.2.1 Modyfikacja generacji przebiegu wzorcowego

Generację przebiegu wzorcowego można podzielić na trzy etapy:

1. Zebranie próbek za pomocą profilowania metodą TBP; 2. Deasemblacja zebranych instrukcji; 3. Zapisanie pliku wynikowego.

Jako metodę profilowania wybrano TBP, gdyż nie wymaga ona komputera z procesorem AMD oraz generuje najmniejszy narzut wydajnościowy, a przy tym jej dokładność i ilość zebranych informacji jest wystarczająca do takiego zastosowania. Potrzebne funkcje API CodeAnalyst zostały obudowane w klasę singleton’a o nazwie CADriver (CADriver.h, CADriver.cpp). Do przetwarzania danych zostały stworzone trzy struktury pomocnicze znajdujące się w pliku CADriverHelper.hpp. Są to: struktura przechowująca zdeasemblowane instrukcje, struktura zawierająca informacje o module, z którego pochodzi instrukcja oraz struktura opisująca potencjalną instrukcję do wstrzyknięcia błędu.

Pierwszy etap zaczyna się od wprowadzenia konfiguracji przez użytkownika. Proces ten jest kontrolowany przez klasę CAGoldenRunManager (CAGoldenRunManager.h, CAGoldenRunManager.cpp). Względem oryginalnej wersji dialogu konfiguracyjnego zawiera on tylko trzy pierwsze pola wejściowe ze ścieżką do testowanego programu, argumentami wywołania oraz ścieżką do pliku wynikowego (Rys. 9). Dodatkowo, użytkownik ma też możliwość konfiguracji interwału zbierania próbek. Dzięki temu może kontrolować narzut wydajnościowy na testowaną aplikację. Wartość interwału jest domyślnie ustawiona na 100 mikrosekund, co jest najmniejszą wartością, jaką obsługuje CodeAnalyst, daje to w przybliżeniu 10000 próbek na sekundę wykonania testu. Zatwierdzenie konfiguracji powoduje skonfigurowanie sterownika profilującego, a następnie uruchomienie testowanej aplikacji w trybie debugowania i jednoczesnego zbierania próbek, aż do jej zakończenia. Wymóg na uruchomienie w trybie debugowania spowodowany jest potrzebą zbierania kolejności tworzenia wątków w celu obsługi wstrzykiwania błędów do aplikacji wielowątkowych. Etap ten realizowany jest przez funkcję bool CADriver::startGRProfiler().

49

Rys. 9 Konfiguracja zapisu przebiegu wzorcowego Po zakończeniu profilowania następuje proces deasemblacji zebranych instrukcji. Służą do tego dwie klasy MemmoryMapper (MemmoryMapper.hpp) oraz Disasm (CADisasm.h, CADisasm.cpp). Klasa Disasm do poprawnego działania wymaga odczytania bajtowego zapisu instrukcji. W tym przypadku znajduje się on w pliku dyskowym wskazywanym przez ścieżkę modułu odczytaną z zebranej próbki. Powoduje to, że wymagane jest odwzorowanie tego pliku w pamięci co realizuje klasa MemmoryMapper. Podczas implementacji napotkano dwie trudności. Pierwszą z nich występowała w przypadku uruchamiania FITS na systemie 64bitowym. FITS jest aplikacją 32bitową – co powoduje, że odwołania do niektórych ścieżek są przez system operacyjny automatycznie przekierowane do innego katalogu. Przykładem takim jest katalog windows\system32. Na szczęście istnieją funkcje systemowe, które umożliwiają wyłączenie tego mechanizmu. Realizuje to kod przedstawiony na następnej stronie.

50

HANDLE m_hFile; PVOID OldValue = NULL; Wow64DisableWow64FsRedirection(&OldValue); m_hFile = CreateFile( modulePath, flags, NULL, OPEN_EXISTING, attr, NULL); Wow64RevertWow64FsRedirection(OldValue);

Kolejną kwestią do rozwiązania było określenie przesunięcia segmentu kodu względem początku odwzorowywanego pliku. Spowodowane jest to obecnością na początku pliku nagłówka o zmiennym rozmiarze. Rozwiązane jest to za pomocą poniższego kodu: char segment[] = ".text"; SetFilePointer(m_hFile,0,0,FILE_BEGIN); cb = ReadFile(m_hFile,&img_dos,sizeof(img_dos),&cb,0)?cb:0; if(!cb){ return false; } if(IMAGE_DOS_SIGNATURE != img_dos.e_magic){ return false; } ReadFile(m_hFile,more_dos,sizeof(more_dos),&cb,0); SetFilePointer(m_hFile,img_dos.e_lfanew,0,FILE_BEGIN); ReadFile(m_hFile,&nt_sign,sizeof(nt_sign),&cb,0); ReadFile(m_hFile,&img_file,IMAGE_SIZEOF_FILE_HEADER,&cb,0); unsigned int seek_optional = SetFilePointer(m_hFile,0,0,FILE_CURRENT); ReadFile(m_hFile,&img_optional,IMAGE_SIZEOF_NT_OPTIONAL_HEADER,&cb,0); for(unsigned int nofs = 0;nofscb){ break; } if(!strcmp((char*)img_section.Name,segment)){ codeOffsetInFile = img_section.PointerToRawData; virtualAddress = img_section.VirtualAddress; } }

Za zapisanie oraz wczytanie pliku wynikowego odpowiada klasa CAGoldenRunLogFile (CAGoldenRunLogFile.h, CAGoldenRunLogFile.cpp). W następnym punkcie zawarty został opis wygenerowanego pliku.

51

5.2.2 Struktura pliku wzorcowego wykonania

Implementacja mechanizmów CodeAnalyst wymogła całkowite przeprojektowanie pliku z zapisem przebiegu wzorcowego. Poniżej przedstawiony jest niewielki fragment testowo wygenerowanego loga, który to zawiera najistotniejsze dane. Pogrubione liczby z pierwszej kolumny nie znajdują się w wygenerowanym pliku, ale zostały dodane w celu łatwiejszego odwoływania się do poszczególnych linii podczas opisu struktury, która to znajduje się w dalszej części podpunktu.

1. H CodeAnalyst Golden Runner log file ( v. 1.0 ) 2. H C:\Users\Piotrek\Dropbox\fits\Release\test\matricesRelease.exe 3. H 500 4. H 56831 5. H 1 6. H 9331 7. H 100 8. M C:\Users\Piotrek\Dropbox\fits\Release\test\matricesRelease.exe 9. M Ox400000 10. M 9118 11. I 2 0x13b7 0345f0 add eax,[ebp-0x10] 12. I 127:1;247:1;

Każdy plik można podzielić na trzy części rozpoznawane za pomocą pierwszego znaku w kolejnej linii:

 Część nagłówkowa, rozpoznawana za pomocą litery H, w każdym pliku znajduje się jeden taki obszar, który składa się z 7 linii (patrz Tabela 6);  Część dotycząca modułu, informacje o module zajmują trzy linie identyfikowane literą M, w jednym pliku może być wiele definicji modułów (patrz tabela Tabela 7);  Część dotycząca instrukcji, która znalazła się wśród zebranych próbek, każda instrukcja skorelowana jest z definicją modułu, która występuje bezpośrednio przed zbiorem instrukcji. Rozpoznawanie opisu instrukcji następuje po literze I, na każdą instrukcję składają się dwie linie opisu (patrz tabela Tabela 8);

Nr. Opis linii linii 1. Stała treść pełniąca rolę wyróżnika wersji klasy obsługującej plik Golden Run; 2. Ścieżka do testowanej aplikacji; 3. Argumenty testowanej aplikacji; 4. Poprawny kod wyjścia z testowanej aplikacji; 5. Czas trwania wzorcowego przebiegu podany w sekundach; 6. Całkowita ilość próbek zebranych podczas pracy aplikacji; 7. Okres zbierania próbek podany w mikrosekundach; Tabela 6 Struktura nagłówka pliku Golden Run

52

Nr. Opis linii linii 8. Ścieżka ładowania modułu; 9. Adres ładowania modułu w przestrzeni wirtualnej zapisany w formacie heksadecymalnym; 10. Całkowita ilość zebranych próbek dla instrukcji z opisywanego modułu; Tabela 7 Struktura pliku Golden Run w części opisującej moduł Nr. Opis linii linii 11. Pierwsza liczba określa ilość zebranych próbek dla danej instrukcji, kolejną heksadecymalną liczbą jest adres instrukcji względem adresu ładowania modułu. Następnie zapisany jest ciąg bajtów w formacie heksadecymalnym, który jest zapisem kodu instrukcji. Reszta linii to zapis mnemonika instrukcji; 12. W tej linii wyszczególnione są kolejne numery próbek dotyczące danej instrukcji. Numery próbek liczone są globalnie względem całego pliku – czyli względem początku przebiegu wzorcowego. Każdy kolejny numer próbki oddzielony jest średnikiem, natomiast po dwukropku zapisany jest numer wątku. Numer wątku informuje o kolejności jego utworzenia, dzięki czemu za każdym uruchomieniem testowanej aplikacji można przyporządkować rzeczywisty identyfikator wątku do rozpatrywanej próbki1; Tabela 8 Struktura pliku Golden Run w części opisującej instrukcję 5.2.3 Implementacja eksperymentu nadzorowanego przez CodeAnalyst

Po zebraniu zapisu przebiegu wzorcowego użytkownik może przystąpić do konfiguracji eksperymentu. Proces konfiguracji eksperymentu opartego na CodeAnalyst wygląda prawie identycznie w stosunku do eksperymentu opartego na krokowym wykonaniu programu. Jedyną różnicą jest dodatkowe okno dialogowe, w którym użytkownik musi zdefiniować dwa dodatkowe parametry (Rys. 10). Parametrami tymi jest wielkość dozwolonego błędu identyfikacji momentu generacji błędu (1.) oraz współczynnik ciągu geometrycznego (2.) na podstawie którego określane są momenty zatrzymania aplikacji w celu sprawdzenia jej stanu zaawansowania. Dialog konfiguracji eksperymentu obsługuje klasa CAExperimentManager (CAExperimentManager.h, CAExperimentManager.cpp)

1 W szczególnych przypadkach może to być niemożliwe, na przykład jeśli kolejność tworzenia wątków podczas testu będzie inna niż przy budowaniu przebiegu wzorcowego.

53

Rys. 10 Konfiguracja eksperymentu

Po skonfigurowaniu eksperymentu można przystąpić do jego realizacji. Czynność ta dzieli się na cztery etapy, przy czym etap 3 i 4 jest wykonywany wielokrotnie gdyż w ramach eksperymentu może zostać wykonanych wiele testów. Etapy są następujące:

1. Odczytanie listy instrukcji z pliku z zapisem przebiegu wzorcowego; 2. Wytypowanie instrukcji do wstrzyknięcia błędu na podstawie przekazanej konfiguracji; 3. Uruchomienie testu; 4. Zakończenie i zapisanie wyników testu;

Odczytanie pliku z logiem Golden Run realizowane jest przez tę samą klasę, co zapis tego pliku, a więc CAGoldenRunLogFile. Informacje zawarte w tym pliku są odtwarzane w pamięci w postaci struktur pomocniczych znajdujących się w pliku CADriverHelper.hpp. Za następny etap odpowiada klasa CATriggersFinder (CATriggersFinder.h, CATriggersFinder.cpp). Implementacja tej klasy oparta jest na klasach TriggersFinder, OpcodesFinder_v1 oraz OpcodesFinder_v2, realizuje ona niemalże identyczną logikę jak w tych trzech klasach z tą różnicą, że jest ona przystosowana do operowania na używanych strukturach danych.

Uruchomienie testu jest realizowane tak samo jak w pierwotnej wersji FITS przez klasę Tracer (Tracer.h, Tracer.hpp). Odpowiada ona za uruchomienie testowanego programu w trybie debugowania oraz za nadzorowanie jego wykonania, czyli między innymi reagowanie na każde zdarzenia DebugEvent. Klasa ta w konstruktorze przyjmuje wskaźnik do obiektu typu DebugEvenHandler. Współpraca pomiędzy tymi dwoma klasami polega na tym, iż gdy testowana aplikacja zgłosi jedno ze zdarzeń wymienionych w Tabela 9 wywoływana jest odpowiednia funkcja z klasy DebugEventHandler.

54

CREATE_PROCESS_DEBUG_EVENT CREATE_THREAD_DEBUG_EVENT EXCEPTION_DEBUG_EVENT EXIT_PROCESS_DEBUG_EVENT EXIT_THREAD_DEBUG_EVENT LOAD_DLL_DEBUG_EVENT OUTPUT_DEBUG_STRING_EVENT RIP_EVENT UNLOAD_DLL_DEBUG_EVENT Tabela 9 Zdarzenia obsługiwane przez klasę Tracer

Ponadto klasa Tracer przekazuje sterowanie do klasy DebugEventHandler wywołując, co określony interwał, metodę bool Idle(Tracer *pTracer). W klasie Tracer zawarte są również funkcje, za pomocą których można ustawić pułapkę pod konkretnym adresem dla określonego wątku. Implementacja eksperymentu nadzorowanego za pomocą sterownika CodeAnalyst opierała się na stworzeniu klasy CAFaultGenerator (CAFaultGenerator.h, CAFaultGenerator.cpp), która dziedziczy po klasie FaultGenerator, a ta z kolei po klasie DebugEventHandler. Przeciążenia wymagały następujące funkcje:

 BOOL Idle(Tracer *pTracer);  BOOL CreateThreadDebugEvent(Tracer *pTracer,CREATE_THREAD_DEBUG_INFO &CTDI);  BOOL CreateProcessDebugEvent(Tracer *pTracer,CREATE_PROCESS_DEBUG_INFO &CPDI);  BOOL ExitThreadDebugEvent(Tracer *pTracer,EXIT_THREAD_DEBUG_INFO &ETDI);  BOOL ExitProcessDebugEvent(Tracer *pTracer,EXIT_PROCESS_DEBUG_INFO &EPDI);  BOOL ExceptionBreakpoint(Tracer *pTracer,EXCEPTION_DEBUG_INFO &EDI);  BOOL ExceptionSingleStep(Tracer *pTracer,EXCEPTION_DEBUG_INFO &EDI);  void OnTerminate(Tracer *pTracer);

Natomiast następujące funkcje musiały zostać dodane:

 BOOL isReadyToInject(Tracer *pTracer);  BOOL setTrap(Tracer *pTracer);

Funkcja setTrap() odpowiada za ustawienie pułapki na żądanej instrukcji w odpowiednim momencie. Jest ona wywoływana jedynie w metodzie Idle(), aby zminimalizować narzut spowodowany ciągłym zatrzymywaniem i uruchamianiem profilowania. Aktywowana pułapka jest obsługiwana w funkcji ExceptionBreakpoint(), gdzie ma miejsce właściwe wstrzyknięcie błędu. W celu stwierdzenia czy nadszedł już odpowiedni moment na ustawienie pułapki wykorzystywana jest funkcja isReadyToInject(). Korzysta ona bezpośrednio z klasy CADriver do zliczania ilości dotychczas zebranych próbek. Poniżej zamieszczono uproszczony kod opisanych funkcji.

55

BOOL CAFaultGenerator::setTrap(Tracer *pTracer){ if(bTrapSet){ return true;} if(isReadyToInject(pTracer) && !bInjected){ //ustawienie dla którego wątka trzeba ustawić pułapkę pTracer->SetThreadHandle(m_pAddress->GetAreaCounter());

DWORD address = m_pAddress->GetAddress(); if (!pTracer->setBreakpoint(address,&m_Opcode)){ //ustawienie pułapki bTrapSet=FALSE; return FALSE; } else{ bTrapSet=TRUE; } } return true; }

BOOL CAFaultGenerator::isReadyToInject(Tracer *pTracer){ if( CAinstance->isProfiling() ){ //zatrzymanie profilowania i sprawdzenie ilości zebranych próbek CAinstance->stopProfiling(); collectedSamplesTillNow += CAinstance->getCollectedSamplesCount();}

//obliczenie interwału kolejnego sprawdzenia ilości zebranych próbek waitTime = ((sampleNumber-collectedSamplesTillNow)*ratio)/samplesRatio; waitTime -= deltaTime;

//sprawdzenie czy nie stan aplikacji nie jest wystarczająco blisko momentu wstrzyknięcia błędu if(waitTime <= 0){ pTracer->setTimeout(100); return true; //można wstrzyknąć }

//ustawienie interwału kolejnego sprawdzenia ilości zebranych próbek if(waitTime>100){ pTracer->setTimeout(100); idleNumber = waitTime/100; } else{ pTracer->setTimeout(waitTime); idleNumber = 1; } CAinstance->startProfiling(); //ponowne uruchomienie profilowania return false; }

W powyższym kodzie widać użycie zmiennych ratio oraz deltaTime. Ich wartości są współczynnikami ustawionymi w ostatnim etapie konfiguracji eksperymentu (Rys. 10). Wyraźnie widać, że zależy od nich ilość odczytów liczby zebranych próbek. Minimalizacja tej ilości występuje przy ustawieniu współczynnika Ratio na 1 oraz dużej wartości współczynnika Delta Time.

Po ustawieniu pułapki przez funkcję setTrap() w momencie, gdy rejestr licznika instrukcji procesora zawiera wartość, pod którą ustawiliśmy pułapkę generowany jest wyjątek

56

przechwytywany przez klasę Tracer, która to wywołuje metodę ExceptionBreakpoint() z klasy CAFaultGenerator. W tej metodzie następuje sprawdzenie czy adres pułapki zgadza się z adresem, pod którym została ona ustawiona i w przypadku zgodności wykorzystywany jest obiekt klasy Generator do generacji błędu na podstawie konfiguracji i jego wstrzyknięcia. Jeśli wstrzyknięcie powiodło się Tracer ma zlecone włączenie krokowego wykonywania instrukcji. Służy to realizacji prymitywnego rejestrowania czasu utajenia błędu. Rejestracja ta polega na krokowym wykonaniu 10 instrukcji po instrukcji wyzwalającej wstrzyknięcie błędu (patrz kod poniżej). Jeśli przez okres wykonania tych 10 instrukcji zarejestrowany zostanie wyjątek to do pliku z wynikiem eksperymentu zapisywany jest numer instrukcji, po której nastąpiło ujawnienie się błędu. Prymitywność rozwiązania spowodowana jest ilością wykonywania krokowo maksymalnie 10 instrukcji, oczywiście nic nie stoi na przeszkodzie zwiększania tej wartości, jednak wiąże się to ze spadkiem szybkości testu.

Kolejnym problemem jest fakt, że jako błąd rozpoznawany jest jedynie wyjątek rzucony do debugera, a nie jest nim np. pierwsza błędna wartość rejestrów w stosunku do przebiegu wzorcowego, jak miało to miejsce w oryginalnym systemie FITS. Wynika to z braku dostępu do historycznych wartości rejestrów procesora zbieranych podczas wzorcowego wykonania w oryginalnym systemie FITS.

BOOL CAFaultGenerator::ExceptionSingleStep(Tracer *pTracer,EXCEPTION_DEBUG_INFO &EDI) { if (bInjected) { ++faultLatency; if(faultLatency > 10){ faultLatency = INT_MAX; //wyłączenie krokowego wykonania pTracer->setSingleStepFlag(FALSE); return true; } } return true; }

Metoda Idle() klasy CAFaultGenerator odpowiada za uruchamianie funkcji setTrap(), która, jak wcześniej wspomniano, realizuje ustawianie pułapki tuż przed wybranym momentem wstrzyknięcia instrukcji (patrz kod poniżej). Uruchomienie tej metody następuje nie rzadziej niż co 100 milisekund, stąd potrzebne było zliczanie ilości jej wywołań dla przypadków, gdy okres odczytywania liczby zebranych próbek został obliczony na więcej niż 100 milisekund. Zmniejszenie częstotliwości wywołania metody Idle() nie było dobrym rozwiązaniem, gdyż wywoływana w niej metoda FaultGenerator::Idle() odpowiada za sprawdzenie czy nie upłynął maksymalny czas wykonania testu lub czy użytkownik nie zażądał jego zakończenia.

57

BOOL Idle(Tracer *pTracer) // zwrócenie FALSE kończy test { BOOL ret; ++idleCount; if(!pTracer->isDebugEvent()){ //pominięcie zliczania próbek, jeśli wywołanie spowodowane przez DebugEvent if(idleNumber <= idleCount){ //sprawdzenie czy upłynął wystarczający okres czasu pTracer->suspendAllThreads(); setTrap(pTracer); idleCount = 1; pTracer->resumeAllThreads(); } } ret = FaultGenerator::Idle(pTracer); return ret; };

Metoda CreateProcessDebugEvent() klasy CAFaultGenerator odpowiada za inicjalizację oraz konfigurację sterownika CodeAnalyst. Natomiast metody ExitProcessDebugEvent() oraz OnTerminate() zapewniają, że po każdym teście zostaną zwolnione zasoby zarezerwowane przez sterownik CodeAnalyst. CreateThreadProcessDebugEvent() oraz ExitThreadProcessDebugEvent() musiały zostać przeciążone w celu dodania obsługi wielowątkowości, chociaż same w sobie nie realizują żadnego przetwarzania a zwracają jedynie informację, aby test był kontynuowany. Zapis pliku zawierającego wynik eksperymentu generowany jest przez pierwotne klasy.

58

6 Weryfikacja rozwiązania

Ogół wykonanych prac został zweryfikowany podczas kilku testowych eksperymentów. Proces testowania wykazał pewne błędy, które zostały wyeliminowane. Na obecnym etapie oprogramowanie powinno być pozbawione błędów spowodowanych niepoprawną implementacją mechanizmu eksperymentu. Wykonanie testów miało również za zadanie oszacowanie precyzyjności wstrzykiwania błędów a także narzutu czasowego spowodowanego przez symulator.

Pod pojęciem precyzyjności wstrzykiwacza zawiera się współczynnik liczby testów do liczby testów, podczas których udało się wstrzyknąć błąd. Niepowodzenie we wstrzykiwaniu błędów spowodowane jest przede wszystkim „przegapieniem” momentu wstrzyknięcia odczytanego z pliku Golden Run. Największy wpływ na ten problem ma konfiguracja eksperymentu ustawiana przez użytkownika. Daje to pewną elastyczność, dzięki której osoba korzystająca z symulatora może znaleźć swój złoty podział pomiędzy precyzyjnością wstrzykiwacza, a wielkością powodowanego przez niego narzutu wydajnościowego. Niemniej, przeprowadzone testy wykazały, iż uzyskanie precyzyjności na poziomie pomiędzy 80% a 90% jest możliwe nawet bez dużego narzutu na wydajność testowanej aplikacji. Niestety podczas testów okazało się również, że duży wpływ na precyzyjność symulatora ma typ testowanej aplikacji. W niektórych przypadkach na wstrzyknięcie błędu nie pozwala błąd funkcji systemowej Windows, która używana jest do ustawienia docelowej pułapki. Sytuacje te wymagają dalszej analizy, która nie jest łatwa ze względu na rzadkie prawdopodobieństwo jej wystąpienia. Osiągnięcie precyzyjności wyższej od wspomnianej może być kłopotliwe ze względu na wykorzystywanie statystyki czasowej. Problem ten jest szczególnie widoczny w aplikacjach z dużym zbiorem unikalnych instrukcji w stosunku do liczby wszystkich wykonanych instrukcji.

Opóźnienie wprowadzane przez FITS do czasu trwania eksperymentu zawiera dwie składowe: stałą i zmienną. W testowanym systemie z procesorem AMD Phenom II 955 stała wartość opóźnienia wynosiła od trzech do sześciu sekund i wynika z inicjalizacji zasobów sterownika CA. Natomiast zmienne opóźnienie to około 10% czasu trwania wzorcowej aplikacji (w przypadku aplikacji o czasie trwania do 20 sekund) co wynika głównie z zatrzymywania testowanej aplikacji w celu zliczenia liczby zebranych próbek. Wyniki takie są akceptowalne z użytkowego punktu widzenia.

Sam proces testowania przeprowadzony został na dwóch typach aplikacji: jednowątkowych oraz wielowątkowych. W przypadku symulowania błędów w oprogramowaniu jednowątkowym, jakim były specjalnie w tym celu napisane programy korzystające głównie z mocy obliczeniowej komputera, nie napotkano żadnych anomalii. Natomiast do testowania aplikacji wielowątkowych wybrano edytor tekstu Word z pakietu Office 2010. Powtarzalność testów oraz obciążenie aplikacji było symulowane za pomocą makra napisanego w języku VBA.

W przypadku testowania niektórych aplikacji ujawniał się problem polegający na tym, że z niewiadomych przyczyn system Windows nie zauważa, iż z testowaną aplikacją powiązany jest symulator FITS, który pełni dla niej rolę debugera. Objawia się to tym, że w niektórych przypadkach informacja o błędzie aplikacji nie jest zwracana do debugera tylko bezpośrednio propagowana do użytkownika systemu operacyjnego. Błąd zostanie wyświetlony za

59

pośrednictwem Narzędzia Raportującego Windows (ang. WER – Windows Error Reporting). Jest to dość uciążliwa sytuacja, ponieważ wymaga od użytkownika dokonania wyboru pomiędzy rodzaj obsługi błędu. Stwarza to kłopot w automatyzacji testów – wymagana reakcja użytkownika lub wydłużenie testów z powodu niepotrzebnego oczekiwania na upłynięcie maksymalnego czasu trwania testu. Na szczęście istnieje możliwość wyłączenia WER. Niestety jest to połowiczne rozwiązanie, które niweluje skutek, a nie przyczynę, jednakże poszukiwanie innego rozwiązania nie przyniosło skutku. W celu wyłączenia WER należy zmodyfikować następujący klucz rejestru:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting] "Disabled"=dword:1 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting] "DontShowUI"=dword:1

Natomiast, aby ponownie aktywować WER należy zaaplikować poniższe ustawienia w rejestrze:

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting] "Disabled"=dword:0 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting] "DontShowUI"=dword:0

Istnieje również możliwość wskazania aplikacji, które mają być wykluczone z raportowania o błędach. W Windows 7 konfiguracja ta znajduje się w podanej ścieżce: “Control Panel\All Control Panel Items\Action Center\Advanced Problem Reporting Settings”.

6.1.1 Wyniki testu porównawczego dla aplikacji jednowątkowej

Ostatnim etapem pracy nad aplikacją FITS było przeprowadzenie testu pomiędzy pierwotnie zaimplementowaną metodą nadzorowania momentu wstrzyknięcia błędu a metodą wykorzystującą do tego celu profiler AMD CodeAnalyst. Poniższe wyniki przeważają szalę na korzyść nowo zaimplementowanej metody, ale należy pamiętać o tym, że oba podejścia istotnie się różnią. Krokowe nadzorowanie aplikacji związane jest z większą ilością istotnych danych oraz możliwością uzyskania lepszego pokrycia testami. Z drugiej strony metoda profilowania znacząco zmniejsza narzut czasowy na testowaną aplikację, a rozkład wstrzyknięć uzyskany tą metodą jest zadowalający, gdyż są one równomiernie rozłożone w czasie wykonania. Do testu wykorzystano niewielki program wykonujący mnożenie macierzy, jako argumenty wejściowe przyjmuje on wymiar macierzy. Z tego względu dwukrotne zwiększenie wartości argumentu zwiększa ilość wykonywanych instrukcji co najmniej czterokrotnie. Dla uproszczenia opisów wykresów przyjęto, że oryginalny sposób pracy FITS oznaczany będzie jego DBG, natomiast zaimplementowany w ramach tej pracy, jako CA. Wyniki przedstawia Rys. 11.

60

Wykonanie przebiegu wzorcowego 1000 802 730 [1] Metoda DBG, argument programu 50

[2] Metoda DBG, 100 140 argument programu 100

[3] Metoda CA, argument 44 programu 500

[4] Metoda CA, argument 10 Czas Czas wykonania [s] programu 1000

6 [5] Metoda CA, argument programu 2000

1

Rys. 11 Porównanie narzutów czasowych podczas wykonania przebiegu wzorcowego

W analizie wykresu z Rys. 11 może pomóc tabela z czasem wykonania aplikacji niepoddanej testowi z przekazanymi argumentami takimi jak na wykresie.

Argument 50 100 500 1000 2000 Czas wykonania [s] 0,006 0,012 0,905 11,527 111 Tabela 10 Czas wykonania aplikacji niepoddanej symulacji błędów

Na podstawie wykresu i tabeli można policzyć procentowy narzut na czas trwania budowy przebiegu wzorcowego. Należy zaznaczyć, że oprócz wpływu samej metody duży wpływ ma również ilość zebranych danych, których w metodzie DBG jest dużo więcej. W przypadku metody CA największy narzut powoduje inicjalizacja sterownika oraz zapisywanie i analizowanie zebranych danych. Widać to wyraźnie pomiędzy 4 a 5 słupkiem: dla 4 zbieranie przebiegu wzorcowego trwało 4 razy dłużej niż aplikacja, a w przypadku testu przedstawionego słupkiem 5 spadek wydajności był 10 krotny. Świadczy to o spadku wydajności struktur danych i algorytmów przetwarzających zebrane próbki. Natomiast dla metody DBG narzut wydajnościowy jest ogromny. Można zauważyć również, że zależy on proporcjonalnie od liczby instrukcji wykonywanych przez testowaną aplikację.

W kolejnym kroku porównano wpływ metod pułapkowania na średni czas trwania testów w eksperymencie złożonym ze 100 testów (pojedynczych wykonań aplikacji testowanej). Wyniki przedstawia wykres na Rys. 12

61

Średnia długość testu 32 [1] Metoda DBG, argument 22,58 programu 50 16 19,13 [2] Metoda DBG, argument

programu 100

8 [3] Metoda CA, argument programu 500, ratio 0,75 6,8 [3] Metoda CA, argument 5,4

4 programu 500, ratio 1 Czas Czas testu[s] 3,42 [4] Metoda CA, argument 2,99 programu 1000, ratio 0,75 2 [4] Metoda CA, argument programu 1000, ratio 1

1

Rys. 12 Średnia długość trwania testu w oparciu o 100 wykonań Chociaż na średni czas długości testu bardzo duży wpływ ma liczba wstrzykniętych błędów, które powodują zamknięcie aplikacji dopiero po upływie maksymalnego czasu (testy z przekroczonym limitem czasowym), to z wykresu na Rys. 12 można wysnuć kilka wniosków. Wyraźnie widać zysk czasu pomiędzy zmianą współczynnik ratio z wartości 0,75 na 1. W pierwszym przypadku jest to zmniejszenie narzutu o 21%, a w drugim o 16%. Nie bez znaczenia na długość testu w szczególności w metodzie DBG może mieć rozkład w czasie wybranych instrukcji, może to być powodem różnicy pomiędzy pierwszym a drugim słupkiem danych.

W ostatnim kroku przeanalizowano efektywność opracowanej metody pułapkowania bazując na zdefiniowanym wcześniej parametrze precyzyjności symulatora. Wyniki przedstawiono na Rys. 13.

62

Precyzyjność symulatora 120 [1] Metoda DBG, argument

100 programu 50

100 100 [2] Metoda DBG, argument 89 programu 100 80 88 86 [3] Metoda CA, argument 75 programu 500, ratio 0,75 60 [3] Metoda CA, argument programu 500, ratio 1 40 [4] Metoda CA, argument

programu 1000, ratio 0,75 Liczbawstrzykniętych błędów 20 [4] Metoda CA, argument programu 1000, ratio 1

0

Rys. 13 Liczba wstrzykniętych błędów podczas 100 testów

Wyraźnie widać, że oryginalna metoda symulatora FITS daje praktycznie pewność na wstrzyknięcie błędu w wytypowaną instrukcję. Dla metody CA skuteczność wacha się od 75% do prawie 90%. Zastanawiająca jest też różnica pomiędzy ostatnim a przedostatnim słupkiem, gdyż zwiększenie współczynnika ratio sugerowałoby zmniejszenie precyzyjności symulatora. Spowodowane jest to tym, że im większy jest współczynnik ratio tym rzadziej sprawdzany jest rzeczywisty stan wykonania testowanej aplikacji. Przez to zwiększa się prawdopodobieństwo, że moment wstrzyknięcia próbki zostanie rozpoznany zbyt późno.

6.1.2 Wyniki testu porównawczego dla aplikacji wielowątkowej

W przypadku testów przeprowadzanych na aplikacji wielowątkowej wyniki są dużo mniej zadowalające niż w przypadku aplikacji jednowątkowych. Uzyskanie mniejszej liczby wstrzykniętych błędów do ogólnej liczby przeprowadzonych testów w stosunku do punktu poprzedniego spowodowane może być kilkoma czynnikami. Od około 15% do 40% procent testów kończyła się niepowodzeniem z powodu błędu podczas pobierania kontekstu wątku w którym miała zostać ustawiona pułapka. Podczas obserwacji działania funkcji systemowej GetThreadContext jedynym zaobserwowanym typem błędu był błąd związany z brakiem dostępu do wątku (Access Denied). Kolejnym powodem może być próba założenia pułapki na instrukcjach wykonywanych na uprzywilejowanym poziomie systemu operacyjnego, dla których to niemożliwe jest ustawienie pułapki przez symulator FITS. Problemu tego można uniknąć przez odfiltrowanie z pliku przebiegu wzorcowego instrukcji, których adres znajduje się w chronionym obszarze pamięci wirtualnej. Niestety odróżnienie instrukcji wykonywanych na różnych poziomach uprzywilejowania nie jest możliwe w aktualnej wersji API CodeAnalyst. Istnieje również możliwość, że powodzenie ustawienia pułapki związane jest w jakiś sposób z rodzajem testowanego modułu. Ostatnia uwaga jest tylko osobiście postawioną tezą niepopartą dostatecznymi testami, którą należy traktować z rezerwą. Czas wykonania testowanej aplikacji niepoddanej eksperymentowi to średnio 4,54 sekundy.

63

Precyzyjność symulatora

25 25 25 25

Ilość testów 16 Ilość udanych wstrzyknięć 13 11 10 Ilość błędów funkcji 9 GetThreadContext

5 4 3

Rys. 14 Rezultaty testowania FITS uzyskane podczas czterech eksperymentów z aplikacją wielowątkową, którą był Word z pakietu Office 2010 z zaimplementowanym makrem umożliwiającym powtarzalność testów

Na Rys. 14 przedstawiono wyniki testu zmodyfikowanego symulatora FITS dla aplikacji wielowątkowej. Niestety nie możliwe było porównanie wyników z oryginalną wersja FITS, gdyż nie oferowała ona wstrzykiwania błędów do aplikacji wielowątkowych. Średnia ilość udanych wstrzyknięć z czterech eksperymentów to około 50%. Jest to bardzo duży spadek w stosunku do wyników uzyskanych dla aplikacji jednowątkowych. W niektórych z wytypowanych instrukcji nie udało się wstrzyknąć błędu z powodu niepowodzenia funkcji GetThreadContext, która wykorzystywana jest do ustawienia pułapki na danej instrukcji. Przyczyna tego problemu nie została do końca zdiagnozowana, ale wyraźnie widać, że jeśli udałoby się go wyeliminować możliwe było by wstrzyknięcie błędów, średnio, do ponad 70% wybranych instrukcji. Na 49 udanych wstrzyknięć błędów w pięciu przypadkach aplikacja zwróciła błędny kod zakończenia, dla takiej samej ilości przypadków zostało wymuszone jej zamknięcie przez system operacyjny, natomiast podczas jednego testu aplikacja została zawieszona i zabita przez FITS.

64

7 Podsumowanie pracy

7.1 Osiągnięte cele

Głównym celem pracy było zwiększenie funkcjonalności oraz wydajności symulatora błędów FITS. Do realizacji tego zadania wykorzystano interfejs programistyczny systemu profilującego AMD CodeAnalyst. Uważam, że postawiony cel został osiągnięty, o czym może świadczyć działająca aplikacja. Na łamach tej pracy przedstawione zostały również pewne aspekty optymalizacji oprogramowania oraz ogólna klasyfikacja i sposób działania aplikacji profilujących. Zamieszczono również bliższe wprowadzanie do aplikacji AMD CodeAnalyst.

Podsumowując, oba zagadnienia, które poruszyła ta praca tj. symulowanie błędów oraz profilowanie aplikacji, są bardzo interesujące. Udowodnione zostało również, że profilery można wykorzystać do całkiem innych celów niż przewidzieli to ich twórcy – niezmiernie w tym ułatwiła dostępność API. Pomimo, że na polu obu tych zagadnień dokonano już bardzo wiele, to możliwości nowych, eksperymentalnych implementacji są jeszcze duże. Jednakże pracę w tych dziedzinach uważam również za dość wymagającą, ponieważ wymaga ona zarówno znajomości rozwiązań sprzętowych oraz niskopoziomowej abstrakcji programowania i systemów operacyjnych.

7.2 Napotkane trudności

Główne trudności polegały na ogromie wiedzy dotyczącej profilowania aplikacji i optymalizacji oprogramowania. Napisanie spójnej i logicznej pracy wymagało przyswojenia dużej ilości dokumentów, chociaż w mojej ocenie ilość informacji na niektóre tematy, choć trochę poruszone w tej pracy jest ogromna i niemożliwa do śledzenia na bieżąco przez jednego człowieka. Do trudności zaliczyłbym również wielkość projektu aplikacji FITS, która składa się z wielu plików źródłowych.

7.3 Możliwości rozwoju

Do możliwości dalszego rozwoju zaliczyłbym głównie rozwój implementacji AMD CodeAnalyst w aplikacji FITS. W tym, między innymi:  obsługa wielu procesów – dzięki czemu można będzie testować aplikacje odtwarzające działanie użytkowników;  implementacja pozostałych trybów analizy AMD CodeAnalyst;  praca nad zwiększeniem precyzji wstrzykiwania błędów – szczególnie w aplikacjach wielowątkowych;  zmniejszenie narzutów powodowanych przez sterownik profilera poprzez modyfikację algorytmów i struktur danych;  analiza kodu maszynowego testowanych aplikacji i generacja dodatkowych instrukcji do potencjalnego wstrzyknięcia na podstawie bloków kodu zawartych pomiędzy dwoma rozgałęzieniami sterowania.

Istotnym jest też aktualizowanie stanu wiedzy na temat aplikacji AMD CodeAnalyst, gdyż w miarę publikacji nowych wersji rozbudowywane może zostać jej API.

65

Zawartość płyty CD

Do pracy dołączona została płyta CD, na której zamieszczono materiały związane z realizacją pracy inżynierskiej. Zawartość płyty jest następująca:

 Tekst pracy w formacie PDF;  Projekt MS Visual Studio 2010 zawierający kod źródłowy aplikacji FITS wraz z modyfikacjami implementującymi mechanizmy AMD CodeAnalyst;  Pliki z danymi zebranymi podczas testów;  Źródła ilustracji oraz wykresów;  Plik instalacyjny AMD CodeAnalyst w wersji 3.8 dla 32 i 64 bitowych systemów Windows;  Kopię strony www oraz źródła i pliki binarne pakietu narzędziowego Performance Inspector.

66

Bibliografia

[1] Ben Wu, Survey of Software Monitoring and Profiling Tools [2] SMART BEAR, Static Analysis Profiler – Overview, http://support.smartbear.com/viewarticle/18240/ [3] Gprof – The GNU profiler, http://www.cs.utah.edu/dept/old/texinfo/as/gprof.html#SEC12 [4] Boogerd C. i Moonen L. (2008). On the Use of Data Flow Analysis in Static Profiling. Proceedings of the 8th IEEE International Working Conference on Source Code Analysis and Manipulation [5] Drogonowski P. (2007). Instruction-Based Sampling: A New Performance Analysis Technique for AMD Family 10h Processors. [6] Fog A. (2012). Optimizing software in C++: An optimization guide for Windows, Linux and Mac platforms. [7] Gawkowski P. (2007). Opis systemu wstrzykiwania błędów FITS-3.1. [8] Wu Y., Larus J. R. (1994). Static branch frequency and program profile analysis. MICRO 27 Proceedings of the 27th annual international symposium on . [9] AMD. (2008). BIOS and Kernel Developer’s Guide (BKDG) For AMD Family 11h Processors. [10] Fog A. (2012). The microarchitecture of Intel, AMD and VIA CPUs: An optimization guide for assembly programmers and compiler makers. [11] Chip Architect: Understanding the detailed Architecture of AMD's 64 bit Core, http://www.chip- rchitect.com/news/2003_09_21_Detailed_Architecture_of_AMDs_64bit_Cor e.html [12] Swehosky F. (2011). CodeAnalyst APIs. [13] Hsueh M., Tsai T., Iyer R. (1997) Fault Injection Techniques and Tools. [14] Alfredo Benso, Paolo Prinetto (Editors), (2003). Fault Injection Techniques and Tools for Embedded Systems Reliability Evaluation, Springer [15] Jeffrey M. Voas, Gary McGraw, (1998). Software Fault Injection, Wiley

67

[16] Drogonowski P. (2008). An introduction to analysis and optimization with AMD CodeAnalyst Performance Analyzer. [17] CodeAnalyst User's Manual (2012) http://developer.amd.com/wordpress/media/2012/10/CodeAnalyst- Linux_Users_Manual-3.4.pdf [18] Krystosik A. ECSM - Extended Concurrent State Machines, Institute of Computer Science, WUT, Research Report nr 2/2003 [19] Jurkiewicz R. (2000) Testowanie potoku przetwarzania instrukcji procesorów INTEL P6, Politechnika Warszawska, praca inżynierska, Wydział Elektroniki i Technik Informacyjnych, Instytut Informatyki [20] Intel, Intel 64 and IA-32 Architectures Optimization Reference Manual (2012) [21] Gawkowski P. (2005) Analiza i zwiększanie odporności na błędy aplikacji programowych w systemach z elementami COTS. Rozprawa doktorska, Politechnika Warszawska, Wydział Elektroniki i Technik Informacyjnych [22] Włodawiec P. (2002) System symulacji błędów MTInjector, Politechnika Warszawska, praca magisterska, Wydział Elektroniki i Technik Informacyjnych, Instytut Informatyki [23] Smulko G. (2009) Optymalizacja techniki programowej symulacji błędów. Politechnnika Warszawska, praca inżynierska, Wydział Elektroniki i Technik Informacyjnych, Instytut Informatyki [24] Gawkowski, P.; Smulko, G.; , "Speeding-up fault injection experiments with dynamic code injection," Information Technology (ICIT), 2010 2nd International Conference on , vol., no., pp.171-174, 28-30 June 2010 [25] Sosnowski J. (2005) Testowanie i niezawodność systemów komputerowych, Akademicka Oficyna Wydawnicza EXIT

68

69