Transkrypcja
Wstęp – ciąg dalszy z poprzedniego odcinka
Cześć Wojtek!
Cześć, Michał!
Chyba wypada dzisiaj opowiedzieć o drugiej części naszej wydajności, a jak już powiedzieliśmy “a”, to powiedzmy “cześć, wydajność – ciąg dalszy”! W poprzednim odcinku bardzo ładnie opowiadałeś o różnych projektach, które tam w świecie firmowym się odbywają. Tych takich, co to trwają i trwają, ale w końcu wypluwają jakieś takie dobre efekty. Miejmy nadzieję.
I to można by mieć nadzieję, że właśnie, jak już się pokażą, to będziemy mieli dużo lepsze narzędzia do tego, żeby jednak te jabłka nam – w sensie nasze wyroby duchowo podobne – nie zmalały tak bardzo, jak zmalały kiedyś, gdzieś tam, dawno temu. Ale może niekoniecznie to jest wszystko, co moglibyśmy zrobić, bo tak naprawdę ci goście, którzy tam naprawiają nam tę Javę, chyba podobne techniki stosują, jak my byśmy mogli zastosować w naszych już aplikacjach. Czy to sam język, który wiadomo, można zoptymalizować na poziomie kompilatora, odrobinę można na poziomie runtime’u, można na poziomie tych zaawansowanych mechanizmów. Java ma też optymalizować, ale przede wszystkim można go nie zepsuć w kodzie źródłowym, naszym.
No tak, to pewnie jakiś tam punkt wyjścia do dobra podstawa, żeby ten kod chociaż był sam z siebie dobry. I rzeczywiście, faktycznie, jeśli nie chcemy czekać pięciu lat, aż nam wyjdzie Project Leyden czy inne projekty, albo jeśli nie mamy możliwości, żeby przemigrować na jakieś frameworki, które są szybsze, no to faktycznie możemy zrobić krótki przegląd tego, co mamy teraz do dyspozycji i co właściwie możemy zrobić, co powinniśmy robić i w ogóle jak to mierzyć.
Nie tyle na frameworki, czy wręcz, czy wiem, że na szybsze cacka wokół JVM-a, co wręcz możemy czasem nie móc. Bo na przykład mamy wymaganie, że Java 8 ma być i nie ma, bo jedenastka ostatecznie przyznaje, że cacka często się wiążą z nowszą zabawką, tak jak nowa buta Trójki i Javy 17. Trzeba by było więc tę Javę podbijać, a tu się okazuje, że kierownik przychodzi i mówi: “Nie ma być to Java i koniec kropka!”. Bo jak nie, to on wszystko wybuchnie.
Wystarczy, żeby w organizacji była wersja wspierana ze względów utrzymaniowych, na zasadzie, że team utrzymania na produkcji nie ma doświadczenia, nie ma jakiś git-booków. Jak tu utrzymywać lepszą wersję, wyższą wersję? Można nie wiedzieć, że jest jakaś wyżej jeszcze Java niż 1.8. Już to się też często nie podpisze, albo w wersji baz danych czy czegoś. No jednak nie jesteśmy samotną wyspą. Nasz projekt nie jest.
Bo boją się, że w Windowsie XP, na którym wszyscy tam jeszcze siedzą, to w ogóle już nie zniknie.
Tak, są też takie przypadki.
Teoria działania kodu i optymalizacje
Dobra, czyli zanim dojdziemy może do tych dobrych praktyk – może niekoniecznie najlepszych, ale przynajmniej tych dobrych – to jeszcze warto by było kilka słów powiedzieć tej teorii, co do której się odgrażaliśmy i przypomnieć po prostu kilka pojęć, czyli na przykład takie coś, jak model wykonywania kodu. Jak to wygląda w ogóle od strony, od kuchni? Mamy ten komputer i on ma tam jakiś procesor? I jak to się dzieje, że ten kod, który wyprodukuje nam to czy inne narzędzie, on jakoś tam się jednak wykona na tym procesorze? No to proszę bardzo, to ja.
No to procesor – takie cacko, które wykonuje instrukcje. Logicznie rzecz ujmując, jedna po drugiej. Ale nie musi wcale tak być i to będzie miało znaczenie później przy tych naszych rozważaniach. Ale możemy sobie taki idealny procesor, jakiś taki bardzo prosty, zamodelować w taki sposób, że mamy ciąg instrukcji, które on rozumie do wykonania, zapisane gdzieś tam w pamięci programu. No i mamy decyzję, jaki sposób podawania tych instrukcji do procesora. Procesor ma jednostkę logiczną i tam sobie bierze te instrukcje po kolei. Ciach, ciach, ciach, coś tam interpretuje każdą z nich i coś tam sobie robi jakąś tam. No ten nasz wynik tego całego programu, który mamy skompilowany. Ten program musi być skompilowany po to, żeby procesor mógł sobie te instrukcje maszynowe łapać i żeby mógł zgodnie z nimi pracować. On tak naprawdę jest taką maszyną. Program razem z nim. To jest taka maszyna stanu, troszkę nieudolny, z takim elementem wykonującym tenże algorytm dla maszyny stanu, bo każda instrukcja opisuje przejście z jakiegoś stanu do jakiegoś tam innego, jakąś czynność. Chociażby odczytania jakiejś wartości z pamięci do rejestru. Wykonanie, na przykład, operacji arytmetycznej na tym, co w tym rejestrze było i wykrycie tego gdzieś tam, żeby to się gdzieś zapisało. Oprócz tego jakieś tam inne jeszcze instrukcje może sobie wykonywać. No i właśnie, wykonując ciąg tych instrukcji, powolutku wykona nam cały program i teraz wywali.
Lub się wywali.
Często się wywali, wiadomo, ale jak się wywali, to poprawiamy i znowu. No ale mamy różnice w tych modelach wykonywania kodu między na przykład Javą a resztą świata. Kompilowane, zresztą kompilowane do kodu natywnego może, bo Java się nie kompiluje do kodu natywnego, tylko do bytecode’u, czyli zamiast instrukcji dla danej architektury sprzętowej. Czyli na przykład jesteśmy na architekturze x86, procesory Intela, AMD. One rozumieją swoje instrukcje. Nie będą rozumiały instrukcji dla rodziny RISC, na przykład, i w ogóle nie będą rozumiały bytecode’u. Natomiast maszyna wirtualna emuluje nam taki procesor do poziomu rozumienia bytecode’u tak, jakby były instrukcjami maszynowymi takiego właśnie wyimaginowanego procesora domowego.
Nota bene są też zresztą mikrokontrolery albo procesory też, które mają interpreter bytecode’u zaszyty sprzętowo. Jednak można też nawet na sprzęcie wykonać, ale są to takie niszowe zastosowania. No i największa różnica między takim kodem, takim modelem skompilowanym statycznie versus dynamicznie, czyli skompilowanym do kodu. Kodem źródłowym, skompilowanym do kodu maszynowego dla danej architektury, a kodem źródłowym Javy skompilowanym do bytecode’u jest taka, że ten pierwszy wykonuje się na sprzęcie, a ten drugi w maszynie.
Wiem, wiem. I teraz jako że procesory poszły sobie do przodu, już tam cały czas sobie idą do przodu, cały czas tam się coś w nich ulepsza i teraz są już niewyobrażalnie wręcz szybkie.
Ale i tak będzie zamulać.
A ten trzeci to jednak nie tak szybko działa. Na mnie trochę działa, trochę działa, ale czasem jeszcze zamula i czasem bluescreeny jeszcze lecą, ale może to jest kwestia niedomodowania go do końca, bo zbyt dużego, zbyt dużego zbudowania. To może być kwestia tego, że tak powiem, upałów, bo taki sierpień czy lipiec ma to do siebie, że te screeny lecą częściej.
No ale właśnie model wykonywania C a model Javy to dwa różne światy. Wróćmy do tego javowego. Więc mamy sobie kod skompilowany dynamicznie, który jest ciurkiem instrukcji bytecode’owych dla Javy i tam raczej nie ma możliwości podmiany kolejności tych instrukcji. Po prostu to, co mamy zapisane w pliku klasy dla danej metody, ma się wykonać na naszym wirtualnym, docelowym procesorze lub na naszej wirtualnej maszynie. Natomiast kod skompilowany statycznie, taki C++-owy, na przykład, który jest dedykowany dla platformy x86, to już niekoniecznie. Musimy mieć pewność, że on faktycznie będzie miał instrukcje poukładane tak, jak mieliśmy to w zamyśle, czyli tak, jak by to wyglądało z takiego najsłabszego, pierwszego przeskalowania po nim kompilatorem. Niekoniecznie ten kod będzie poukładany w ten sposób, ponieważ instrukcje różnego typu mają różną długość i kompilator może je zechcieć optymalizować, zamieniając po prostu ich kolejność tak, żeby pewna taka równoległość wykonywania, która jest dostępna w procesorach tych instrukcji, mogła zostać użyta.
Czyli na przykład instrukcje, które wykonują się dłużej, będą sobie w innej kolejności wykonywane niż te, które się wykonają szybciej. Poza tym one tam mogą się oprócz tego, że zamieniać kolejnością. To jeszcze instrukcja, która chyba ciągnie coś z pamięci, jeśli dobrze nie wiem, tutaj konfigurować, ale wydaje mi się, że niektóre instrukcje mogą na wzór wątków. Podobnie też czekać na, znaczy wykonywać się tak jakby w tle.
Tak, generalnie też jest pewne przewidywanie, na przykład w kontekście odczytywania kolejnych bloków pamięci. Jeśli mamy poszczególne operacje, które czytają sobie kawałki pamięci w miarę, powiedzmy, tak samo. Otrzyma jakiś tam, jakiś tam offset. Procesory potrafią sprytnie przewidzieć, że aha, czyli następny kawałek pamięci to będzie ten + 4 tam 4 offset czy 4? Co dalej? Więc z pewnym przewidywaniem gdzieś tam w tle jest robione, więc dużo takich optymalizacji.
Tak, bo to chodzi o wszystko, wyciąganie kodu, instrukcji w locie, żeby to się wszystko ładnie spisało. Czyli tutaj to przewidywanie instrukcji jest bardzo ważne dla procesorów i ważne jest, żeby kompilatory też to rozumiały i żeby wypływały kod w takiej kolejności, te instrukcje już maszynowe, żeby one były ułożone w takiej kolejności, żeby dało się bardzo, jak najbardziej efektywnie wciągać do maszynki i wykonywać.
Just-in-Time Compilation i Hotspoty
No i teraz co tam z tą Javą? No Java nie ma możliwości zamiany kolejności instrukcji, więc taka maszyna wirtualna mogłaby sobie popatrzeć na to, jak to się wykonuje i zechcieć, na przykład, sobie zamienić w locie coś tam takiego, i okazuje się, że ona umie to zrobić nawet. I to się nazywa Just-in-Time (JIT) kompilatorem. Tylko że wymaga to takiego czegoś, o czym chyba też już wspominaliśmy, tak na razie po cichu, co się nazywa rozgrzewanie maszyny JVM.
Ta zdolność to jest taka jej zdolność do podglądania tego, co właściwie się dzieje w programie. Każdy program jest wiadomo, inny, specyficzny. I każdy ten ciąg instrukcji musi zostać najpierw obejrzany przez maszynę po to, żeby ona mogła sobie wytypować kawałki, które się wykonują częściej. Po to, żeby na przykład te kawałki zechcieć wrzucić do pamięci kodu, skompilować i wrzucić do takiego natywnego kodu, który będzie uruchamiany. Zamiast tego bytecode’u, który był oryginalnie. To się nazywa Just-in-Time Compilation.
A takie właśnie te najgorętsze kawałki noszą nazwę hotspotów. Stąd między innymi jest nazwa Oracle HotSpot maszyny wirtualnej Oracle.
Czyli jednak coś, co potrafi w natywnym języku się porozumieć.
No potrafi bardzo. Oszukiwali nas na początku, mówiąc: “Razem od bytecode’u napisz raz i odpal wszędzie”. No to wielka lipa. Tak naprawdę to tak jest, bo zanim te hotspoty zostaną wykryte i ten… I zostanie to skompilowane do postaci natywnej, to i tak się to wykonuje w interpreterze JVM jako obiekt.
Tutaj też warto. Warto zauważyć, że nie mamy zbytnio nad tym kontroli, nad tym, kiedy faktycznie te hotspoty zostaną wykryte. No bo to zależy od tak naprawdę kontekstu użycia, od wielokrotności użycia pewnych, pewnych metod, i dopiero te hotspoty, czyli te metody, które są często wykonywane, one rzeczywiście będą gdzieś tam skompilowane do natywnego kodu.
Warto również pamiętać, że są pewne ograniczenia, więc jeśli mówimy o kompilacji metod takich hotspotowych, na przykład, takim ograniczeniem jest rozmiar bytecode’u takiej metody. I tym rozmiarem jest 8 kilobajtów. Jeśli nasz bytecode naszej metody, czyli metody już po skompilowaniu do… do kodu dużej JVM-ki przekracza 8 kilobajtów, to jest parametr, może oczywiście skonfigurować, ale domyślnie, jeśli to przekroczymy, to ta metoda nigdy nie zostanie skompilowana do natywnego.
Mało tego, jeszcze maszyna wyśle maila do wujka Boba, on przyleci skarcić, bo metody miały być krótkie.
Tak, metoda miała być. To jest jeden z powodów też. Generalnie hotspoty są zwykle metodami krótkimi.
Ano bo to są najczęściej metody, często albo pętle, albo pętle. Więc jeśli metoda jest jakaś długa, to raczej są małe szanse, że ona rzeczywiście będzie hotspotem i będzie wykonywana wielokrotnie, więc są to pewne ograniczenia, które też tam warto. Warto wiedzieć, jeśli oczekujemy, że coś tam zostanie skompilowane i będzie szybciej.
A wcale nie jest.
To czy zostanie skompilowane, czy nie, to to jest też kwestia tego, jak często się to wykonuje. Bo jeżeli to się wykonuje raz, jest to jakiś kod inicjalizacji albo jest po prostu to tak rzadka funkcjonalność, że człowiek widzi to jako raz na jakiś czas, to na tle innych rzeczy, które ma po prostu cały czas, po prostu oleje ten fragment, bo mu się nie będzie chciało tego kompilować i też nie ma to najmniejszego sensu.
No tak, tu musi zostać przeprowadzona analiza, która pokaże częstość wykonywania poszczególnych fragmentów. Czyli wykonując poszczególne fragmenty, JVM będzie sobie tam odkładał dane i po czasie tym zwanym czasem rozgrzewania maszyny już będzie wiedział, które kawałki kodu można podmienić. Przy czym wydaje mi się, że nawet jak już się rozgrzeje, to i tak później jeszcze w locie może sobie też coś podmieniać, bo na przykład mogą mu się statystyki tego kodu zmienić i program przejdzie w inny profil, na przykład, i wtedy będzie wykorzystywana inna część kodu bardziej. Więc dlaczegóż by go nie zoptymalizować?
No i też warto pamiętać o tym, o tym rozgrzewaniu, czy niedeterministycznym czasie wykonania. W przypadku mierzenia jakimkolwiek benchmarkiem czy zwykłym upływem czasu, sprawdzenia, ile coś się wykonuje, no bo jednak wtedy te wyniki mogą się znacznie różnić w zależności od tego, czy to będzie wykonane normalnie w bytecode’ie, czy jednak w jakimś natywnym? Skompilowany benchmarkowanie Javy też nie jest takie, takie łatwe, no bo jednak sposób uruchomienia może się różnić.
Dokładnie, to nie jest tak jak w C++. Wróć. W jakimkolwiek kodzie skompilowanym natywnie, który ma bardzo deterministyczny runtime, który zawsze działa tak samo. No bo tutaj runtime się zmienia. Runtime jest połączony z tym, co my mamy w programie, także dochodzi kolejna warstwa abstrakcji, więc zawsze im więcej tych warstw mamy, to na pewno zawsze jest zawsze więcej takich czynników, które gdzieś tam zaburzają czy to wydajność, czy jakieś parkowanie. Więc tak, to jest na pewno jakieś, jakieś zmienne.
Ładowanie kodu i Class Loadery
Kolejnym takim teoretycznym zagadnieniem jest ładowanie kodu. To już tam dotknęliśmy chwilę wcześniej, że procesor musi mieć dostarczone instrukcje maszynowe swoje, żeby móc w ogóle wykonywać ten kod. Natomiast Java musi mieć dostarczony bytecode. Kod Java ciągnie to, co wykonuje, z plików z rozszerzeniem .class
– to są skompilowane klasy Javy lub jakieś tam inne jej widmowe klasy w sensie z innych języków, jakieś artefakty, które się kopiują też do plików .class
, tak żeby JVM mógł to złapać. No i to coś jest pakowane w słoiki zwane JARami, na przykład, czy w jakieś tam inne, różnej maści archiwa. Trafia do jakiegoś takiego ustrojstwa, które jest wdzięcznie nazwane Class Loaderem. Class Loader ma za zadanie… No tutaj wielkie zdziwko – załadować te klasy.
No i to jest też może taki rzut oka trochę na to, jak to drzewiej bywało, bo kiedyś bardzo modne było żonglowanie tymi Class Loaderami, jakieś tam przystawki do nich, jakieś sprawdzanie bezpieczeństwa, jakieś kiedyś były dziwne pomysły. W ogóle taki modularizm na poziomie samej klasy gdzieś tam jakieś się pojawiały, ale to dawno nieprawda. Na szczęście to umarło śmiercią naturalną, bo się do niczego nie nadawało, ale jak ciężko…
Się nadaje w niektórych modelach, fajnie z tym radzi. Z takimi właśnie customowymi Class Loaderami i faktycznie można sobie mieć na przykład tę samą klasę w różnych wersjach, w różnych modułach i jakoś to sobie…
Pozwól, że zapytam, na co to komu?
Są tam specyficzne przypadki, oczywiście, użycia, ale, ale jednak da się też to zrobić.
Da się to zrobić. No i właśnie takie mniej wydajne jakieś Class Loadery czy jakieś inne. Po prostu im bardziej skomplikowany mechanizm, tym tym będzie gorzej sobie z tym radził. Mieliśmy z tym do czynienia w na przykład tych już narzekano poprzednio, jakiś tam kontenerach serwletu czy serwerach aplikacyjnych, które po prostu w jakim zajmowało załadowanie jakiegoś tam archiwum WAR, a raczej innego tam takiego. Polecam takie większe archiwa niż pojedynczy. Zajmowało to wieki, bo też warto. Warto wiedzieć, że od wersji dziewiątej Javy, a mianowicie JDK właściwie, zaszło. Zaszło w niej sporo refaktoryzacji, jeśli chodzi o modularność pewnych, pewnych bibliotek, takich wewnętrznych i używanych przez samą, samą maszynę wirtualną. Przed wersją dziewiątą. Jednym z głównych JARów. Tych, w których siedzą właśnie klasy Javy, był rt.jar
. I to był taki pliczek, który potrafił mieć około 60 MB. Tam siedział, bo musiał mieć tam wszystko. Plik, bo właściwie wszystko tam było wewnętrzne, wszystkie te klasy fanowskie, których oczywiście nigdy nie wolno używać, ale zawsze znajdzie się jakiś projekt, który je importuje i coś tam z nimi robi, czy przeróżne rzeczy takie dobre z pakietu właśnie java.*
, javax.*
. No i jakby nie patrzeć, ten JAR często musiał być ładowany bądź skanowany w poszukiwaniu jakiejś klasy, więc na dzień dobry 60 megabajtów trzeba było sobie załadować.
Od wersji dziewiątej nastąpiła już spora modularność. I wszystkie te klasy zostały rozpracowane na pomniejsze, pomniejsze JARy i pomniejsze pliki, więc już tego pliku w ogóle nie ma. Zostało to troszkę usprawnione, więc też jest to jakiś tam delikatny boost, jeśli chodzi o przynajmniej ten start, bo właśnie to taki cold start.
Nigdy nie miałem przyjemności na takiej dziewiątce, albo więcej odpalać jakiegoś Gothica starego takiego. Bardzo ciekawe, jak by to poszło.
Są bardzo drogie, bo to osoby chyba często używane przez jakiś Thule czy gdzieś tam, gdzie podawało się ścieżkę do JAR-a, bo coś tam było wymagane, żeby zaciągnął. No i z nowszą Javą to już nie będzie takie proste, bo tego JAR-a nie ma.
Aby było.
To jakieś krakowiacy, jak kiedyś Rydzyka.
No chyba koledzy Krakowa… koledzy tak opowiadali, koledze kafelek.
Dobra.
Tak, tak.
No to mieliśmy tamte symulujące środowiska, które dodatkowo nam pogarszały wydajność. No bo jak one musiały tam przerzucać te klasy? I jeszcze wiadomo, takie klasy chlubne, wszystkie wynalazki używają tych klas, co zresztą widać po użyciu profilera jakiegokolwiek w aplikacji, takiej może większej niż kalkulator dodający dwie liczby, bo jednak tych klas to coś tam ładuje w dziesiątkach, czasem tysięcy po prostu. I to wszystko. I tak musi siedzieć w pamięci, ale sam fakt załadowania tego i ten, i później ewentualnie. Nie pamiętam, czy to się udało to, czy to tak sobie tam zalega?
Chyba to coś zalega i w starszych wersjach wiem, jak w nowszych. W nowszych jest możliwość wywalenia z tej pamięci plus jakiś cross nieużywanych może od dawna czy co?
Tylko problem w tym, że jak już raz się załadowaliśmy, to prawdopodobnie zajęło nam wolne. Zajęło to raz, a dwa, że pewnie już jednak będzie potrzebne, bo okazuje się, że ktoś tam zawsze czegoś używa i gdzieś to zawsze ta referencja nam pozostanie, czyli po prostu zaśmieca nam tą pamięć kodu, którą ma przyznaną, zwiększoną ten, ten cały memory footprint. Dzięki temu więc stare wynalazki, które bardzo lubiły to robić i ładowały hurtem wszystko, co było, bardzo szybko miały pod względem pamięci.
Mierzenie wydajności i ustalanie celów
No i tak to chyba teoretycznie tak bardzo daleko, z daleka jeszcze względem tej naszej. Tych dobrych praktyk. Tak mniej więcej by się to przedstawiało, a pewnie reszta zagadnień wyjdzie nam, jak już zaczniemy się tych dobrych praktyk czepiać i pewnie zaczniemy szukać dziury w całym. Bo to szukanie dziury w całym to jest tutaj potrzebne przy badaniu wydajności. No bo mamy coś, co nie działa wydajnie. Miotają się też pytania. Pytanie: co to znaczy “nie działa”? Czy to jest zwykłe “nie działa” na zasadzie: klikam, a tu nic?
No właśnie, najczęściej to to są takie objawy, że klikam gdzieś na interfejsie i coś tam się wolno ładuje, coś tam mogłoby szybciej. Gdzieś tam klient narzeka, że ten czy ów widok ładuje się za wolno i to są takie pierwsze, pierwsze objawy. Tylko trzeba sobie postawić pytanie: czy to są faktycznie objawy tego, że gdzieś tam w tym naszym kodzie Java jest jakiś problem, czy prędzej to może być problem gdzieś tam w warstwie dostępu w bazie danych, za dużo zapytania, czy może jeszcze nawet częściej gdzieś tam na tym froncie. Jednak ten nasz uszkodzony JavaScript to zajmuje 5 MB i musimy go też załadować. Jakby warto znaleźć tego winowajcę na początku.
No i nie oszukujmy się, w takich aplikacjach biznesowych, już nawet nie mówię czysto klubowych, to prędzej to będzie właśnie to niewydajne zapytanie. Prędzej to będzie jakiś JavaScript, który po prostu też jest za duży i ma za dużo zależności i zwyczajnie jest.
Tu mamy doskonałe pole do po prostu zgadywania. Czyli możemy znaną metodę z Wiedźmina 3 zastosować, czyli pójść do Onej Randki. Ona sobie wyjaśni, co tam się w naszej aplikacji krzyczy aktualnie, na czym ta aplikacja zamula i już możemy usiąść do kąciku i poprawić ten fragment.
No tak, jeśli chodzi o sesję, jeśli chodzi o takie. W ten sposób monitorowanie frontu – narzędzi jest bardzo dużo, więc tu jesteśmy w pełni, że tak powiem, przygotowani w różne i zabezpieczeni, żeby jeszcze, żeby sprawdzić, czy to nie są przypadkiem za duże rozmiary JavaScriptu, czy nie zoptymalizowane obrazki CSS i tego typu rzeczy bez. Przekonałem, że ten krok mamy już sprawdzony. Możemy również sprawdzić ilości zapytań w bazie danych, jeśli podejrzewamy, że to może być baza danych i też zrobić jakieś plany zapytań. Mi się ten krok też mówi, że wszystko wygląda OK, to wtedy możemy palcem wskazać tę naszą Javę i ten nasz, ten nasz kod, ten nasz kod, i który tam robi get
, set
, get
, set
i może get
. Dowolne jednak.
Ale zanim doszliśmy do tej Javy, to już i tak zastosowaliśmy taką już po części naukową metodę o takich fundamentach naukowych, może polegającą na mierzeniu czegoś – szkło i oko. Albo mamy po prostu całościowo jakieś rozwiązanie i sprawdzamy, który element z tego najwyższego rozwiązania jednak odpowiada za największy spadek tej naszej wydajności, czyli prędkość działania. Tutaj bierzemy pod uwagę, mierzymy czasy wykonywania poszczególnych fragmentów. Jeżeli cały nasz request trwa sekundę, no to mierzymy, ile z tego czasu faktycznie rzeźbi nasza Java, ile komunikacja z bazą zajmuje, ile komunikacja z frontem. Bo może się okazać, że na przykład wszystko jest błyskawiczne, tylko internet mamy na modemie.
I co tak naprawdę możemy zrobić? Jeszcze trochę krok wstecz i zastanowić się też trochę nad charakterystyką tej wydajności naszych aplikacji. No bo tutaj tych charakterystyk też może być, może być wiele, bo możemy w naszej aplikacji wymagać bardzo dużej responsywności. W sensie, że faktycznie każdy request musi być bardzo szybko wykonany. Odpowiedź czy jakiś wynik rezultatu, a wynik rezultatu to jest szybkie wewnętrzne.
No ale to zawsze działa. Maszyna nasza odpowiedź po prostu musi być, musi być dobry wynik, musi być szybko. No i jeśli mamy przepisać link, tak, czyli jakieś transakcje. Nie wiem, może jakieś bukmacherskie aplikacje, gdzie też musimy bardzo szybko spekulować, spekulować. Oczekujemy, że te czasy odpowiedzi będą naprawdę szybkie, bo tego wymaga nasz, nasz biznes.
Z drugiej strony może być też taki profil odnośnie przepustowości w rozumieniu ilości danych, jakie chcemy stosować w naszej aplikacji. Tak, weźmy, na przykład, te wspomniane w poprzednich odcinkach wielkie, te responsywne z jakiegoś topowego, na przykład, nazwiska. Słupa. Przepraszam, tylko to może być nawet jeszcze więcej. To mogą być jakieś modele do uczenia maszynowego, gdzie dużo danych musi kobieta czekać. To może być też na przykład tak, dostaliśmy w jakiejś prezentacji, ktoś podał dane, jakie generuje CERN, czyli akcelerator. Gdzie tam ileś tych różnych czujników mierzy rozpadu cząstek i tam dane są generowane w ilości gigabajtów na sekundę, czy nawet jeszcze większych. Tak naprawdę to są zupełnie skale, które poza metodą…
Tych cząstek by nie mogli się rozpierzchli.
Wszystkie mają, tyle generują i jedzą.
To by w Javie 5 napisali.
I to są. I to są takie ilości, których pewnie nigdy nie uświadczymy w aplikacjach stricte biznesowych, bo chyba żaden użytkownik na sekundę nam nie wygeneruje GB. No i z drugiej strony jeszcze możemy z trzeciej strony mieć taki, takie bardziej kryterium, może współbieżne, gdzie po prostu mamy bardzo wielu użytkowników naraz, którzy potrzebują wykonać jakąś akcję. Oczywiście ten czas odpowiedzi jakoś tam jest ważny, ale tutaj bardziej idziemy w ilość użytkowników, czy…
Ze przeszkody. MMO w świecie AKA…
Po części też. Chociaż tam może nawet ta responsywność jest ważniejsza, ale chyba bardziej takim przykładem byłaby przeglądarka internetowa, gdzie tysiące ludzi na świecie, miliony ludzi na świecie stara się do tego Google’a czy czystego Binga. Wyszukiwarka, powiedziałem przeglądarka, przeglądarka, wyszukiwarka. Oczywiście mam na myśli wyszukiwarkę, gdzie miliony ludzi wklepuje te swoje, swoje zapytania. No jednak te wyszukiwarki muszą dość szybko ten wynik dostarczyć, no bo inaczej to pójdziemy do innej wyszukiwarki.
To też jest fajny model, że zasadniczo. Można powiedzieć, to jest taki przykład, trochę taki bezstanowej aplikacji, czyli w zasadzie nasz wynik wyszukiwania nie powoduje żadnego zapisania stanu na to samo wyszukanie, bo to sobie oczywiście przeglądamy gdzieś tam w indeksach Google, sobie przegląda. Wiadomo, pod spodem oczywiście te wyniki zostaną zapisane w naszej historii wyszukiwania, zostaną wysłane gdzieś tam do jakichś usług reklamowych. Sam algorytm będzie z tego korzystał, żeby nam lepiej to wyszukać, ale to wszystko będzie się działo już asynchronicznie pod spodem. Natomiast synchroniczna sama operacja to jest tylko i wyłącznie właściwie odpytywanie, sortowanie i operacja. Bez tym to się bardzo fajnie horyzontalnie może skalować.
Więc wszędzie gdzie mamy właśnie te horyzontalne skalowanie horyzontalne i bezstanowość. To coś fajnie się zawsze łączy. Natomiast o tak, to może po prostu zwiększać ilość klocków. Niemniej jednak sam, sam klocek, ten pojedynczy, bazowy, z którego budujemy tę naszą horyzontalną skalowalność, to rzecz. On musi być wystarczająco wydajny. No bo jak ma słabe klocki, to słabo nam się to skaluje. Im lepsze klocki mamy, tym lepsze efekty dostaniemy z tego skalowania.
Więc tak jakby nie patrzeć, to tam trzeba i tak solidnie mierzyć coś, najczęściej jakiś czas czegoś podczas wykonywania jakiegoś kodu, tego czy tamtego. Wszystko jedno, bo bez tego nie da rady stwierdzić, czy to, co się dzieje, dzieje się właśnie tak jak powinno.
Ale to mierzenie tego czasu jest jak powinno. Bo jak już zmierzymy, mamy jakąś wartość, to skąd wiemy, że to wartość jest dobra? Nulla ma jakieś.
W tę stronę przechodzi do celu Owner i mówi: “To za wolno”.
A jak ma być szybciej?
Szybciej, ale za szybko?
Jak najszybciej i jak najszybciej.
No dlatego właśnie tak te procesory teraz mają i7, i9. Ile herców na jednym rdzeniu?
No właśnie, czyli z tego wychodzi. Tak, to prawda, taka troszkę może nie nauka, ale warto zastanowić się, do czego dążymy. Jeśli biznes powie nam: “jak najszybciej”, to warto zastanowić się, OK, to czy może być, na przykład, za sekundę? Czy to jak najszybciej musi być szybciej? To może pół sekundy? OK, to może być. Może być pół sekundy. I wtedy mamy jakiś namacalny cel, do którego chcemy dążyć.
Musimy mieć te cyferki, żeby po prostu wpisać w to nasze narzędzie, które nam coś będzie mierzyło. No bo inaczej żeby coś w tym odcinku czasu, dajmy na to, możemy bez końca to optymalizować i nigdy nie będzie dobrze. Tutaj, to znaczy, tutaj, warto też sobie cele jakiś tam ustanowić, czy to w jakiś czasach odpowiedzi, w ilości przepracowanych danych, w ilości jednoczesnych użytkowników – tego typu parametry, które nam coś w stanie są powiedzieć w dalszych benchmarkach.
No ale mamy taki benchmark, który już nam przez sekundę coś tam mierzy i coś nam po nim, jeżeli na przykład nie ustalimy warunków, w jakich on się wykonuje. Więc tutaj oprócz określenia tych zależności czasowych musimy jeszcze mieć zależności środowiskowe. Bardzo dobrze opisane, bo mało tego, bo to trochę wyprzedzam już, że tak powiem, plan na dzisiejszy odcinek, ale zrobię tę wzmiankę, bo jest teraz dobra w kontekście. Że tak naprawdę taki kod benchmarku, czyli testu sprawdzającego wydajność danego fragmentu jakiegoś tam komponentu czy po prostu fragmentu kodu, to jest rzecz bardzo podobna do testu. W ogóle bez powodu się to testem też nazywa jakoś tam. A testy mają taką, mają taki zestaw wytycznych, które muszą spełniać, muszą być izolowane, muszą być powtarzalne, co tam jeszcze niezależne od nocy?
Tutaj izolacja załatwia.
Tak, czyli generalnie musimy zadbać o to, żeby te testy wykonywały się zawsze w takim samym środowisku. Jeżeli nam się nie uda tego zapewnić, no to to też nie mamy pewności, że aktualnie to, co się zepsuło, jest winą samego kodu testowanego, a nie środowiska. I przy benchmarkach ma to kolosalne znaczenie, bo nie możemy sobie testować wydajności, łupiąc w jakąś gierkę na tej samej maszynie, która to gierka po prostu nam 90% mocy procesora zabiera, a 10% nam zostaje na tą naszą maszynerię do testowania, bo wtedy to będzie totalnie niewspółmierne z tym, co co dostaniemy w innych warunkach.
Czyli trzeba wyłączyć sobie gry w trakcie pracy.
Tak, chociaż paradoksalnie, jeżeli na tych 10 procentach nam wyjdzie świetny wynik, to na tym serwerze już później nam będzie rewelacyjnie to śmigało.
No tak, no ale nie możemy tutaj oczywiście, śmieszki na bok, bo nie możemy sobie zaśmiecać środowiska naszych testów jakimś czymś niepożądanym, więc musimy. Musimy sobie przygotować taki setup, który będzie przede wszystkim odtwarzany, powtarzalny. Przez to będziemy go mogli postawić bez użycia “klika-d”, najlepiej z jakiegoś skryptu, totalnie automatycznie. I najlepiej w kilku egzemplarzach nawet, żeby można było je porównać.
No to to już jest bardzo mile widziane. W sensie i na przyszłość, bo wystarczy, że jedno takie środowisko będzie odtwarzane i w powtarzalny sposób będzie sobie wykonywało te testy.
Dobra, ale do tego jeszcze dojdziemy. Czyli tak, szukamy tej dziury w całym i wiemy, że ma być jak najszybciej. Czyli określiliśmy co, to znaczy jak to jest “jak najszybciej”. Wiemy, że musimy mierzyć, zamiast zgadywać. Ale co jeszcze? Co jeszcze możemy sobie powiedzieć?
Dobre praktyki i unikanie pesymizacji
Otóż w tym szukaniu dziury w całym wkraczamy troszkę w nasz świat kodowania. Bo powiedzmy, te te przygotowania, które sobie opowiedzieliśmy przed chwilą, to było takie dopiero przygotowanie podwórka do tego, teraz wchodzimy już w ten nasz kodzik. No i kiedyś mówiono nam, że nie ma co tam za wcześnie optymalizować, bo na co to komu i trzeba dopiero później zmierzyć. Ale może jednak gdyby tak zacząć pisać ten kod? Od unikania tak zwanej pesymizacji, która jest odwrotnością optymalizacji, czyli na poziomie doboru algorytmu, na przykład, zdecydować, że sortowanie bąbelkowe to nie jest najlepszy sposób sortowania dużej kolekcji obiektów.
No jak to nie? Takie ładne napisałem przecież.
No tak, ale każdy pisał kiedyś jakieś bąbelki. One fajnie tam sobie bąbelkują. To jest w ogóle świetna rzecz do ćwiczenia algorytmów i polecam to każdemu. Ale jak mamy, na przykład, do dyspozycji jakieś Heapsort, Quicksort i inne sorty, a bąbelki, to może lepiej zastosować ten szybszy, bo bąbelkowe sortowanie ma złożoność kwadratową, a na przykład taki Quicksort ma logarytmiczną.
No i teraz. O co chodzi? Złożoność kwadratowa powoduje, że czas wykonania takiego fragmentu kodu rośnie proporcjonalnie do kwadratu ilości elementów, które mamy do przetworzenia w pętli. Natomiast logarytmiczna złożoność – logarytmiczna jest proporcjonalna do logarytmu z tej wartości. Więc która liczba będzie mniejsza? Kwadrat czy logarytm? Na przykład. Jeszcze jest złożoność liniowo-logarytmiczna – to jest liniowa pomnożona przez ten logarytm właśnie.
Najgorsze w tym wszystkim jest to, że lokalnie może nie być różnicy większej, bo lokalnie zwykle pracujemy na małych zestawach danych. Często nie mamy jakiejś takiej dużej skali.
Gadałem o tym, to zepsułeś to, to wręcz przeciwnie.
Wręcz przeciwnie.
Lokalna optymalizacja a skala działania
Bardzo dobrze, że to poruszyłeś, ale żeby ktoś to trafnie zarzucił: “Ale u mnie przecież działa! U mnie sortowanie bąbelkowe na moim laptopie działa super! U mnie, u mnie zawsze działa dobrze”. No właśnie i po co mam teraz coś wymyślać? Oczywiście to sortowanie bąbelkowe jest tylko pewnym, pewnym symbolem.
Oczywiście. Ja to mam taki przykład z niedawnego mojego ćwiczenia takiego po lekcjach, bo znajomy poprosił mnie, żebym napisał taki indeksowany fajnie słownik do wyszukiwania części składowych wyrazów zwanych morfeumami. No i dopisałem takie coś. To był odwrotny indeks. Testowałem to sobie na jakimś takim małym zestawie danych. Wyodrębniłem sobie z tego słownika właściwego tam kilkadziesiąt haseł i po prostu zapuściłem, to śmigało jak burza. Ucieszyłem się: jakiż to fajny algorytm! Co tam się nie wyrabia, to Java jest szybka w ogóle! Po czym zapuściłem na takim słowniku prawdziwym, co ma 62 MB – nie, sorry, 62 MB to mało, hehehehehe, ale to też miliony. W każdym razie haseł mam chyba 4 miliony z groszami, bo to był język ten, ten słownik SJP, i tam to już trwało tyle, że mi się maszyna wykrzaczyła.
No właśnie, po to jest dokładnie ten przykład, o którym wspomniałem, że czasami możemy nawet nie zauważyć, że coś jest troszkę nieoptymalne, czy nawet bardzo, no bo zwyczajnie sortujemy trzy liczby, które sobie wpisaliśmy w naszym teście z palca. No i tutaj nie zauważymy absolutnie różnicy. Ale tak jak tak jak wspominasz, jak zamontujemy sobie 4 miliony, to już będzie można coś poczuć.
Dlatego właśnie te, na przykład, benchmarki powinniśmy też tak projektować, żeby one jednak coś robiły sensownego. Czyli nie wiem, jak mamy jakąś pętlę do obsługi requestów, to niech niech ona się wykona 10 milionów razy. I niech te dane będą… To właśnie jest osobne zagadnienie: jak dobierać dane? Czy dobrać dane losowo tak, żeby na przykład prześlizgnęły się przez nasz algorytm, często nawet nie powodując wejścia do niego? Czy raczej przysiąść i tak dobrać dane, żeby jednak to się wykonywało? Byłbym za tym drugim, więc jak byśmy mieli taki zestaw danych, to wtedy taka pętla już miałaby sens. Większy na pewno niż taka wykonana 100 razy i jeszcze podzielona n razy i poprzedzona jeszcze fazą wygrzewania.
Oczywiście maszyna, bo tak. Bo jeśli rozkład tych danych będzie zbyt jednorodny, to też pewne optymalizacje mogą wejść i, i generalnie może to być troszkę inne zachowanie niż na takiej dystrybucji danych produkcyjnych, gdzie to jednak będzie bardziej zbliżone do faktycznego użycia, więc wyniki też mogą być trochę inne.
Unikanie pesymizacji i świadome optymalizacje
No ale wracając do tego unikania pesymizacji akurat w kwestii doboru algorytmów, to od tego można by zacząć. Oczywiście to jest daleko szersze zagadnienie, bo te nasze algorytmy pisane na piechotę też po prostu można tak robić, żeby unikać jakiś nadmiernych alokacji pamięci, jakiś niepotrzebnych operacji. Może coś używać w nich, oczywiście upewniwszy się, że można tego czegoś użyć, bo często się pisze po prostu coś na piechotę w pętli, alokując jakieś nowe obiekty co moment, nie zwracając w ogóle uwagi, ile te obiekty ważą i jak właściwie. Może to kwestia zajętości pamięci jest tutaj tylko obok, bo tak naprawdę czas inicjalizacji takiego obiektu może tutaj grać jakąś rolę, więc zamiast używać jakiegoś obiektu, który się musi długo inicjalizować, może jesteśmy w stanie użyć te obiekty już raz zainicjalizowane z puli, na przykład. Zrobić singleton albo uogólnienie singleton na pulę, czyli singleton of. No to sobie nie pesymizujemy.
I jednocześnie nie bójmy się optymalizować już na tym wczesnym etapie. Oczywiście nie popadajmy w paranoję, bo nie ma co optymalizować tak wszystkiego na 100%, angażując się w to na etapie pisania pierwszych linii kodu. No bo wtedy to faktycznie nie wiemy, co tam się będzie działo. Więc tutaj nie mam jeszcze tego aspektu mierzenia, ale jak już możemy coś zmierzyć albo chociażby jakieś wzorce, intuicja nam podpowiada, które można by to było zastosować, bo się milion razy stosowaliśmy i dobrze się sprawdziły. Zresztą nie tylko my, tylko cały świat. No to grzechem by było tego nie zastosować. Nie tylko napisać jako jakiegoś ślepca po swojemu i tyle. Później się zastanawiać, czemu to wolno działa.
Zwykle rzeczywiście przy tworzeniu na początku jakiejś funkcjonalności czy kawałka systemu, to ta wydajność rzeczywiście nie, nie będzie i pewnie nie jest na pierwszym miejscu. No chyba, że ten czynnik szybkości jest rzeczywiście jakimś tam ważnym czynnikiem w naszej architekturze. Jeśli piszę jakiś silnik 2D do gier w Javie, bo nie znamy innych i wierzymy, że sama może być używana do gier, no to wtedy może. Oczywiście są pewne optymalizacje, będą jak najbardziej na miejscu, ale no nie oszukujmy się, jeśli mamy akurat CRUD-a bardzo prościutkiego, to tam nawet ciężko nam będzie zrobić. Jakiekolwiek optymalizacje w spółce w warstwie czołowej. No bo tych operacji, czy to algorytmów, raczej nie będzie dużo, więc trzeba jasno powiedzieć, że musi być przestrzeń. To są takie optymalizacje.
No chyba, że ktoś bardzo, bardzo lubi mieć taki schludny pod kątem wydajności kod, bo tutaj rzeczywiście bardziej to będzie już chyba skutkowało czystością tego kodu. Jednak trochę też trochę dobrze jest. Jak w ogóle ten algorytm jest tam pod spodem, bo w króluje to tak średnio?
No właśnie, właśnie to z tego powodu nie ma co optymalizować. I tu też warto też zastanowić się, czy lepszy jest kod wydajny, czy jednak czytelny. Bo czasami to jest takie troszkę. Najlepiej by to był kompromis, ale jak miałbyś wybierać? Na przykład. Oczywiście to nie mówimy o takim niewydajne na zasadzie, że 100 razy wolniejsze, ale jeśli coś będzie troszkę wolniejsze, albo coś będzie jednak bardziej czytelne, to co byś wybrał?
Jednak chyba wybrałbym to, co się da przeczytać, bo kiedyś próbowałem przeczytać kod jakiejś tam transformaty Fouriera. I to jeszcze pisanej. Chyba nie pamiętam już, co to było, ale to się pewnie śniło po nocach.
No właśnie, właśnie chyba to, to jest taka, jest dobra tak samo. Rzeczywiście wolałbym mieć coś troszkę wolniejsze, ale jednak jeśli to jest czytelniejsze i jest nawet troszkę nadmiarowo napisane, czyli troszkę więcej tych linii kodu, które opisują ten proces lepiej, to nawet jeśli to nie jest tak optymalnie napisane, to to może być, może być lepsze, bo pamiętajmy, że za chwilę ten kod będziemy czytać sami, albo będziemy uważnie czytać nasi, czy nasi koledzy, więc też nie ma co przesadzać z jakimiś, jakimiś optymalizacjami i tak jak John Carmack robił w swoich silnikach. O dziwo, on się z tymi optymalizacjami urodził, musiał tylko później przenieść do kodu.
No tak, tak. Ale tutaj statystyka nam pomaga, bo statystyka pokazuje, że tam chyba różne już liczby słyszałem, ale kilkadziesiąt razy na pewno to będzie tak poczynając od kilkunastu do powyżej 20. Właśnie tyle razy częściej ten kod czytamy niż piszemy. Może dużo więcej. Nie, nie pamiętam, jakie to są liczby.
Ja ostatnio bardzo dużo czytam, a mało staram się pisać, bo pisanie to jest strata czasu.
No i właśnie jak się mało pisze, to co jest mniejszy ten kod, mniej jest go napisanego, to raz, że się mniej może zepsuć, a dwa, że szybciej musi działać.
Tylko że jest mniej zrobione. No, mniej zrobione, znaczy może zrobione to samo, tylko co jak…
Nie przybywa funkcji, o co mi chodzi.
Bo niestety nie przybywa. No ale jak mamy do wyboru, jak my. Ale jak taki JVM ma do wyboru załadować mniejszy kodzik albo albo większy i zacząć wnioskować na jednym i drugim, to chyba woli na tym mniejszym wnioskować, żeby sobie hotspoty znaleźć i coś tam optymalizować.
Tym bardziej, że w tym mniejszym to pewnie też. Będzie to jakoś napisane schludnie, z dokładnością do tego, że tam się nie stosuje takiego stylu znanego z lat osiemdziesiątych. Wszystko jest pokazywane jak u wujka Boba, czyli i tak po wujkowi. Metody są krótkie, schludne, czytelne. Robią jedną rzecz. Pojedyncza odpowiedzialność to jest dużo większa szansa też na lokalność tego całego, zachowanie lokalności tego kodu w domenie. W zagadnieniu po prostu on się lokuje w tym miejscu zagadnienia, operuje na tych symbolach, które są blisko. Więc nawet jakieś tam mechanizmy redukcyjne, jeżeli by chciała zastosować, to też tutaj mają zastosowanie, bo ma to wszystko pod ręką i dużo łatwiej. To na pewno jest jakiś sensowny model wykonywania z tego zbudować i jakoś przełożyć go na kod maszynowy, który też będzie z tej lokalności korzystał i wtedy wszystkie te produkcje będą się ładnie spinać.
Czyli jednak małe jest lepsze.
Mówią też, że mały, ale wariat. Więc taki mniejszy kodzik, dobrze zoptymalizowany, to można nieźle śmigać. Dobry kodzik z dobrym algorytmem. Jeżeli algorytm jest potrzebny, bo nie piszemy CRUD-a. Mały CRUD, jeżeli piszemy CRUD-a.
Unikanie zbędnych operacji i wyjątków
No i może jeszcze to jakieś niepotrzebne wieści byśmy unikali, może jakieś niepotrzebne wywołania byśmy chcieli zrobić, a można się bez nich obejść? Nie wiem, spekuluję, bo ciężko sobie czasem wyobrazić. Jak już mamy taki mały, schludny kodzik, to pewnie już w nim nie ma za dużo. Ale z drugiej strony przysiąść do jakiegoś fajnego legacy stacka. To co tam się nie wyrabia, to tam się nie woła, gdzie tam, jakieś struktury się nie tworzą tylko po to, żeby zostać zniszczone za moment. Więc może takich przypadków byśmy byli w stanie unikać. A jak już je napotkaliśmy, bo ktoś je zrobił przed nami, to może byśmy to spróbowali jakoś po prostu uprościć. Już nie mówiąc o tym, że byśmy to chcieli optymalizować, mierzyć i optymalizować, ale już tak dla zwykłej czytelności naszych i dla spokoju ducha byśmy po prostu zczepali to, to starocie i zastosowali jakieś jedno coś nie tak.
Tutaj pewnie jeszcze coś. Ten problem często może być nie tyle po stronie naszego kodu i naszych lokalnych klas, ile też po stronie frameworków, które używamy. No bo wiadomo, jeśli mamy gdzieś przykład takiego Hibernate i jakieś mapowanie relacyjne obiektowe, to sobie można tylko wyobrazić, ile tam pod spodem tworzy nam się obiektów i generuje zapytań.
Albo w XML-u.
Może być w XML-u, żeby jeszcze na dokładkę tego XML-a doczytać, chociaż to wszystko tam na początku czytane, więc to idzie przeboleć.
Poza tym nie czytam.
Poza tym tak, tak akurat i sama tego nie lubię.
Nie lubię czytać XML-a czy pisać.
I pisać, ale też dobrze wiedzieć chociaż troszkę o bebechach, co tam się dzieje w naszych komórkach, które są używane.
W Hibernate to lepiej nawet nie wiedzieć. Gdzie lepiej śpi jakoś tak.
No może za dużo się dowiemy. No to rzeczywiście wszystko i zobacz i skończymy na tym, że będziemy pisać każde zapytanie ręcznie, co też może w jakiś prostych przypadkach nawet być lepszym rozwiązaniem, ale niemniej jednak nasz kod to jest jedna płaszczyzna, ale też kod jakiś tam bibliotek. Pomińmy może tego Springa, czy to są to rzeczy, które rzeczywiście ciężko zastąpić od razu czymś innym.
I też mówiliśmy w poprzednim odcinku, że Spring nam bardzo mocno polega na takich refleksyjnych rzeczach. Coś za coś. On po prostu kosztem wydajności taśmowej daje nam wygodę akurat kodowania. Bo po prostu daje nam potrzebne rzeczy na wyciągnięcie ręki. Dlatego tak właśnie chyba ludzie go lubią. I podobnie jest z Hibernate. Tam też jest dużo refleksji w środku, jakiś dziwactw i w ogóle jest dużo takich zaawansowanych frameworków domowych.
Tak, ma.
Bo tutaj też możemy mieć pełno różnych pomocniczych bibliotek, które sobie tam ładujemy i coś robimy. Czasami już nawet ich nie używamy, bo bo coś to zastąpiło, ale się załadowałem, bo wolę nową. Oczywiście, że do siebie ładowałem.
To jest taki trochę problem, z którym masz dziesiątki, bo nigdy nie wiesz, który Ci się przyda.
Nie wiem, co leży w tych garach.
Gęstość tego folderu osiąga już masę krytyczną.
Więc to jest ogólnie problem. Gdzieś tam zależności oprogramowania, czyli systemy są coraz bardziej skomplikowane, więc wymagają coraz więcej klocków, które są poskładane. Dlatego też warto jakby troszkę trzymać ten sklep, ten zakres tych naszych zależności w ryzach.
Układy, pojedyncza odpowiedzialność, na przykład przeniesiona z poziomu kodu na poziom organizacji kodu, też może tutaj nam pomóc. To jest właśnie to unikanie takiej przedwczesnej pesymizacji, a jednocześnie taka nieunikanie przedwczesnej optymalizacji, bo tutaj kolejny raz mamy okazję do załadowania mniejszej ilości kodu, więc nie obciążamy tej naszej dużej JVM-ki tak, jak byśmy obciążali, gdybyśmy tego jednak nie zrobili.
Kesowanie i świadome wywołania metod
No dobra, ale teraz tak. Wracając jednak do tego kodu, to może się okazać, że na przykład jednak mamy ten algorytm i on coś tam nam robi i coś tam wylicza nam co chwilę jakąś wartość, na przykład. Więc można by się zastanowić, czy by nie skesować sobie takich wyników po prostu i kosztem jakiejś niewielkiej zajętości pamięci, bo nie wiem, wrzucimy. Kilku KB-ową tablicę zawierającą jakieś tam już wstępnie przeliczone liczby, czy tablicę, czy mapę, czy jakkolwiek tam to, co jest. Konstruujemy jakąś tam strukturę danych, która zawiera. Najlepiej to na prymitywach jak zrobić, żeby to jednak nie ciągnęło tych naszych nagłówków z ostatniego odcinka. Więc jak mamy taką tablicę liczb, które coś tam już znaczą dla naszego algorytmu i wiemy, że one będą musiały być wykorzystywane w kolejnych jego fazach, to podobnie. To będzie taka analogia troszkę do tego hotspotu, nie? Bo to będzie taki hotspot w naszym algorytmie, więc go sobie wstępnie kompiluje i już na poziomie kodowania przygotowujemy sobie taką statyczną tablicę i później tylko z niej wyciągamy sobie elementy i będzie na to przykład, na przykład.
Kasowanie. Tylko nie wiem, czy jakoś tak dobrze idzie, że dzisiaj nie zdążymy skleić starych. Coś tak czuję.
W ogólności kesowanie też, bo możemy sobie kesować pojedyncze fragmenty naszych, naszych wyliczeń. Możemy sobie kesować same metody.
Możemy to robić tak.
Albo…
Więc możemy to w locie robić. Oczywiście jak razie to też jakaś forma optymalizacji, oczywiście zajmująca wtedy pamięć, czyli coś kosztem czegoś.
Coś kosztem czegoś. Ale jeśli rzeczywiście mamy jakąś taką cięższą do wykonania operację, a jednocześnie te wyniki możemy skasować i jednak ten rezultat trzymać przez jakiś czas po to, żeby się kosztowania może nam fajnie, fajnie przyspieszy, żebyśmy nie musieli przeliczać jakiejś wartości cały czas, skoro od razu, czy zrobiona może nie zmienia się często w czasie? Może możemy pokazać naszą wartość? To jest na pewno fajna forma przyspieszenia i zredukowania tutaj naszych, naszych wyliczeń czegokolwiek.
Dokładnie, a na poziomie samego kodowania, jeżeli wiemy, że coś na pewno będzie miało jakąś tam wartość, to zamiast uporczywie wołać jakąś metodę, która zawsze tę samą wartość dokładnie nam wyliczy i zwróci, to wyliczy się raz na etapie inicjalizacji, na przykład. Złożyć sobie w tej strukturze danych i używać później dowolnie i już mieć z głowy w kontekście niepotrzebnych wywołań.
Logowanie a wydajność
To tutaj dobrym przykładem, znaczy w sensie unikania tych niepotrzebnych wywołań. Taki typowy kod blokujący coś tam. Gdzieś tam pracujemy nad algorytmem naszym, coś się tam dzieje i nagle log.debug
i siup, poszło. Do tego do baru jakiś kupę dyrdymałów i na przykład z jakiegoś czegoś wołamy jakąś metodę, która wylicza nam na potrzeby tego debuga wartość. No i teraz co się dzieje w takim kodzie? Mamy ten kod skompilowany. O tym wprawdzie nie wspominaliśmy, to jest może dobry moment na to, jak w ogóle procesor – wróć – procesor, tylko jakiś język programowania, czy też na przykład taki JVM, więc wykonujemy to, do czego ten stos służy. Stos wywołań nota bene. I okazuje się, że żeby wywołać jakiś kodzik, który przyjmuje argumenty, musimy te argumenty gdzieś złożyć do jakiejś pamięci. Tą pamięcią jest stos. Odkładamy je po kolei, jeden po drugim, na tym stosie, a na końcu wywołujemy jakąś funkcję w naszym kodzie, która sobie z tego stosu te argumenty ściąga i coś tam sobie z nimi robi.
No i teraz jak mamy takiego loggera i wołamy inną metodę wewnątrz tego loggera, czy wewnątrz tego, tego wywołania logującego, to wiadomo, że najpierw musi zostać wyliczony wynik tej metody, żeby ta wartość została złożona na stos po to, żeby kolejne wywołania już to nasze logowanie mogły sobie zdjąć i wykorzystać. Ale jeżeli na przykład ten logger jest na tyle sprytny, że mamy logowanie typu debug, ale flaga loggera jest ustawiona wyżej, czyli na przykład na info. To ten kodzik nie zostanie zawołany, ale te wartości, które się tam wyliczają i trafiają na stos, zostaną wyliczone. Więc tutaj lepiej jest zastosować sprytniejsze interfejsy takiego logowania, na przykład jakiś tam funkcyjny, gdzie podamy funkcję, która wyliczy nam wartość tego loga, ale tylko wtedy, kiedy ten będzie faktycznie musiał się wyliczyć. Czyli jeżeli już biblioteka, czy w ogóle mechanizm logujący, jakiego byśmy tam nie użyli, zdecyduje, że ma wyświetlić nam ten komunikat. To niech sobie przygotuje. I niech sobie wtedy zawoła wszystko, co mu potrzebne, ale tylko wtedy i ani kiedy.
No tak, podobno to optymalizacja, którą się czasami stosuje w Javie w opcjonalnych. Jeśli mamy sekcję or else
. I rzeczywiście tak na pierwszy rzut oka możemy w tym or else
wykonać jakąś tam metodę, która zwróci nam dokładnie ten sam przypadek, ale. To zawsze zostanie wykonane, więc ta metoda musi być znana, bo wynik musi być znany, więc ten or else
to zawsze będzie wykonany, nawet jeśli nie będzie zwrócony, bo tak naprawdę Optional
już przejdzie, a ten or else
to równie dobrze może w środku robić zapytanie do bazy danych, przeliczenie czegoś tam i sprawdzenie pogody, pogody, zawołanie pięciu innych mikroserwisów. A tak naprawdę nie naszą intencją było, żeby to zrobić, bo chcieliśmy to zrobić tylko jeśli tej pierwszej wartości nie ma. Tutaj musimy używać orElseGet
i tam przez Supplier, czyli przez funkcję, przez interfejs funkcyjny, przekazujemy sobie malutką funkcję, która nam to wyliczy tylko wtedy, jeśli tego przekonania nie będzie. Wielką tam przekazać. Nie ma sprawy, może być nawet wielka, bo wtedy to będzie wykonane tylko wtedy, kiedy będzie potrzeba, a nie za każdym razem, kiedy po prostu trafimy na tego. To są takie drobne rzeczy.
Na szczęście ten or else
to może być null
, więc wtedy nie ma znaczenia. Robimy or else
i i możemy. Możemy na tym poprzestać, nie trzeba robić or else get null
to ja.
To już można sobie odpuścić.
Ale, ale to są takie drobne, można, można powiedzieć optymalizacje, które w specyficznych sytuacjach rzeczywiście mogą zrobić pewną różnicę, jeśli chodzi o czas. I tak jak mamy unikać niepotrzebnych wywołań, to może potrzebnych byśmy lepiej nie unikali i choćby troszkę wykonywało i to on będzie miał, na przykład. Znaczenie przy benchmarkach. O tym później. Na pewno jak już będziemy, jak brniemy do benchmarków wreszcie, bo się może okazać, że na przykład kod będzie pakowany, nie jest włączony, bo tak nam się poskładał, że po prostu nie dochodzi do wywołania i pętla śmiga nam na pusto i mamy świetną wydajność, a tak naprawdę nie mierzymy wydajności kodu.
No tak, bo musimy się upewnić, że to, co mierzymy, trafia w odpowiednie miejsca pamięci, tak żeby czy to do rejestrów. Nawet już można takie triki stosować jak jakieś zmienne volatile tu i ówdzie, żeby się upewnić, że faktycznie ten kod czy chociażby odczytywać rezultat takiej operacji po to, żeby na przykład właśnie. Jak wiem, nie doszedł do wniosku, a tutaj mamy wynik czegoś tam nie używany, to jest tego nie wyszło, bo na co mnie to? A to nam zależało właśnie, żeby to się wywołało. Nie tylko zapomnieliśmy przypisać gdzieś wynik tego w taki sposób, żeby ten wynik przetrwał, to wywołanie i 10 milionów razy pętla nam się wykonała błyskawicznie nad wykonaniem.
Tak, tak, tak, bo to także taka prosta rzecz, ale trzeba o niej pamiętać, bo później wynik niezły, kwiatki z tego wychodzą.
Wyjątki jako wyjątki, nie reguła
No tak, co jeszcze możemy unikać w takim kodzie? Wyjątki to jest taka fajna rzecz, która w wielu językach i oczywiście w javie jest, i ten wyjątek. Trochę niewygodnie się go używanie, bo jesteśmy gdzieś tam dowolnie głęboko w strukturze tego naszego czegoś, co tam robimy, nagle się coś zepsuło. No i żeby nie robić znaną metodą z czasów C od panów tego. Gajka i raczej tak nie to co dzieci zrobili. Tego ostatniego króla Artura, Tyriona i Lichego. Ci książkę napisali dawno temu. Tak, tak, od tych dwóch panów w książce do C. Nie tylko w książce, bo w języku C też mieliśmy takie konstrukcje, gdzie jakąś tam błąd wykonania trzeba było trzymać gdzieś obok tego wykonania i jak się coś puściło, to trzeba go było propagować z palca w górę, jak byśmy gdzieś tam, nie wiem, 10 metod w środku. No to cały kodzik wywołujący się w głąb musiał za każdym razem sprawdzać stan tej zmiennej. Tego czegoś, co mu wraca z tego, z tej warstwy niższej po to, żeby móc to gdzieś tam ewentualnie przekazać wyżej, z błędem, jeżeli się tam niżej zepsuło. Taki kod błędu.
No więc wpadł na pomysł, że po co to nazywa się tak męczyć się jak wystarczy cieknąć wyjątkiem z tego środka i on sobie przeleci przez wszystkie warstwy, wyleci na górę. Jak go ktoś tam złapie to dobrze, jak nie to, to zamykamy wątek i ten albo ten proces i już i kończymy z płaczem. No i wyjątki okazują się bardzo przydatnym narzędziem. Tylko co się dzieje, jeżeli mamy ileś tam. Poziomów zagnieżdżenia, ale ze środka leci nam ten wyjątek i robimy to 10 milionów razy w pętli i ten wyjątek nam tam właśnie wraca.
Tak, to pętla chyba tutaj są najlepszym przykładem na złe miejsca. Taki wyjątek w tym punkcie.
No dokładnie, bo cały ten mechanizm wyjątku wiąże się z czymś, co musi wdzięczną nazwę: odwijania stosu. Czyli tam musimy po prostu wrócić z tym stosem wywołań i im głębiej byliśmy, tym więcej, tym dłużej się wraca. I jakkolwiek mechanizm wyjątku, bo wraca się zawsze szybciej. Zresztą w pętli to już się kolejny raz szybciej. No ale tak, może to nie być dość szybko, hehe, jak na naszego Product Ownera, który mówi, że ma być jak najszybciej. No ale to wyjątek. Rzucamy, bo wspomniałem, że wtedy nasze logi potrafią niemiłosiernie puchnąć.
Jak ktoś jeszcze loguje po kilka razy. Rotacja logów może nam też mocno wybuchnąć. A było tak, że nam kiedyś jakiś serwer padł? Bo się logi z tego przepełnienia. To nie to…
Kolega opowiadał.
Tak, tak, oczywiście, bo to raz u kolegi, najczęściej u tego samego. Także jeżeli widzimy, że nasz kodzik w jakiś sposób spodziewamy się, że ten nasz kodzik w jakiś sposób jednak. Jest krytyczny pod względem szybkości, to lepiej tam z wyjątkami ostrożnie do tego podejść. Wtedy to już nawet zdawać by się mogło przestarzałe zwracanie statusu jest po prostu lepsze, bo nie angażujemy tego wbudowanego mechanizmu, tylko co najwyżej zwracamy szybko wartość przez stos i i tyle. Jakoś trochę zaoszczędzamy ileś tam cykli naszego cennego procesora.
Zwłaszcza, że ten status to tak naprawdę nie musi być tak, jak kiedyś zwracało się minus jeden. Niewiele mówiące, poza tym, że konwencja twierdziła, że minus to jest, to jest błąd. Ale ten zwrot może być po prostu naszym jakimś biznesowym, domenowym obiektem, który opisuje status operacji. Czy może mieć w środku jakiś status, jakiś komunikat? Może jakieś dane kontekstowe?
Często może być to kontekstowe, ale w ogóle kontekst obiektu jakiś mógłby się tu przydać. Na którym te wszystkie metody w głąb pracowały i jeżeli któreś spędziła, tak. To po prostu return false
. I w tym kontekście już mamy stan tego. De facto to jest test, czy to będzie kontekst, czy jakiś stan, czy cokolwiek. Tutaj już mamy zapisane to wszystko, co z czym wracamy, a my po prostu wracamy i tyle.
Nie, dokładnie. Na końcu.
Sygnalizuje. Może wróciliśmy już takie, dopiero na końcu. Powiedz mi już poza pętlą, czy poza jakimś tam takim częściej wykonanym fragmentem? Wtedy możemy chcieć rzucić wyjątek. Jeśli ma pójść na prawdę to warstwy wyższej, albo jeśli tutaj będzie poza tą pętlą, to będzie poza pętlą on tutaj. Wewnętrznie to możemy sobie polegać jakimś fajnym obiekcie statutowym, który rzeczywiście nam lepiej opisze tę wyjątkową sytuację tak jak my z tym wyjątkiem, bo też nie ma się co bać tych wyjątków tak do samego końca. I jak już ktoś powie gdzieś tam w internetach, że wyjątków to się nie rzuca, to teraz wszyscy przestają rzucać wyjątki. Bo wyjątkiem jest jednak fajny mechanizm, bo na pewno w niektórych sytuacjach dochodzi do sytuacji insert. Sytuacja do sytuacji dochodzi wyjątkowej, gdzie nie wiadomo co zrobić. Nie i nawet nie wiadomo. Co by użytkownik chciał zrobić, więc niech zdecyduje co zrobić, czy zamknie to całkiem, czy po prostu coś ponowił, czy to cokolwiek, czyli coś nam się zepsuło. Nie połączyliśmy się z bazą. No to wracamy i mówimy w Bąbla, który był dzięki niemu tam na ekranie, że hej, sorry, baza w tym momencie nie odpowiada. No i rób co chcesz.
Czyli wyjątek powinien być wyjątkiem, a nie regułą.
Tak, dokładnie. Wyjątek powinien być wyjątkiem, ale nie powinien być stosowany do kontroli przepływu w aplikacji, bo jeżeli jest stosowany do kontroli przepływu, to sorry, ale coś tu się pogubił, bo my mamy instrukcję do tego, albo konstrukcje wręcz w języku przewidziane w języku, we frameworkach. Na wielu różnych poziomach mamy tylko instrukcje przewidziane do tego, żeby kontrolować przepływ. Na przykład jedną z takich konstrukcji jest nasza Event Loop. To jest już taka zaawansowana konstrukcja do kontrolowania przepływu. Ale to jest właśnie sedno tej antologii, a nie wyjątkowość bloga.
Optymalizacja kodu pod kątem typowego użycia i zarządzanie pamięcią
Dobra, to tak, unikamy wyjątków. No i co? I optymalizujemy kod pod kątem typowego użycia, czyli. Czyli krótko czy typowo użycie? Poza tym to nie ma się co rozwodzić zanadto nad tym tematem. No to to już właściwie o tym to już powiedzieliśmy, bo, bo potem, bo to było wcześniej wzmiankowane, żeby właśnie te składowe tych naszych algorytmów, tak? I przykrywać, żeby one odpowiadały tym wykonywanym naszym hotspotom, u mnie gorącym planom w domenie. Które będą najczęściej wykonywane tak samo jak maszynka wirtualna będzie przykrywać ten nasz kodzik do tego, co się tam najczęściej wykonuje.
No i jeszcze w kontekście w kontekście maszynki jako takiej można by też zagaić o tym tych referencjach, bo wiadomo, że dostęp do pamięci mamy przez referencje do obiektów w świecie JVM. I teraz jest taki magiczny mechanizm Garbage Collector, który to ma za zadanie nam pousuwać obiekty, bo nam w Javie nie dali operatorowi delete
jak w C++. Więc mamy Garbage Collector, mamy pamięć zarządzaną. Mamy referencje, przez którą trzymamy sobie obiekty, które leżą w tej pamięci. I teraz dobrze jest, żeby nieużywane obiekty nie były trzymane. Bo jak obiekt jest trzymany, to Garbage Collector go nie usuwa, bo jest na tyle uprzejmy, żeby nam po prostu nie wyrywać z rąk tego, nad czym aktualnie pracujemy. Więc jeżeli widzi, że gdzieś tam referencja jest użyta w jakiejś kolekcji, to ta kolekcja jest użyta w innej kolekcji. To tak sobie hierarchicznie jest zbudowany cały graf obiektów, bo gdzieś tam jest jakaś referencja do magicznego roota tego wszystkiego. Wielki korzeń tego grafu obiektów, jest obiektów, jest trzymany gdzieś tam w aplikacji i jest osiągalny. Jeśli jest osiągalny, to go Garbage Collector nie złapie i też nie będzie przepuszczał jego dzieci, więc będziemy mieli cały czas cuchnącą pamięć i średnie, średnią korzyść z klikania tego piasku, co, co klipsem.
No i czasem. Czasem pomaga, to nie tam jednak nie trzymają tego wszystkiego. Generalnie jeżeli zmienne są, jakby nowym świecie, niepotrzebne, to to co to jest? Nie trzymajmy ich nigdzie. Pozwólmy im po prostu wyjść ze scope’u i tyle. I jak już licznik referencji takiej zmiennej, gdzie każda właściwie zmienna, która jest obiektem, nie jest prymitywem, ma ten licznik referencji, bo to jest zaszyte właśnie chyba w okolicach tego, chyba…
Tak to jest w tym folderze magicznym, gdzie siedzą różne flagi, to tam sobie odnośnik albo z kolektora.
No dokładnie, on musi wiedzieć, czy obiekt jest w użyciu, czy nie jest, więc pozwólmy mu nie być w użyciu temu obiektowi, żeby Garbage Collector go zgarnął i żeby nam pamięć od śmieci. No i chyba, nie wiem, tak z takich rzeczy podstawowych. To chyba byłyby takie wytyczne najgorętsze, żeby sobie w ogóle umożliwić pisanie programów, które później faktycznie będą miały mieszaną wydajność. Pewnie jak nam coś przyjdzie jeszcze do głowy w dalszych częściach, to będziemy o tym wspominać.
Tak, to temat na pewno jest.
Na pewno już, na pewno o czymś zapomnieliśmy i na pewno to wyjdzie.
Przykłady optymalizacji i nauka na błędach
A może tak jeszcze na koniec? Czy miałeś taki ciekawy przykład użycia jakiejś ciekawej, fajnej optymalizacji? No bo rzeczywiście, nie ukrywajmy, że nieczęsto się spotyka, zwłaszcza w takich aplikacjach stricte biznesowych, jak już wspominaliśmy. Żeby w ogóle móc zastosować jakieś takie fajne triki czy inne sztuczki.
A to są na pewno jakieś ciekawe rzeczy i będą ciekawe rzeczy do omówienia w tej części poświęconej tej stali, gdzie będziemy opowiadać o silniku dla szachów. To to jest taka najfajniejsza rzecz. I miałem. Miałem niewątpliwą przyjemność pracować przy przepisywaniu, a właściwie przy pisaniu nowej wersji już działającego silnika takich szachów. Szachy były dla 4 graczy jednocześnie, więc tam też ciekawsze zasady. Jak się okazuje, to zasady szachów dla dwóch graczy są już na tyle rozluźnione. Są gotowe biblioteki nawet dla jakich po prostu bierzesz taki język, nie działa, i już. Nie trzeba nawet w czapce, bo wystarczy wziąć gotowy, jakiś zoptymalizowany, i on po prostu śmiga jak, jak dziki.
Ale taki powszechny, nie? Jest rzeczywiście, no, osobowe przynajmniej.
Tak, tak, są ludzie, to już rozpisał już naprawdę, jeśli wierzyć literaturze, to ciężko wygrać z takim Androidem, nawet będąc takim graczem, wiadomo niedzielnym. Ale już takie szachy dla czterech graczy to już jest ciekawsze zagadnienie. No i tam miałem okazję troszkę optymalizować i właśnie te techniki, o których tu wspominaliśmy, jak najbardziej tam były w użyciu.
Ale tak całkiem niedawno miałem też przy okazji tego słownika takiego małego, takie małe olśnienie, jak na przykład właśnie. Próbowałem tam zbudować indeksy dla, dla jakiś takich zbiorów czteromilionowych i mi uparcie tak po szóstej minucie takiego indeksowania pamięć dochodziła do 10 GB i nagle proces został unicestwiony. Taki komunikat dostawałem dosłownie tak: nie, proces został unicestwiony i tyle.
Nie tak gwałtownie.
I gwałtownie, ale proces też nie był taki super cool, bo 10 GB sobie zablokował i ani myślał przestawiać. Tak myślę. Coś co ten Garbage Collector jakoś tak tak średnio mi to zwalnia, spisuje. Na co komu taki Garbage Collector? No i zacząłem tam sprawdzać, o co w ogóle chodzi. Zerknąłem w ten kodzik. No i co się okazało? Że właśnie w tych czasach jak testowałem ten algorytm na tych właśnie małych zestawach danych, potrzebowałem szybkiego wglądu takiego debuggera w to, co tam się w ogóle dzieje. Więc sobie wrzucając te poszczególne cząstki do indeksu, bo cały ten indeks to jest taka wielopoziomowa struktura, gdzie dla poszczególnych długości fraz, poczynając od 2 znakowych, a kończąc na maksymalnej – ja tam maksimum ustawiłem na 20 znaków, bo więcej to nie ma za bardzo sensu – buduje sobie taki indeks tychże właśnie fraz na podstawie wszystkich słów ze słownika. Czyli mam ten słownik o umownej ilości 4 miliony 200 haseł i tam sobie jadę po kolei najpierw naukowcy, trójcy, naukowcy i takie tam, nie?
Więc wpadłem na pomysł, że łatwiej mi się to chyba debugowało, żeby wziąć te indeksy, które już mi się budowały. A to są takie wielkie mapy, wrzucić je do jednej wielkiej mapy zbiorczej i będzie. No i później oczywiście jak to miało kilkadziesiąt haseł, no to taka mapa była malutka i ja sobie robiłem na niej jakiegoś taniej, to tam piękne drzewko wpisywało i widziałem co tam. Zresztą nie musiałem drzewka wypisywać. Wystarczyło, że zatrzymałem na końcu i tam widziałem, co, jak, gdzie to było poindeksowane – to było bardzo przydatne narzędzie. No i zostawiłem. Później odpaliłem to dla tego wielkiego słownika i to rosło w nieskończoność, bo tam mi te rzeczy wyskakiwały. To jest taka beznadziejnie prosta ilustracja do tego, żeby uważać na utrzymywane referencje.
Wyzwania z alokacją pamięci i optymalizacja
Mapy
w Javie też są dość kosztowną operacją, więc jeśli dodałeś kolejne elementy i ona się później musiała… i dlatego, by proces nie dochodziło do magicznej… ustawiłem na przykład limit na 16 giga dla maszynki. W sensie pamięć maksymalną, to już później się bawiłem flagami i poszedłem na całość. To właściwie i dałem 16 giga, bo już stwierdziłem, że jak na 16 mi nie przejdzie, to znaczy, że trzeba będzie optymalizować.
To był ten punkt. I to był ten punkt. No i akurat przy tych 16 właśnie chyba dochodziło do tego, o czym wspomniałeś. Czyli mapa musiała się w którymś momencie przereorganizować i już jej nie dotykało, bo nie była w stanie nowa mapa powstać, w której by się to wszystko pomieściło. Więc po prostu out of memory
i proces unicestwiony i tyle.
No i optymalizacja polegała na wyrzuceniu tej mapy po prostu i zajętość pamięci spadła do 400 mega maksymalnie. A jeszcze dodatkowo pokusiłem się, jak byłem na tyle wkurzony tym wszystkim, że po prostu zrobiłem to, co się robi, czyli zawołałem Garbage Collector na końcu takiego indeksowania.
Pomiar wydajności i rola Garbage Collectora
I teraz java.lang.System.gc()
– można sobie wymusić zawsze.
Dokładnie. Bierzemy sobie Runtime
z Runtime
, bierzemy gc()
i poszło. I się okazuje, że tam jest jeszcze taki miernik, taki bardzo prosty, bo on pokazuje użytą pamięć i pamięć totalną JVM. Więc sobie zrobiłem szybkie matematyczne obliczenia z sizeof
tego wszystkiego. I na początku zanim ten indeks mi się budował, zajętość – to taka ciekawostka – w ogóle użycie pamięci przez taką małą aplikację Javową na poziomie ośmiu mega, na przykład. Rzecz niespotykana przy jakichkolwiek większych aplikacjach, ale tutaj zaczynało od 8 mega i potrafiło do 10 giga urosnąć.
Ale po zastosowaniu tych wszystkich cache'ingów
plus czyszczenie wymuszonym kolektorem to mi zajętość pamięci rosła do 400-500 mega. To zależy od akurat długości tej frazy indeksującej, bo po prostu różnią się rozmiarem, bo jest więcej lub mniej tych elementów. W sumie na całym tym zbiorze słów i na przykład dla tych krótszych fraz jest ich bardzo dużo, a dla dłuższych już jest mało. Łatwo się domyślić, że na przykład dla takich 20-znakowych to jest dosłownie kilkanaście albo kilkadziesiąt. Przy tych 4.2 miliona haseł to tam będzie z kilkaset, może do kilku tysięcy co najwyżej. Tam chodzi o odmiany. No ale mniejsza o to, nie taka domena.
No i się okazało, że to po prostu dostaje takie ząbki, które rosną od tych ośmiu mega do kilkuset i później spadają do 8. I tak dla wszystkich tych fraz. I nie dość, że to działa szybciej, zauważalnie szybciej dla oka, po prostu ja tam więcej żadnych mierników nie zrobiłem, bo ja tylko pamięć sobie tam wyświetlałem. To przestało się wywalać i po prostu działa. Taka prosta mała rzecz, a cieszy.
Metryka sukcesu i różnice wersji Javy
Najlepsza metryka. Aplikacja się nie wywala. I wywala się, działa i jeszcze nawet da się doczekać do końca tego działania. W takim przypadku jak mój, gdzie PO interesowałby ten, ten słownik i się zbierała.
I jeszcze mało tego, bo odpaliłem to najpierw na swoim laptopie i tutaj jako że tak bardzo lubię stare Javki, to mam jedenastkę, a później odpaliłem to na serwerze, na którym sobie zainstalowałem 19 i tam zadziałało szybciej. Ciekawostka przyrodnicza. Okazuje się, że faktycznie ten łopatologiczny benchmark pokazał mi, że Java 19 jest szybsza od mojej jedenastki.
No właśnie, gdzie w ogóle ta maszyna serwerowa nie była jakoś dramatycznie szybsza od mojego laptopa?
Czyli kolejny dowód na to, że warto.
Choć biblioteki nie mają czerwonej żadnej gierki w tle na swoim protopie, to można tu domniemywać, że z grubsza wydajności się zgadza, że za bardzo nie oszukiwałem na swoim.
Czyli jednak 19, 19-tka lepiej sobie optymalizuje na pewno wykonanie kodu. Czyli widać już, że te algorytmy poszły do przodu i lepiej zarządza pamięcią, bo tam jeszcze były lepsze, lepsze wartości dla dokładnie tego samego zestawu danych. Czyli algorytm na pewno wykonywał się krok po kroku. Na pewno to nie, bo nie mogę powiedzieć, że na pewno, bo pewnie zostały jakoś tam optymalizowane i też pewnie lepsze optymalizacje ma 19 względem 11, to bez dwóch zdań, ale pod względem zarządzania pamięcią, gdzie jednostka wygrywa i Garbage Collector, to tutaj…
Zanim to zaobserwowałem, to jeszcze lubiłem hejtować Garbage Collectory, ale teraz już mi przeszło.
Podsumowanie i rekomendacje
No ale to taki, taki przykład z życia i stali mikro na dzisiaj może by wystarczył. A no chyba w następnym odcinku już pojedziemy z kolejnymi etapami naszych optymalizacji. Właściwie jakoś wreszcie do człapiemy do tego mierzenia wydajności. No i na końcu czekają nas te szachy jako taki przykład. Jak to się optymalizuje.
Jeszcze zanim do tego dojdziemy, bo już tutaj wkroczyliśmy w teorię. Jeszcze warto wspomnieć może o takiej książce, która jest teraz łatwo osiągalna w dwóch wersjach: po angielsku i po polsku. Po polsku z Helionu. Oczywiście tam oprócz spiętrzenia wątków, tu to nie mamy się do czego przyczepić.
Spiętrzenie, spiętrzenie wątków, kij wie co to, ale jeszcze nie Google.
„Wydajność Javy. Szczegółowe porady dotyczące programowania i strojenia aplikacji w Javie” Scotta Oaks’a.
Rok 2021 Helion. No i tam chyba 20 albo 21.
Spory mi. Jakoś tak. W każdym razie Scott Oaks to jest taki gość.
Dobrze pójdzie, to producent dorzuci link do opisu, a właściwie to go tu przypilnuje i dorzuci, to będzie łatwiej i łatwiej w to kliknąć. Albo sobie po prostu wygooglować i kupić. Książeczkę się czyta.
No nie gorzej od.
I to do mnie, nie tylko do Wiedźmina porównywał.
Trochę gorzej się czytało, ale tylko troszkę.
Tylko troszkę. Miejmy nadzieję, że nie będzie z tego serialu, więc może i lepiej. Nie, zachowajmy tego. Ale, ale jednak na potwory też tam się poluje i i ubija to takie co zaburzają wydajność. Dlatego książeczkę polecamy.
I co? I chyba tyle na dzisiaj, bo się rozgadaliśmy, a to dopiero na początku jesteśmy – ???? kurka wodna.
Temat szeroki i ciekawy, ciekawy
i ciekawy
i wkurzający
i wkurzający.
To częsta kombinacja.
Dobra, super. Dzięki Michał za dzisiaj.
Dzięki również. I do zobaczenia w następnym odcinku.
Do następnego.