Czy Java jest znacznie trudniejsza do „ulepszenia” pod względem wydajności w porównaniu z C / C ++? [Zamknięte]

11

Czy „magia” JVM utrudnia wpływ programisty na mikrooptymalizacje w Javie? Niedawno czytałem w C ++, czasem porządkowanie elementów danych może zapewnić optymalizację (przyznane, w środowisku mikrosekund) i założyłem, że ręce programisty są związane, jeśli chodzi o wyciskanie wydajności z Javy?

Rozumiem, że przyzwoity algorytm zapewnia większy wzrost prędkości, ale kiedy masz prawidłowy algorytm, trudniej jest dostosować Javę ze względu na kontrolę JVM?

Jeśli nie, ludzie mogą podać przykłady sztuczek, które można zastosować w Javie (oprócz prostych flag kompilatora).

użytkownik997112
źródło
14
Podstawowa zasada optymalizacji Javy jest następująca: JVM prawdopodobnie zrobił to już lepiej niż ty. Optymalizacja polega głównie na stosowaniu rozsądnych praktyk programowania i unikaniu zwykłych rzeczy, takich jak łączenie łańcuchów w pętli.
Robert Harvey,
3
Zasadą mikrooptymalizacji we wszystkich językach jest to, że kompilator już to zrobił lepiej niż ty. Inną zasadą mikrooptymalizacji we wszystkich językach jest to, że włożenie na nią większej ilości sprzętu jest tańsze niż mikrooptymalizacja czasu programisty. Programista musi mieć problemy ze skalowaniem (algorytmy nieoptymalne), ale mikrooptymalizacja to strata czasu. Czasami mikrooptymalizacja ma sens w systemach wbudowanych, w których nie można wrzucać na nią więcej sprzętu, ale Android z Javą i raczej jej słaba implementacja pokazują, że większość z nich ma już wystarczającą ilość sprzętu.
Jan Hudec
1
dla "Java sztuczek Performance", warto studiować to: Effective Java , Angelika Langer Linki - Java wydajność i skuteczność w zakresie artykuły Brian Goetz w teorii i praktyce Java i Threading lekko serii wymieniony tutaj
gnat
2
Zachowaj szczególną ostrożność w zakresie wskazówek i sztuczek - maszyna JVM, systemy operacyjne i sprzęt poruszają się dalej - najlepiej poznaj metodologię dostrajania wydajności i zastosuj ulepszenia w swoim konkretnym środowisku :-)
Martijn Verburg
W niektórych przypadkach maszyna wirtualna może przeprowadzać optymalizacje w czasie wykonywania, których wykonanie jest niepraktyczne w czasie kompilacji. Korzystanie z pamięci zarządzanej może poprawić wydajność, choć często wiąże się to z większym obciążeniem pamięci. Niewykorzystana pamięć jest zwalniana, gdy jest to wygodne, a nie JAK NAJSZYBCIEJ.
Brian,

Odpowiedzi:

5

Jasne, na poziomie mikrooptymalizacji JVM zrobi pewne rzeczy, nad którymi będziesz miał niewielką kontrolę, szczególnie w porównaniu do C i C ++.

Z drugiej strony różnorodność zachowań kompilatora szczególnie w C i C ++ będzie miała o wiele większy negatywny wpływ na twoją zdolność do mikrooptymalizacji w jakikolwiek sposób mało przenośny (nawet w różnych wersjach kompilatora).

To zależy od tego, jaki projekt poprawiasz, jakie środowiska docelowe i tak dalej. I w końcu nie ma to tak naprawdę znaczenia, ponieważ i tak uzyskuje się kilka rzędów wielkości lepszych wyników z optymalizacji algorytmicznej / struktury danych / projektu.

Telastyn
źródło
To może mieć duże znaczenie, gdy okaże się, że Twoja aplikacja nie skaluje się w różnych rdzeniach
James
@james - chcesz opracować?
Telastyn
1
@James, skalowanie między rdzeniami ma bardzo mało wspólnego z językiem implementacji (z wyjątkiem Pythona!), A więcej z architekturą aplikacji.
James Anderson
29

Mikrooptymalizacje prawie nigdy nie są warte czasu, a prawie wszystkie łatwe są wykonywane automatycznie przez kompilatory i środowiska wykonawcze.

Istnieje jednak jeden ważny obszar optymalizacji, w którym C ++ i Java różnią się zasadniczo, a mianowicie dostęp do pamięci masowej. C ++ ma ręczne zarządzanie pamięcią, co oznacza, że ​​możesz zoptymalizować układ danych aplikacji i wzorce dostępu, aby w pełni wykorzystać pamięci podręczne. Jest to dość trudne, nieco specyficzne dla sprzętu, na którym pracujesz (więc wzrost wydajności może zniknąć na innym sprzęcie), ale jeśli zrobisz to dobrze, może to prowadzić do absolutnie zapierającej dech w piersiach wydajności. Oczywiście płacisz za to z potencjalnym rodzajem straszliwych błędów.

W przypadku śmieciowego języka, takiego jak Java, tego rodzaju optymalizacji nie można wykonać w kodzie. Niektóre mogą być wykonane przez środowisko wykonawcze (automatycznie lub przez konfigurację, patrz poniżej), a niektóre są po prostu niemożliwe (cena, którą płacisz za ochronę przed błędami zarządzania pamięcią).

Jeśli nie, ludzie mogą podać przykłady sztuczek, które można zastosować w Javie (oprócz prostych flag kompilatora).

Flagi kompilatora nie mają znaczenia w Javie, ponieważ kompilator Java prawie nie optymalizuje; środowisko wykonawcze działa.

Rzeczywiście, środowiska wykonawcze Java mają wiele parametrów, które można modyfikować, szczególnie w przypadku śmieciarza. W tych opcjach nie ma nic „prostego” - wartości domyślne są dobre dla większości aplikacji, a uzyskanie lepszej wydajności wymaga dokładnego zrozumienia, co robią opcje i jak zachowuje się aplikacja.

Michael Borgwardt
źródło
1
+1: w zasadzie to, co napisałem w odpowiedzi, może lepsze sformułowanie.
Klaim
1
+1: Bardzo dobre punkty, wyjaśnione w bardzo zwięzły sposób: „Jest to dość trudne ... ale jeśli zrobione dobrze, może prowadzić do absolutnie zapierającej dech w piersiach wydajności. Oczywiście płacisz za to z potencjałem dla wszelkiego rodzaju strasznych błędów . ”
Giorgio,
1
@MartinBa: Za optymalizację zarządzania pamięcią płacisz więcej. Jeśli nie spróbujesz zoptymalizować zarządzania pamięcią, zarządzanie pamięcią C ++ nie będzie takie trudne (unikaj jej całkowicie przez STL lub względnie łatwo używaj RAII). Oczywiście implementacja RAII w C ++ wymaga więcej wierszy kodu niż nic nie robić w Javie (tj. Ponieważ Java obsługuje go za Ciebie).
Brian,
3
@Martin Ba: Zasadniczo tak. Zwisające wskaźniki, przepełnienia bufora, niezainicjowane wskaźniki, błędy w arytmetyce wskaźników, wszystkie rzeczy, które po prostu nie istnieją bez ręcznego zarządzania pamięcią. I optymalizacji dostępu do pamięci dość dużo wymaga, aby zrobić dużo ręcznego zarządzania pamięcią.
Michael Borgwardt,
1
Jest kilka rzeczy, które możesz zrobić w Javie. Jednym z nich jest pula obiektów, która maksymalizuje szanse na lokalizację obiektów w pamięci (w przeciwieństwie do C ++, gdzie może zagwarantować lokalizację pamięci).
RokL
5

[...] (przyznane w środowisku mikrosekund) [...]

Mikrosekundy sumują się, jeśli zapętlamy ponad miliony do miliardów rzeczy. Osobista sesja optymalizacji vtune / mikro z C ++ (bez ulepszeń algorytmicznych):

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

Wszystko oprócz „wielowątkowości”, „SIMD” (odręcznie pokonany kompilator) oraz optymalizacji łatki 4-walencyjnej były optymalizacjami pamięci na poziomie mikro. Również oryginalny kod, począwszy od początkowych czasów 32 sekund, został już dość zoptymalizowany (teoretycznie optymalna złożoność algorytmu) i jest to ostatnia sesja. Przetwarzanie oryginalnej wersji na długo przed ostatnią sesją zajęło ponad 5 minut.

Optymalizacja wydajności pamięci może często pomóc w dowolnym miejscu, od kilku razy do rzędów wielkości w kontekście jednowątkowym, a więcej w kontekstach wielowątkowych (korzyści z wydajnego rep pamięci często mnożą się z wieloma wątkami w mieszance).

O znaczeniu mikrooptymalizacji

Trochę niepokoi mnie myśl, że mikrooptymalizacje to strata czasu. Zgadzam się, że to dobra ogólna rada, ale nie wszyscy robią to niepoprawnie w oparciu o przeczucia i przesądy, a nie pomiary. Prawidłowo wykonane nie musi wywoływać mikro uderzenia. Jeśli weźmiemy własny Embree (jądro raytracing) Intela i przetestujemy tylko prosty skalarny BVH, który napisali (nie pakiet ray, który jest wykładniczo trudniejszy do pokonania), a następnie spróbujemy pokonać wydajność tej struktury danych, może to być najbardziej upokarzające doświadczenie nawet dla weterana przyzwyczajonego do profilowania i strojenia kodu przez dziesięciolecia. A wszystko to dzięki zastosowanym mikrooptymalizacjom. Ich rozwiązanie może przetwarzać ponad sto milionów promieni na sekundę, gdy widziałem specjalistów przemysłowych pracujących w raytracingu, którzy potrafią „

Nie ma sposobu, aby zastosować prostą implementację BVH z jedynie algorytmicznym skupieniem i uzyskać ponad sto milionów przecięć pierwotnego promienia na sekundę w stosunku do dowolnego kompilatora optymalizującego (nawet własnego ICC Intela). Prosty często nie dostaje nawet miliona promieni na sekundę. Wymaga rozwiązań profesjonalnej jakości, aby często uzyskać nawet kilka milionów promieni na sekundę. Mikrooptymalizacja na poziomie Intela pozwala uzyskać ponad sto milionów promieni na sekundę.

Algorytmy

Myślę, że mikrooptymalizacja nie jest ważna, dopóki wydajność nie jest ważna na poziomie minut do sekund, np. Godzin lub minut. Jeśli weźmiemy przerażający algorytm, taki jak sortowanie bąbelkowe, i wykorzystamy go jako przykład danych wejściowych masy, a następnie porównamy go nawet z podstawową implementacją sortowania korespondencji seryjnej, przetworzenie tego pierwszego może potrwać miesiące, a w rezultacie 12 minut. złożoności kwadratowej vs liniowo-rytmicznej.

Różnica między miesiącami a minutami prawdopodobnie sprawi, że większość ludzi, nawet tych, którzy nie pracują w obszarach krytycznych pod względem wydajności, uważa czas wykonania za niedopuszczalny, jeśli wymaga to od użytkowników oczekiwania miesięcy na uzyskanie wyniku.

Tymczasem, jeśli porównamy niezoptymalizowany mikro-prosty, prosty sposób scalania z sortowaniem scalonym (który wcale nie jest lepszy algorytmicznie od sortowania scalonego i oferuje jedynie ulepszenia na poziomie mikro dla lokalizacji odniesienia), mikrooptymalizowany szybki zestaw może zakończyć się w 15 sekund zamiast 12 minut. Zmuszanie użytkowników do czekania na 12 minut może być całkowicie do przyjęcia (rodzaj przerwy na kawę).

Myślę, że ta różnica jest prawdopodobnie nieistotna dla większości ludzi, powiedzmy, od 12 minut do 15 sekund, i dlatego mikrooptymalizacja jest często uważana za bezużyteczną, ponieważ często przypomina jedynie różnicę między minutami a sekundami, a nie minutami i miesiącami. Innym powodem, dla którego uważam, że jest bezużyteczny, jest to, że często stosuje się go w obszarach, które nie mają znaczenia: jakiś niewielki obszar, który nie jest nawet zapętlony i krytyczny, co daje pewną wątpliwą różnicę 1% (co może być po prostu hałasem). Ale dla osób, które dbają o tego rodzaju różnice czasowe i są skłonne zmierzyć i zrobić to dobrze, myślę, że warto zwrócić uwagę przynajmniej na podstawowe pojęcia hierarchii pamięci (szczególnie na wyższe poziomy związane z błędami strony i brakami pamięci podręcznej) .

Java pozostawia dużo miejsca na dobre mikrooptymalizacje

Uff, przepraszam - z takim narzekaniem na bok:

Czy „magia” JVM utrudnia wpływ programisty na mikrooptymalizacje w Javie?

Trochę, ale nie tak bardzo, jak ludzie mogą pomyśleć, jeśli zrobisz to dobrze. Na przykład, jeśli wykonujesz przetwarzanie obrazu, w natywnym kodzie z ręcznie napisaną kartą SIMD, wielowątkowością i optymalizacją pamięci (wzorce dostępu, a być może nawet reprezentacja w zależności od algorytmu przetwarzania obrazu), łatwo jest zgnieść setki milionów pikseli na sekundę przez 32- bit RGBA (8-bitowe kanały kolorów), a czasem nawet miliardy na sekundę.

Nie można zbliżyć się do Javy, jeśli powiesz, że stworzyłeś Pixelobiekt (to samo zwiększyłoby rozmiar piksela z 4 bajtów do 16 na 64-bit).

Ale możesz być w stanie podejść o wiele bliżej, jeśli unikniesz Pixelobiektu, użyjesz tablicy bajtów i zamodelujesz Imageobiekt. Java jest nadal dość kompetentna, jeśli zaczniesz używać tablic zwykłych starych danych. Próbowałem już tego rodzaju rzeczy w Javie i byłem pod dużym wrażeniem, pod warunkiem , że nie stworzysz wszędzie małych małych obiektów, które są 4 razy większe niż normalnie (np. Użyj intzamiast Integer) i zaczniesz modelować masowe interfejsy jak Imageinterfejs, a nie Pixelinterfejs. Zaryzykuję nawet stwierdzenie, że Java może konkurować z wydajnością C ++, jeśli zapętlasz stare, zwykłe dane, a nie obiekty (ogromne tablice float, np. Nie Float).

Być może nawet ważniejsze niż rozmiary pamięci jest to, że tablica intgwarantuje ciągłą reprezentację. Tablica Integernie. Ciągłość jest często niezbędna dla lokalizacji odniesienia, ponieważ oznacza, że ​​wiele elementów (np. 16 ints) może zmieścić się w jednej linii pamięci podręcznej i potencjalnie być dostępnym razem przed eksmisją dzięki wydajnym wzorcom dostępu do pamięci. Tymczasem pojedynczy Integermoże być spleciony gdzieś w pamięci, a otaczająca pamięć jest nieistotna, tylko po to, aby ten obszar pamięci został załadowany do linii pamięci podręcznej, aby użyć tylko jednej liczby całkowitej przed eksmisją, w przeciwieństwie do 16 liczb całkowitych. Nawet jeśli mieliśmy cudowne szczęście i otoczenieIntegersbyły w porządku obok siebie w pamięci, możemy zmieścić tylko 4 w linii pamięci podręcznej, do której można uzyskać dostęp przed eksmisją, ponieważ Integerjest 4 razy większy, i to jest najlepszy scenariusz.

Jest tam wiele mikrooptymalizacji, ponieważ jesteśmy zunifikowani w ramach tej samej architektury / hierarchii pamięci. Wzorce dostępu do pamięci są ważne bez względu na to, jakiego języka używasz, pojęcia takie jak kafelkowanie / blokowanie pętli mogą być generalnie stosowane znacznie częściej w C lub C ++, ale w równym stopniu korzystają z języka Java.

Niedawno czytałem w C ++ czasami porządkowanie członków danych może zapewnić optymalizacje [...]

Kolejność elementów danych na ogół nie ma znaczenia w Javie, ale to w większości dobra rzecz. W C i C ++ zachowanie kolejności elementów danych jest często ważne z powodów ABI, więc kompilatory nie mają z tym problemu. Pracujący tam programiści muszą być ostrożni, wykonując czynności takie jak rozmieszczanie członków danych w porządku malejącym (od największego do najmniejszego), aby uniknąć marnowania pamięci na wypełnianie. W przypadku Javy najwyraźniej JIT może zmieniać kolejność elementów w locie, aby zapewnić prawidłowe wyrównanie przy jednoczesnym zminimalizowaniu wypełniania, więc pod warunkiem, że tak jest, automatyzuje coś, co przeciętni programiści C i C ++ często robią źle i w ten sposób marnują pamięć ( co nie tylko marnuje pamięć, ale często marnuje prędkość, niepotrzebnie zwiększając krok między strukturami AoS i powodując więcej braków pamięci podręcznej). To' jest bardzo robotyczną rzeczą do zmiany układu pól w celu zminimalizowania paddingu, więc idealnie ludzie nie radzą sobie z tym. Jedynym momentem, w którym rozmieszczenie pól może mieć znaczenie w sposób, który wymaga od człowieka znajomości optymalnego ustawienia, jest to, że obiekt jest większy niż 64 bajty, a my układamy pola w oparciu o wzorzec dostępu (nie optymalne wypełnienie) - w takim przypadku może być przedsięwzięciem bardziej ludzkim (wymaga zrozumienia kluczowych ścieżek, z których niektóre są informacjami, których kompilator nie mógłby przewidzieć, nie wiedząc, co użytkownicy zrobią z oprogramowaniem).

Jeśli nie, ludzie mogą podać przykłady sztuczek, które można zastosować w Javie (oprócz prostych flag kompilatora).

Największą różnicą dla mnie pod względem optymalizującej mentalności między Javą a C ++ jest to, że C ++ może pozwalać na używanie obiektów nieco (nieco mniejszych) niż Java w scenariuszu krytycznym pod względem wydajności. Na przykład C ++ może zawijać liczbę całkowitą do klasy bez żadnego narzutu (testowany w każdym miejscu). Java musi mieć ten styl metadanych w stylu wskaźnika + wypełnienia wyrównania na obiekt, dlatego Booleanjest większy niż boolean(ale w zamian zapewnia jednolite korzyści z odbicia i możliwość zastąpienia dowolnej funkcji nieoznaczonej jak finaldla każdego UDT).

W C ++ jest nieco łatwiej kontrolować ciągłość układów pamięci w niejednorodnych polach (np. Przeplatanie liczb zmiennoprzecinkowych i liczb całkowitych w jednej tablicy poprzez strukturę / klasę), ponieważ lokalizacja przestrzenna jest często gubiona (lub przynajmniej traci się kontrolę) w Javie podczas przydzielania obiektów za pomocą GC.

... ale często rozwiązania o najwyższej wydajności często i tak je dzielą i wykorzystują wzorzec dostępu SoA na ciągłych tablicach zwykłych starych danych. Tak więc w obszarach, które wymagają najwyższej wydajności, strategie optymalizacji układu pamięci między Javą i C ++ są często takie same i często zmuszają cię do demolowania tych niewielkich interfejsów obiektowych na rzecz interfejsów w stylu kolekcji, które mogą wykonywać takie czynności jak hot / dzielenie pola zimnego, powtórzenia SoA itp. Niejednorodne powtórzenia AoSoA wydają się w Javie trochę niemożliwe (chyba że użyłeś surowej tablicy bajtów lub czegoś podobnego), ale są to rzadkie przypadki, w których obasekwencyjne i losowe wzorce dostępu muszą być szybkie, a jednocześnie mieć mieszankę typów pól dla gorących pól. Dla mnie większość różnic w strategii optymalizacji (na ogólnym poziomie) między tymi dwoma jest sporna, jeśli sięgasz po szczytową wydajność.

Różnice różnią się znacznie bardziej, jeśli po prostu sięgasz po „dobrą” wydajność - nie jest w stanie zrobić tyle z małymi obiektami, jak Integervs., intmoże być trochę bardziej PITA, szczególnie ze względu na sposób, w jaki współdziała z lekami generycznymi . Jest to nieco trudniejsze, aby po prostu zbudować jeden rodzajowy struktury danych jako centralny cel optymalizacji w Javie, który pracuje dla int, floatitp unikając tych większych i droższych UDTs, ale często najbardziej obszary wydajności krytycznych wymagać będzie ręcznie toczenia własnych struktur danych i tak dostrojony do bardzo konkretnego celu, więc denerwuje tylko kod, który dąży do dobrej wydajności, ale nie do maksymalnej wydajności.

Obiekt nad głową

Zauważ, że narzut obiektu Java (metadane i utrata lokalizacji przestrzennej oraz tymczasowa utrata lokalizacji czasowej po początkowym cyklu GC) jest często duży dla rzeczy, które są naprawdę małe (jak intvs. Integer), które są przechowywane przez miliony w jakiejś strukturze danych, która w dużej mierze przylegające i dostępne w bardzo ciasnych pętlach. Wydaje się, że w tym temacie jest dużo wrażliwości, więc powinienem wyjaśnić, że nie chcesz się martwić o narzut obiektów w przypadku dużych obiektów, takich jak obrazy, tylko bardzo małe obiekty, takie jak pojedynczy piksel.

Jeśli ktoś ma wątpliwości co do tej części, proponuję zrobić punkt odniesienia między zsumowaniem miliona losowych intsa milionem losowych Integersi zrobić to wielokrotnie ( Integersprzetasowanie pamięci po początkowym cyklu GC).

Ultimate Trick: projekty interfejsów, które pozwalają zoptymalizować

Tak więc najlepsza sztuczka Java, jaką widzę, jeśli masz do czynienia z miejscem, które wytrzymuje duże obciążenie małych obiektów (np. A Pixel, 4-wektorowa, macierz 4x4, a Particlenawet Accountjeśli ma tylko kilka małych pola) to unikanie używania obiektów dla tych drobiazgów i używanie tablic (ewentualnie połączonych razem) zwykłych starych danych. Obiektów następnie stać interfejsy kolekcji jak Image, ParticleSystem, Accounts, zbiór macierzy lub wektorów itp Poszczególne te mogą być dostępne przez indeks, np Jest to również jeden z ostatecznych sztuczek projektowych w C i C ++, ponieważ nawet bez tego podstawowego napowietrznych obiektu i rozłączona pamięć, modelowanie interfejsu na poziomie pojedynczej cząstki zapobiega najbardziej wydajnym rozwiązaniom.

ChrisF
źródło
1
Biorąc pod uwagę, że zła wydajność w masie może faktycznie mieć przyzwoitą szansę na przytłaczanie wydajności szczytowej w obszarach krytycznych, nie sądzę, że można całkowicie pominąć zaletę łatwej wydajności. A sztuczka przekształcenia tablicy struktur w strukturę tablic nieco się psuje, gdy wszystkie (lub prawie wszystkie) wartości składające się na jedną z oryginalnych struktur będą dostępne jednocześnie. BTW: Widzę, że odkrywasz wiele starych postów i dodajesz własną dobrą odpowiedź, czasem nawet dobrą odpowiedź ;-)
Deduplicator
1
@Deduplicator Mam nadzieję, że nie denerwuję ludzi przez zbyt duże uderzenia! Ten był trochę tandetny - może powinienem go trochę poprawić. SoA vs. AoS jest dla mnie często trudny (dostęp sekwencyjny vs. losowy). Rzadko wiem z góry, którego powinienem użyć, ponieważ w moim przypadku często występuje połączenie dostępu sekwencyjnego i losowego. Cenną lekcją, której często się nauczyłem, jest projektowanie interfejsów, które pozostawiają wystarczająco dużo miejsca na zabawę z reprezentacją danych - trochę bardziej rozbudowane interfejsy, które mają duże algorytmy transformacji, gdy to możliwe (czasami nie jest to możliwe z przypadkowo dostępnymi tu i tam bitami).
1
Zauważyłem tylko dlatego, że rzeczy są naprawdę wolne. I nie spieszyłem się z każdym z nich.
Deduplicator
Naprawdę zastanawiam się, dlaczego user204677odszedł. To świetna odpowiedź.
oligofren
3

Pomiędzy mikrooptymalizacją, z jednej strony, a dobrym wyborem algorytmu, jest z drugiej strony.

Jest to obszar przyspieszeń o stałym współczynniku i może przynieść rzędy wielkości.
Robi to tak, że skraca całe ułamki czasu wykonania, na przykład pierwsze 30%, następnie 20% pozostałej części, następnie 50% tego i tak dalej przez kilka iteracji, aż prawie nic nie pozostanie.

Nie widać tego w małych programach w stylu demonstracyjnym. Widzisz to w dużych poważnych programach z dużą ilością klasowych struktur danych, w których stos wywołań ma zwykle wiele warstw głębokości. Dobrym sposobem na znalezienie możliwości przyspieszenia jest zbadanie losowych próbek stanu programu.

Zasadniczo przyspieszenia obejmują:

  • minimalizowanie wywołań newpoprzez łączenie i ponowne wykorzystywanie starych obiektów,

  • rozpoznawanie rzeczy, które są tam robione ze względu na ogólność, a nie są faktycznie konieczne,

  • przegląd struktury danych przy użyciu różnych klas kolekcji, które zachowują się tak samo jak duże O, ale korzystają z faktycznie używanych wzorców dostępu,

  • zapisywanie danych pozyskanych przez wywołania funkcji zamiast ponownego wywoływania funkcji (naturalną i zabawną tendencją programistów jest zakładanie, że funkcje o krótszych nazwach działają szybciej).

  • tolerowanie pewnej niespójności między redundantnymi strukturami danych, w przeciwieństwie do prób zachowania ich pełnej zgodności ze zdarzeniami powiadomień,

  • itd itd.

Ale oczywiście żadna z tych rzeczy nie powinna zostać wykonana bez uprzedniego wykazania problemów przy pobieraniu próbek.

Mike Dunlavey
źródło
2

O ile mi wiadomo, Java nie daje żadnej kontroli nad lokalizacjami zmiennych w pamięci, więc masz trudność, aby uniknąć takich rzeczy, jak fałszywe współdzielenie i wyrównanie zmiennych (możesz uzupełnić klasę kilkoma nieużywanymi elementami). Inną rzeczą, której nie sądzę, że możesz skorzystać, są instrukcje takie jakmmpause , ale te są specyficzne dla procesora, więc jeśli uważasz, że potrzebujesz, Java może nie być językiem używanym.

Istnieje klasa Niebezpieczna, która daje elastyczność w C / C ++, ale także w niebezpieczeństwie C / C ++.

Pomoże ci to spojrzeć na kod asemblera generowany przez JVM dla twojego kodu

Aby przeczytać o aplikacji Java, która analizuje ten rodzaj szczegółów, zobacz kod Disruptor wydany przez LMAX

James
źródło
2

Odpowiedź na to pytanie jest bardzo trudna, ponieważ zależy od implementacji języka.

Ogólnie rzecz biorąc, obecnie jest bardzo mało miejsca na takie „mikrooptymalizacje”. Głównym powodem jest to, że kompilatory korzystają z takich optymalizacji podczas kompilacji. Na przykład nie ma różnicy w wydajności między operatorami wstępnego i późniejszego przyrostu w sytuacjach, w których ich semantyka jest identyczna. Innym przykładem może być na przykład taka pętlafor(int i=0; i<vec.size(); i++) której można argumentować, że zamiast wywoływaćsize()funkcja elementu podczas każdej iteracji lepiej byłoby uzyskać rozmiar wektora przed pętlą, a następnie porównać z tą pojedynczą zmienną, unikając w ten sposób wywołania funkcji na iterację. Są jednak przypadki, w których kompilator wykryje ten głupi przypadek i buforuje wynik. Jest to jednak możliwe tylko wtedy, gdy funkcja nie ma skutków ubocznych, a kompilator może być pewien, że rozmiar wektora pozostaje stały podczas pętli, więc ma on zastosowanie tylko w dość trywialnych przypadkach.

zxcdw
źródło
Co do drugiego przypadku, nie sądzę, aby kompilator mógł go zoptymalizować w dającej się przewidzieć przyszłości. Wykrywanie, że optymalizacja vec.size () jest bezpieczna, zależy od udowodnienia, że ​​rozmiar wektora / utraconego nie zmienia się w pętli, co moim zdaniem jest nierozstrzygalne z powodu problemu z zatrzymaniem.
Lie Ryan
@LieRyan Widziałem wiele (prostych) przypadków, w których kompilator wygenerował dokładnie identyczny plik binarny, jeśli wynik został ręcznie „buforowany” i wywołano size (). Napisałem trochę kodu i okazuje się, że zachowanie jest wysoce zależne od sposobu działania programu. Są przypadki, w których kompilator może zagwarantować, że nie ma możliwości zmiany rozmiaru wektora podczas pętli, a potem są przypadki, w których nie może tego zagwarantować, podobnie jak problem zatrzymania, jak wspomniałeś. Na razie nie jestem w stanie zweryfikować mojego roszczenia (dezasemblacja C ++ jest uciążliwa), więc zredagowałem odpowiedź
zxcdw
2
@Lie Ryan: wiele rzeczy nierozstrzygalnych w ogólnym przypadku jest doskonale rozstrzygalnych w konkretnych, ale powszechnych przypadkach, i to naprawdę wszystko, czego potrzebujesz.
Michael Borgwardt
@LieRyan Jeśli wywołasz constmetody tylko na tym wektorze, jestem pewien, że wiele optymalizujących kompilatorów to rozwiąże.
K.Steff,
w C # i myślę, że czytam również w Javie, jeśli nie buforujesz rozmiaru pamięci podręcznej, kompilator wie, że może usunąć kontrole, aby sprawdzić, czy wykraczasz poza granice tablicy, a jeśli robisz rozmiar pamięci podręcznej, musi wykonać kontrole , które generalnie kosztują więcej niż oszczędzasz dzięki buforowaniu. Próbowanie przechytrzyć optymalizatory rzadko jest dobrym planem.
Kate Gregory,
1

ludzie mogą podać przykłady sztuczek, które można zastosować w Javie (oprócz prostych flag kompilatora).

Oprócz ulepszeń algorytmów należy wziąć pod uwagę hierarchię pamięci i sposób, w jaki procesor z niej korzysta. Zmniejszenie opóźnień w dostępie do pamięci ma duże zalety, gdy zrozumiesz, w jaki sposób dany język przydziela pamięć do swoich typów danych i obiektów.

Przykład Java, aby uzyskać dostęp do tablicy 1000 x 1000 int

Rozważ poniższy przykładowy kod - uzyskuje on dostęp do tego samego obszaru pamięci (tablica ints 1000x1000), ale w innej kolejności. Na moim komputerze Mac mini (Core i7, 2,7 GHz) moc wyjściowa jest następująca, co pokazuje, że przemierzanie tablicy o rzędy ponad dwukrotnie zwiększa wydajność (średnio ponad 100 rund każda).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Jest tak, ponieważ tablica jest przechowywana w taki sposób, że kolejne kolumny (tj. Wartości int) są umieszczane obok siebie w pamięci, podczas gdy kolejne wiersze nie są. Aby procesor rzeczywiście wykorzystał dane, muszą zostać przesłane do pamięci podręcznej. Transfer pamięci odbywa się za pomocą bloku bajtów, zwanego linią pamięci podręcznej - ładowanie linii pamięci podręcznej bezpośrednio z pamięci wprowadza opóźnienia, a tym samym zmniejsza wydajność programu.

W przypadku Core i7 (mostek piaskowy) linia bufora zawiera 64 bajty, dlatego każdy dostęp do pamięci pobiera 64 bajty. Ponieważ pierwszy test uzyskuje dostęp do pamięci w przewidywalnej sekwencji, procesor pobierze dane przed ich faktycznym wykorzystaniem przez program. Ogólnie rzecz biorąc, powoduje to mniejsze opóźnienia w dostępie do pamięci, a tym samym poprawia wydajność.

Kod próbki:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }
miraculixx
źródło
1

JVM może i często przeszkadza, a kompilator JIT może zmieniać się znacznie między wersjami Niektóre mikrooptymalizacje są niemożliwe w Javie z powodu ograniczeń językowych, takich jak przyjazna dla hiperwątkowości lub najnowsza kolekcja SIMD procesorów Intel.

Bardzo pouczający blog na ten temat od jednego z autorów Disruptor zaleca się przeczytać:

Zawsze trzeba zapytać, dlaczego warto używać Java, jeśli chcesz mikrooptymalizować, istnieje wiele alternatywnych metod przyspieszania funkcji, takich jak użycie JNA lub JNI do przekazania do biblioteki natywnej.

Steve-o
źródło