Blog pisany [prawie] codziennie o tym, czego się dzisiaj nauczyłem, o wszystkim związanym z programowaniem.
Testowanie kodu
Pobierz link
Facebook
X
Pinterest
E-mail
Inne aplikacje
Już pisałem o testowaniu różnic między LinkedList, a ArrayList, dzisiaj napiszę o tym jak otrzymałem narzędzie do bardziej dogłębnego testowania różnic między różnymi rzeczami.
Jeszcze tego dokładnie nie testowałem, ale wygląda dosyć ciekawie. Ten sposób jest z tej strony:
Jak przyspieszyć nasz kod w Javie? Czyli kilka słów o JMH.
Witam Cię bardzo serdecznie!
Dziś zapraszam na tematy typowo Javowy, wchodzący trochę głębiej z zagadnienia JVMa. Opiszę narzędzie, przy pomocy którego będziemy mogli zmierzyć szybkość wykonywania naszego kodu i dzięki wyciągniętym wnioskom przyspieszyć nasze programy w Javie. 🙂
JMH
Java Microbenchmark Harness, o tym będzie dziś mowa, jest narzędziem do tworzenia benchmarków w Javie, czyli do badania wydajności fragmentów naszej aplikacji. Tak na marginesie, „harness” to po angielsku uprząż. Innymi słowy, zakładamy uprząż na konia (w tym przypadku na Javę) i sprawdzamy jak szybko możemy na nim pojechać. 🙂
JMH zostało napisane przez Aleksey Shipilёv’a, który pracował w Oracle i jest współtwórcą JITa. (Tak, tego JITa, który optymalizuje nasz kod w JVMie). Nie jest to narzędzie pierwszej świeżości, ma już kilka lat, ale wciąż nie jest dobrze znane i raczej rzadko używane przez programistów. Postaram się zatem przekonać Cię, że warto JMH przynajmniej poznać. 🙂
Po co w ogóle badać wydajność?
Zanim odpowiemy na pytanie JAK? zadajmy sobie pytanie PO CO? W zasadzie powodów jest kilka:
Porównanie dwóch różnych implementacji tej samej funkcjonalności. Jeżeli mamy kilka bibliotek, służących do tego samego, możemy zmierzyć, która z nich działa najszybciej. Weźmy na przykład problem parsowania stringa do JSONa. Bibliotek, które to robią w Javie jest wiele. Ale która z nich jest lepsza do małych plików, a która do dużych? Która szybciej będzie parsować bardziej płaskie struktury, a które te wielokrotnie zagnieżdżone? Warto zmierzyć wydajność dostępnych bibliotek i wybrać taką, która najlepiej się sprawdzi w naszym konkretnym przypadku.
Sprawdzenie czy nasz algorytm nie trwa zbyt długo. Warto zmierzyć ile czasu trwa wykonywanie kluczowych fragmentów naszej aplikacji. Może się okazać, że operacja pozornie szybka, zajmuje dużo więcej czasu niż się spodziewamy i spowalnia nasz program. JHM pozwala nam namierzyć takie miejsca, a następnie wyeliminować.
Kiedy nie starcza nam zasobów. Tu mogą pojawić się głosy sprzeciwu – przecież dzisiaj sprzęt jest tani. Jeśli nasz program działa zbyt wolno to bardziej opłaca jest dorzucić trochę sprzętu, kilkadziesiąt serwerów i problem rozwiązany. Pozornie tak jest, jednak mimo wszystko nie zawsze jest możliwość powiększenia naszej infrastruktury. Niekoniecznie też jest to tanie. Jeżeli rozważymy środowisko chmur obliczeniowych okaże się, że maszyny wirtualne są relatywnie jedną z droższych usług. Co więcej, wchodzimy teraz intensywnie w świat Serverless. Przykładowo, usługa AWS Lambda, która uruchamia naszą funkcję w nieznanym nam środowisku, może wykonywać się maksymalnie 5 minut. Szkoda by było, gdyby nasz kod nie zdążył w tym czasie wykonać się w całości…
Jako broń w „świętej wojnie”. 🙂 Jeżeli kiedykolwiek prowadziliście dyskusje nad wyższością Javy nad innymi językami programowania, to JMH jest w stanie dostarczyć Wam twardych dowodów na poparcie Waszej tezy. Możecie zmierzyć czas trwania jakiejś operacji i wykazać jaka to Java jest szybka. 😉
Takie liczby świetnie też działają marketingowo. Bo przecież przed refactorem nasz kod uruchamiał się 5x czasu, a teraz kończy się w mniej niż x! Dzięki temu nasza aplikacja działa szybciej i możemy pozyskać większą ilość klientów.
Zasadniczo pisanie banchmarków w JMH jest proste i przypomina trochę pisanie testów. Musimy jedynie oznaczyć naszą metodę adnotacją @Benchmark. Przykładowo:
Gdy JMH zakończy pomiary zobaczymy komunikat z podsumowaniem i wynikami naszych badań.
Benchmark krok po kroku
W pierwszej fazie, JMH przeprowadza rozgrzewkę i wykonuje nasz test kilka razy, ale nie mierzy jeszcze wyników. Dzieje się tak dlatego, gdyż JVM musi nabrać troszeczkę rozpędu. W drugiej kolejności, następuje faza pomiaru, podczas której nasz kod jest uruchamiany wielokrotnie i mierzony jest czas każdego wykonania. Na końcu tworzony jest nowy proces, w którym cały schemat jest powtarzany od początku.
Dlaczego tworzymy nowy proces? Wyobraźmy sobie, że testujemy dwie różne implementacje tego samego interfejsu. Dopóki tylko jedna z nich jest używana, JIT zastąpi wołanie interfejsu poprzez bezpośrednie wywoływanie metody go implementującej. Kiedy jednak natrafi na drugą implementację nie będzie mógł tak robić. W rezultacie pomiar drugiej implementacji będzie wolniejszy. Żeby pozbyć się tego „efektu pamięci” tworzymy właśnie nowy proces dla każdego nowego pomiaru.
Ilość procesów oraz fazy „warm-up” i „measurement” możemy kontrolować przy pomocy odpowiednich adnotacji:
@Benchmark @Fork(value =1)// test powtarzamy tylko dla jednego procesu @Warmup(iterations =1, time =2)// jedna iteracja rozgrzewkowa trwająca 2 sekundy @Measurement(iterations =2, time =2)// dwie iteracje pomiarowe, każda trwające 2 sekundy publicint benchmark()throwsInterruptedException{ Math.log(x); return0; }
Test modes
Ustawiając tryb testowania możemy mierzyć:
Throughput – ilość wykonanych operacji w jednostce czasu
Average time – średni czas wykonania operacji
Sample Time – czas wykonania operacji; w tym miejscu uwzględniamy statystyki, percentyle itp
SingleShot Time – nasza operacja jest uruchamiana raz i mierzony jest czas tego wykonania
Jednostki czasowe
Nasz wynik możemy wyrazić w następujących jednostkach czasowych:
NANOSECONDS
MICROSECONDS
MILLISECONDS
SECONDS
MINUTES
HOURS
DAYS
State
Stan jest dość ważnym konceptem w JMH. Zazwyczaj nasza funkcja, którą mierzymy operuje na jakichś danych. Często też te dane w jakiś sposób trzeba przygotować. Nie chcemy jednak mierzyć czasu przygotowywania tych danych. W takim właśnie przypadku używamy klasy State.
Przykład poniżej:
publicclass MyBenchmark {
@State(Scope.Thread) publicstaticclass MyState { publicint a =1, b =2; publicint sum;
Klasa MyState zawiera zmienne „a”, „b” oraz „sum”. Instancja tej klasy jest podawana jako argument do testowanej funkcji.
Tak samo jak np w JUnit, klasa State może zawierać metody @Setup i @TearDown.
Nic nie stoi na przeszkodzie, żeby cała klasa, w której znajdują się testowane metody była oznaczona @State:
@State(Scope.Thread) publicclass MyBenchmark { publicint a =1; publicint b =2;
@Benchmark @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.MINUTES) publicvoid testMethod(){ int sum = a + b; } }
Jak pisać dobre benchmarki i na jakie pułapki uważać?
Zasadniczo są 3 pułapki na które należy uważać podczas pisania benchmarków. Wynikają one ze specyfiki pracy JITa, który poprzez rozmaite optymalizacje stara się, żeby nasz kod działał jak najszybciej. Nie zawsze jest to zgodne z naszym celem, czyli faktycznym mierzeniem wydajności. Już tłumaczę o co chodzi.
1. Dead Code Elimination
Prosta sprawa. Jeżli JIT zauważy, że nasza metoda nie ma żadnego wpływu na otoczenie, może uznać, że nie ma sensu jej wykonywać ipo prostu tego nie robić. Przykładowy, źle napisany benchmark mógłby wyglądać tak:
@Benchmark publicvoid benchmarkBad(){ double a =Math.log(x); double b =Math.log(y); double c = a + b; }
Wystarczy jednak, że nasza funkcja zwróci pewną wartość i JIT już nie będzie miał argumentów, żeby ją zoptymalizować:
@Benchmark publicdouble benchmarkOK(){ double a =Math.log(x); double b =Math.log(y); double c = a + b; return c; }
Co w przypadku, kiedy chcielibyśmy wykonać kilka operacji w jednej funkcji? Nie możemy zwrócić przecież kilku wartości… W takim przypadku JMH udostępnia obiekt czarnej dziury, który zapobiega optymalizacjom za strony JITa:
@Benchmark publicvoid benchmarkOK(Blackhole bh){ double a =Math.log(x); double b =Math.log(y); double c = a + b; bh.consume(c); bh.consume(a - b); }
Dlatego pamiętajmy, że nasza funkcja musi zawsze zwracać wartość, albo wrzucać ją do czarnej dziury. 🙂
2. Optymalizacja pętli
JIT jest świetny w optymalizacji pętli. Zarówno FOR jak i WHILE. Wykonanie obu poniższych fragmentów kodu zajmie tyle samo czasu:
@Benchmark publicint benchmark(){ int sum =0; for(int i =0; i <100000; i++){ sum++; } return sum; }
@Benchmark publicint benchmark2(){ int sum =0; for(int i =0; i <10; i++){ sum++; } return sum; }
Dlatego pamiętajmy o drugiej zasadzie, żeby unikać pętli w benchmarkach.
3. Constant folding
Co się kryje pod tym tajemniczym hasłem? Zobaczmy to na przykładzie:
W pierwszym przypadku JIT zobaczy, że funcja operuje na stałych. Nie ma więc sensu liczyć ciągle tego samego, kod zostanie więc skrócony. W drugim przypadku zmienna x pochodzi spoza funkcji, z klasy State. Dlatego JIT nie będzie optymalizować tej funkcji.
Podsumowując, zmienne, na których działa benchmark powinny zawsze pochodzić z klasy State.
Większy przykład z życia
Rozpatrzmy zatem jakiś konkretny przykład. Załóżmy, że chcemy parsować stringi na typy numeryczne (long i double). Porównamy więc metodę natywną z Javy, z klasy Long – Long.valueOf() oraz metodą Longs.tryParse() z biblioteki Google Guava. Która z nich okaże się szybsza?
Żeby test był kompletny zakładamy, że string nie musi być poprawną liczbą. Dlatego używamy metody Longs.tryParse(), która w przypadku błędu zwróci nulla. Z kolei Long.valueOf() otoczymy blokiem try-catch. Poniżej kod całego testu:
Żeby być super poprawnym powinienem w metodach „JDKlongOK” oraz „JDKdoubleOK” również użyć blocku try-catch, jednak w tym przypadku jego użycie nie wpływa na wydajność kodu.
Wyniki
Wykananie powyższych benchmarków zajęło mojemu średniej klasy laptopowi niecałą godzinę. Poniżej tabelka z wynikami parsowania 4 stringów zarówno przy pomocy funkcji z JDK jak i z Guavy:
JDK
Guava
long OK („123456”)
16 971 583 ops/s
14 266 048 ops/s
long Bad („123#45”)
343 430 ops/s
24 741 318 ops/s
double OK („123.4567”)
11 405 944 ops/s
1 498 003 ops/s
double Bad („123#456”)
276 102 ops/s
1 747 459 ops/s
Wyniki są ciekawe i wcale nie są jednoznaczne.
Dla poprawnego longa obie implementacje działają podobnie. JDK jest minimalnie szybsze, ale nie jest to duża różnica. Dla niepoprawnego longa zdecydowanym zwycięzcą jest Guava. W sumie trudno się dziwić – rzucanie wyjątków jest kosztowne, stąd tak słaby wynik dla JDK.
Możemy też zauważyć, jak szybko Guava poradziła sobie z niepoprawnym stringiem. Dlaczego? Jeśli zajrzymy w kod źródłowy zobaczymy, że funkcja analizuje kolejne znaki w stringu. Przerywa działanie, gdy napotka na niepoprawny symbol. Dlatego jeślibym miał parsować jedynie longi, zdecydowałbym się na Google Guavę.
Źródło: wykop.pl
Inaczej sytuacja wygląda w przypadku liczb zmiennoprzecinkowych. W JDK jest duża różnica między czasem parsowania poprawnego stringa i niepoprawnego. Z kolei Google Guava rozczarowuje. W obu przypadkach funkcja jest raczej wolna. Dlaczego? Okazuje się, że Google Guava używa wyrażenia regularnego do sprawdzenia poprawności parsowanego stringa. Używanie RegExpów jest kosztowne i stąd słaby wynik benchmarku.
W przypadku doubli, na korzyść Guavy przemawia taki sam czas parsowania niezależnie od poprawności stringa. Dlatego wybór między JDK a Guavą dla parsowania liczb zmiennoprzecinkowych uzależniłbym od ilości stringów niepoprawnych w stosunku do ilości poprawnych, które mamy do przeparsowania.
I to by było na tyle 🙂
Mam nadzieję, że przynajmniej zaintrygowałem Cię tematem wydajności w Javie. Zachęcam, żeby trochę się tym pobawić i nabrać intuicji. Warto być świadomym programistą, który ma choćby podstawowe pojęcie o wydajności swojego kodu.
Tymczasem, do zobaczenia i…
NIECH KOD BĘDZIE Z TOBĄ!
Bardzo mnie interesuje co o tym sądzisz, dlatego byłoby mi miło, jeśli byś napisał w komentarzu coś o tym, może być cokolwiek :)
Na Staku znalazłem ciekawe wyjaśnienie tego, jak dla mnie wielokrotnego już pytania: It's as simple as Ctrl + mouse wheel . If this doesn't work for you, enable File → Settings → Edito r → General → (checked) Change font size (Zoom) with Ctrl+Mouse Wheel . koniecznie daj znać, czy to Ci się przydało :)
Mógłbym dużo opisywać, ale lepiej wkleić filmik i klikać tak samo :) To tyle ode mnie na dziś, zapraszam Cię do dzielenia się swoimi wrażeniami z tego posta, lub np. swoim dzisiejszym odkryciem np. w komentarzu :)
/* Zgodnie z https://www.centrumxp.pl/Publikacja/Jak-nagrac-zawartosc-ekranu-w-Windows-10-bez-dodatkowych-programow */ Jak przygotować nagrywanie ekranu? Najpierw musimy się upewnić, czy włączony jest u nas DVR z gry . Na większości komputerów powinien być, choć starsze maszyny mogą nie wspierać tej funkcji. Wchodzimy do Ustawień > Granie > Pasek gry i upewniamy się, że przy opcji Rejestruj klipy z gry oraz zrzuty ekranu i emituj je za pomocą paska gry ustawienie jest Włączone . Domyślnym skrótem, by rozpocząć/zatrzymać nagrywanie, jest Win+Alt+R . Możemy (ale nie musimy) wprowadzić też własny skrót. W tym celu zjeżdżamy niżej, klikamy pole obok napisu Twój skrót i wykonujemy taki skrót na klawiaturze (przykładowo może to być Shift + P). Teraz przechodzimy do zakładki DVR z gry . Warto zwrócić uwagę na kilka ustawień. Jeśli chcemy, aby nagrywany był dźwięk, upewniamy się, że włączona jest opcja Rej...
Komentarze
Prześlij komentarz