Najlepsza praktyka tworzenia milionów małych obiektów tymczasowych

109

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:

  1. Zminimalizuj obciążenie związane z wyrzucaniem elementów bezużytecznych i
  2. 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.

Skromny programista
źródło
11
Czy wzór wagi muszej byłby odpowiedni dla twojego przypadku? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland
4
Czy potrzebujesz zamknąć go w obiekcie?
nhahtdh
1
Wzorzec Flyweight nie jest odpowiedni, ponieważ obiekty nie udostępniają istotnych wspólnych danych. Jeśli chodzi o hermetyzację danych w obiekcie, jest on zbyt duży, aby można go było spakować do prymitywu, dlatego szukam alternatyw dla POJO.
Humble Programmer
2
Gorąco polecam przeczytać: cs.virginia.edu/kim/publicity/pldi09tutorials/…
rkj

Odpowiedzi:

47

Uruchom aplikację z pełnym wyrzucaniem elementów bezużytecznych:

java -verbose:gc

I powie ci, kiedy się zbierze. Byłyby dwa rodzaje przeciągnięć, szybkie i pełne przeciągnięcie.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

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.

Niels Bech Nielsen
źródło
Poeksperymentuj z rozmiarem sterty Java, aby spróbować znaleźć punkt, w którym pełne czyszczenie pamięci jest rzadkie. W Javie 7 nowy G1 GC jest w niektórych przypadkach szybszy (w innych wolniejszy).
Michael Shops w
21

Od wersji 6 tryb serwera JVM wykorzystuje technikę analizy ucieczki . Używając go, możesz razem uniknąć GC.

Michaił
źródło
1
Analiza ucieczki często zawodzi, warto sprawdzić, czy JVM zorientował się, co robisz, czy nie.
Nitsan Wakart
2
Jeśli masz doświadczenie w korzystaniu z tej opcji: -XX: + PrintEscapeAnalysis i -XX: + PrintEliminateAllocations. Byłoby wspaniale się tym podzielić. Bo tego nie robię, mówiąc szczerze.
Michaił
zobacz stackoverflow.com/questions/9032519/ ... będziesz potrzebować kompilacji debugującej dla JDK 7, przyznaję, że tego nie zrobiłem, ale z JDK 6 to się udało.
Nitsan Wakart
19

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

for(int i=0, i<max, i++) {
  // stuff that implies i
}

Nie myślmy o rozwijaniu pętli (optymalizacjach, które JVM wykonuje w dużym stopniu w kodzie). Jeśli maxjest równe Integer.MAX_VALUE, wykonanie pętli może zająć trochę czasu. Jednak izmienna 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!

Pierre Laporte
źródło
11

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 ByteBuffersz 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 tego ByteBuffer, 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 finalpól, ponieważ wymagają one ogrodzenia pamięci na niektórych platformach (mianowicie ARM / Power), jednak na x86 jest bezpłatny.

bestsss
źródło
8

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:

  • wielowątkowość: użycie pul lokalnych wątków może działać w Twoim przypadku
  • zapasowa struktura danych: rozważ użycie ArrayDeque, ponieważ działa dobrze po usunięciu i nie ma narzutu alokacji
  • ogranicz wielkość swojego basenu :)

Zmierz przed / po itp

Nitsan Wakart
źródło
6

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.

StanislavL
źródło
To tylko z ciekawości: co ci to dało pod względem wydajności? Czy sprofilowałeś swoją aplikację przed zmianą i po niej, a jeśli tak, to jakie były tego skutki?
Axel
@Axel obiekty zajmują znacznie mniej pamięci, więc GC nie jest tak często wywoływane. Zdecydowanie sprofilowaliśmy naszą aplikację, ale był nawet wizualny efekt zwiększonej szybkości.
StanislavL
6

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.

OldCurmudgeon
źródło
2

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.

David Plumpton
źródło
1

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

rkj
źródło
Martwy link. Czy jest inne źródło tego artykułu?
dnault
0

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. :)

gyorgyabraham
źródło
1
Przepraszam stary, nie zgadzam się z twoim podejściem ... Java, jak każdy język programowania, polega na rozwiązaniu problemu w ramach swoich ograniczeń, jeśli OP jest ograniczony przez GC, w jaki sposób mu pomagasz?
Nitsan Wakart
1
Mówię mu, jak faktycznie działa Java. Jeśli nie jest w stanie uniknąć sytuacji posiadania milionów obiektów tymczasowych, najlepszą radą może być: klasa tymczasowa powinna być lekka i musi zapewnić, że zwolni odwołania tak szybko, jak to możliwe, a nie będzie już o jeden krok. Czy coś mi brakuje?
gyorgyabraham
Java obsługuje tworzenie śmieci i wyczyściłaby je dla Ciebie, to prawda. Jeśli OP nie może uniknąć tworzenia obiektów, a nie jest zadowolony z czasu spędzonego w GC, to smutne zakończenie. Mój sprzeciw dotyczy zalecenia, które dajesz, aby zrobić więcej pracy dla GC, ponieważ jest to w jakiś sposób poprawna Java.
Nitsan Wakart
0

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

luke1985
źródło
0

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.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}
Ilya Gazman
źródło
3
Używanie puli dla małych obiektów jest dość złym pomysłem, potrzebujesz puli na wątek do rozruchu (lub współdzielony dostęp obniża wydajność). Takie baseny również radzą sobie gorzej niż dobry śmieciarz. Wreszcie: GC jest darem niebios, gdy ma do czynienia z kodem / strukturami współbieżnymi - wiele algorytmów jest znacznie łatwiejszych do wdrożenia, ponieważ naturalnie nie ma problemu z ABA. Nr ref. liczenie w środowisku współbieżnym wymaga przynajmniej atomowej operacji + ogrodzenie pamięci (LOCK ADD lub CAS na x86)
bestsss
1
Zarządzające obiektów w basenie może być bardziej kosztowne niż pozwalając uruchomić garbage collector.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Generalnie zgadzam się z Tobą, ale zauważ, że wykrycie takiej różnicy jest nie lada wyzwaniem, a kiedy dojdziesz do wniosku, że GC działa lepiej w Twoim przypadku, to musi to być bardzo wyjątkowy przypadek, jeśli taka różnica ma znaczenie. Jakkolwiek na odwrót, może się zdarzyć, że pula obiektów zapisze Twoją aplikację.
Ilya Gazman
1
Po prostu nie rozumiem twojego argumentu? Bardzo trudno jest wykryć, czy GC jest szybsze niż pule obiektów? Dlatego powinieneś używać puli obiektów? JVM jest zoptymalizowana pod kątem czystego kodowania i krótkotrwałych obiektów. Jeśli o to właśnie chodzi w tym pytaniu (mam nadzieję, że jeśli OP generuje ich milion na sekundę), to powinno być tylko wtedy, gdy istnieje udowodniona zaleta, aby przejść na bardziej złożony i podatny na błędy schemat, jak ten, który sugerujesz. Jeśli jest to zbyt trudne do udowodnienia, po co się tym przejmować.
Thorbjørn Ravn Andersen
0

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.

Michael Röschter
źródło