Jakie są „sprawdzone metody” tworzenia (i zwalniania) milionów małych obiektów?
Piszę program szachowy w Javie, a algorytm wyszukiwania generuje pojedynczy obiekt „Move” dla każdego możliwego ruchu, a wyszukiwanie nominalne może łatwo wygenerować ponad milion obiektów ruchu na sekundę. JVM GC poradził sobie z obciążeniem mojego systemu programistycznego, ale jestem zainteresowany zbadaniem alternatywnych podejść, które:
- Zminimalizuj obciążenie związane z wyrzucaniem elementów bezużytecznych i
- zmniejszyć zużycie pamięci szczytowej dla systemów niższej klasy.
Zdecydowana większość obiektów jest bardzo krótkotrwała, ale około 1% wygenerowanych ruchów jest utrwalanych i zwracanych jako trwała wartość, więc każda technika buforowania lub buforowania musiałaby zapewniać możliwość wykluczenia określonych obiektów z ponownego wykorzystania .
Nie oczekuję w pełni rozwiniętego przykładowego kodu, ale byłbym wdzięczny za sugestie dotyczące dalszego czytania / badania lub przykłady open source o podobnym charakterze.
źródło
Odpowiedzi:
Uruchom aplikację z pełnym wyrzucaniem elementów bezużytecznych:
I powie ci, kiedy się zbierze. Byłyby dwa rodzaje przeciągnięć, szybkie i pełne przeciągnięcie.
Strzałka wskazuje rozmiar przed i po.
Dopóki robi tylko GC, a nie pełny GC, jesteś bezpieczny w domu. Zwykły GC jest kolekcjonerem kopii w „młodym pokoleniu”, więc obiekty, do których nie ma już odniesień, są po prostu zapomniane, co jest dokładnie tym, czego byś chciał.
Czytanie Java SE 6 HotSpot Virtual Machine Garbage Collection Tuning jest prawdopodobnie pomocne.
źródło
Od wersji 6 tryb serwera JVM wykorzystuje technikę analizy ucieczki . Używając go, możesz razem uniknąć GC.
źródło
Cóż, w jednym jest kilka pytań!
1 - Jak zarządza się obiektami krótkotrwałymi?
Jak wspomniano wcześniej, JVM doskonale radzi sobie z ogromną ilością obiektów krótkotrwałych, ponieważ opiera się na hipotezie słabego pokolenia .
Zwróć uwagę, że mówimy o obiektach, które dotarły do pamięci głównej (sterty). Nie zawsze tak jest. Wiele tworzonych obiektów nie opuszcza nawet rejestru procesora. Na przykład rozważmy tę pętlę for
Nie myślmy o rozwijaniu pętli (optymalizacjach, które JVM wykonuje w dużym stopniu w kodzie). Jeśli
max
jest równeInteger.MAX_VALUE
, wykonanie pętli może zająć trochę czasu. Jednaki
zmienna nigdy nie ucieknie z bloku pętli. Dlatego JVM umieści tę zmienną w rejestrze procesora, regularnie ją zwiększając, ale nigdy nie wyśle jej z powrotem do pamięci głównej.Zatem tworzenie milionów obiektów nie jest wielkim problemem, jeśli są używane tylko lokalnie. Będą martwe, zanim zostaną przechowane w Edenie, więc GC nawet ich nie zauważy.
2 - Czy warto zmniejszyć narzut GC?
Jak zwykle to zależy.
Najpierw należy włączyć rejestrowanie GC, aby mieć jasny obraz tego, co się dzieje. Możesz to włączyć za pomocą
-Xloggc:gc.log -XX:+PrintGCDetails
.Jeśli Twoja aplikacja spędza dużo czasu w cyklu GC, to tak, dostrój GC, w przeciwnym razie może to nie być tego warte.
Na przykład, jeśli masz młody GC co 100 ms, który zajmuje 10 ms, spędzasz 10% swojego czasu w GC i masz 10 kolekcji na sekundę (co jest huuuuug). W takim przypadku nie spędzałbym czasu na strojeniu GC, ponieważ te 10 GC / s nadal by tam były.
3 - Trochę doświadczenia
Podobny problem miałem na aplikacji, która tworzyła ogromną ilość danej klasy. W logach GC zauważyłem, że szybkość tworzenia aplikacji wynosiła około 3 GB / s, czyli zdecydowanie za dużo (no dalej ... 3 gigabajty danych na sekundę?!).
Problem: zbyt wiele częstych GC spowodowanych tworzeniem zbyt wielu obiektów.
W moim przypadku założyłem profiler pamięci i zauważyłem, że klasa reprezentowała ogromny procent wszystkich moich obiektów. Prześledziłem instancje, aby dowiedzieć się, że ta klasa była w zasadzie parą wartości logicznych opakowanych w obiekt. W takim przypadku dostępne były dwa rozwiązania:
Przerób algorytm, aby nie zwracać pary wartości logicznych, ale zamiast tego mam dwie metody, które zwracają każdą wartość logiczną osobno
Buforuj obiekty, wiedząc, że były tylko 4 różne instancje
Wybrałem drugą, ponieważ miała najmniejszy wpływ na aplikację i była łatwa do wprowadzenia. Umieszczenie fabryki z niezabezpieczoną wątkowo pamięcią podręczną zajęło mi kilka minut (nie potrzebowałem zabezpieczenia wątków, ponieważ ostatecznie miałbym tylko 4 różne instancje).
Współczynnik alokacji spadł do 1 GB / s, podobnie jak częstotliwość młodych GC (podzielona przez 3).
Mam nadzieję, że to pomoże!
źródło
Jeśli masz tylko obiekty wartości (to znaczy nie ma odniesień do innych obiektów) i naprawdę, ale mam na myśli naprawdę tony i tony z nich, możesz użyć bezpośrednio
ByteBuffers
z natywnym porządkiem bajtów [to drugie jest ważne] i potrzebujesz kilkuset wierszy kod do alokacji / ponownego użycia + getter / setters. Gettery wyglądają podobnie dolong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}
To rozwiązałoby problem GC prawie całkowicie, o ile przydzielasz tylko raz, to znaczy ogromną porcję, a następnie sam zarządzasz obiektami. Zamiast odwołań miałbyś tylko indeks (to znaczy
int
) do tegoByteBuffer
, co trzeba przekazać. Być może będziesz musiał wyrównać pamięć.Technika wydawałaby się być używana
C and void*
, ale przy pewnym zawijaniu jest do zniesienia. Wadą wydajności może być sprawdzanie ograniczeń, czy kompilator nie może go wyeliminować. Główną zaletą jest lokalność, jeśli przetwarzasz krotki jak wektory, brak nagłówka obiektu również zmniejsza zużycie pamięci.Poza tym prawdopodobnie nie potrzebujesz takiego podejścia, ponieważ młode pokolenie praktycznie wszystkich maszyn JVM umiera w trywialny sposób, a koszt alokacji to tylko wskazówka. Koszt alokacji może być nieco wyższy, jeśli używasz
final
pól, ponieważ wymagają one ogrodzenia pamięci na niektórych platformach (mianowicie ARM / Power), jednak na x86 jest bezpłatny.źródło
Zakładając, że uznasz, że GC jest problemem (jak inni wskazują, że może nim nie być), zaimplementujesz własne zarządzanie pamięcią dla swojego specjalnego przypadku, tj. Klasy, która cierpi z powodu masowej rezygnacji. Spróbuj puli obiektów, widziałem przypadki, w których działa to całkiem dobrze. Wdrażanie pul obiektów to dobrze wydeptana ścieżka, więc nie ma potrzeby ponownego odwiedzania tego miejsca, zwróć uwagę na:
Zmierz przed / po itp
źródło
Spotkałem się z podobnym problemem. Przede wszystkim spróbuj zmniejszyć rozmiar małych obiektów. Wprowadziliśmy kilka domyślnych wartości pól odnoszących się do nich w każdej instancji obiektu.
Na przykład MouseEvent ma odwołanie do klasy Point. Buforowaliśmy Punkty i odwoływaliśmy się do nich zamiast tworzyć nowe instancje. To samo dotyczy na przykład pustych ciągów.
Innym źródłem było wiele wartości logicznych, które zostały zastąpione przez jeden int, a dla każdego z nich używamy tylko jednego bajtu int.
źródło
Miałem do czynienia z tym scenariuszem jakiś czas temu z kodem przetwarzania XML. Odkryłem, że tworzę miliony obiektów znaczników XML, które były bardzo małe (zwykle tylko ciąg znaków) i wyjątkowo krótkotrwałe (niepowodzenie sprawdzenia XPath oznaczało brak dopasowania, więc odrzucenie).
Przeprowadziłem kilka poważnych testów i doszedłem do wniosku, że mogę osiągnąć tylko około 7% poprawę szybkości, używając listy odrzuconych tagów zamiast tworzyć nowe. Jednak po wdrożeniu stwierdziłem, że wolna kolejka wymagała dodania mechanizmu do jej przycinania, jeśli stała się zbyt duża - to całkowicie unieważniło moją optymalizację, więc przełączyłem ją na opcję.
Podsumowując - prawdopodobnie nie warto - ale cieszę się, że o tym myślisz, pokazuje, że Ci zależy.
źródło
Biorąc pod uwagę, że piszesz program szachowy, istnieje kilka specjalnych technik, których możesz użyć, aby uzyskać przyzwoite wyniki. Jednym prostym podejściem jest utworzenie dużej tablicy długości (lub bajtów) i traktowanie jej jako stosu. Za każdym razem, gdy twój generator ruchów tworzy ruchy, umieszcza kilka liczb na stosie, np. Przesuń się z kwadratu i przejdź do kwadratu. Oceniając drzewo wyszukiwania, będziesz usuwać ruchy i aktualizować reprezentację tablicy.
Jeśli chcesz wyrazistej mocy, użyj przedmiotów. Jeśli chcesz, aby szybkość (w tym przypadku) była natywna.
źródło
Jednym z rozwiązań, których użyłem do takich algorytmów wyszukiwania, jest utworzenie tylko jednego obiektu Move, zmutowanie go za pomocą nowego ruchu, a następnie cofnięcie ruchu przed opuszczeniem zakresu. Prawdopodobnie analizujesz tylko jeden ruch na raz, a następnie po prostu przechowujesz gdzieś najlepszy ruch.
Jeśli z jakiegoś powodu jest to niewykonalne i chcesz zmniejszyć maksymalne wykorzystanie pamięci, dobry artykuł o wydajności pamięci jest tutaj: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf
źródło
Po prostu stwórz miliony obiektów i napisz swój kod we właściwy sposób: nie przechowuj niepotrzebnych odniesień do tych obiektów. GC wykona za ciebie brudną robotę. Możesz pobawić się z pełną GC, jak wspomniano, aby sprawdzić, czy naprawdę są one GC. W Javie chodzi o tworzenie i zwalnianie obiektów. :)
źródło
Myślę, że powinieneś przeczytać o alokacji stosu w Javie i analizie ucieczki.
Ponieważ jeśli zagłębisz się w ten temat, może się okazać, że Twoje obiekty nie są nawet przydzielane na stercie i nie są zbierane przez GC w taki sam sposób, jak obiekty na stercie.
Istnieje wikipedia wyjaśnienie analizy ucieczki, z przykładem tego, jak to działa w Javie:
http://en.wikipedia.org/wiki/Escape_analysis
źródło
Nie jestem wielkim fanem GC, więc zawsze staram się to obejść. W tym przypadku sugerowałbym użycie wzorca Object Pool :
Chodzi o to, aby uniknąć tworzenia nowych obiektów przez przechowywanie ich w stosie, aby można było ich później użyć ponownie.
źródło
Pule obiektów zapewniają olbrzymią (czasami 10x) poprawę w stosunku do alokacji obiektów na stercie. Ale powyższa implementacja przy użyciu połączonej listy jest zarówno naiwna, jak i błędna! Połączona lista tworzy obiekty do zarządzania jej wewnętrzną strukturą, niwelując wysiłek. Bufor pierścieniowy korzystający z tablicy obiektów działa dobrze. W przykładzie daj (program szachowy zarządzający ruchami) Ringbuffer powinien być zawinięty w obiekt holdera dla listy wszystkich obliczonych ruchów. Wówczas przekazywane byłyby tylko odniesienia do obiektu uchwytu ruchu.
źródło