Wprowadzenie
Otrzymujesz e-maila z informacją, że Twoja gra przeglądarkowa lub aplikacja internetowa działa słabo po upływie określonego czasu. Przeglądasz kod, ale nie widzisz niczego, co by się wyróżniało, dopóki nie otworzysz narzędzi do sprawdzania wydajności pamięci w Chrome i nie zobaczysz tego:
Jeden z Twoich współpracowników uśmiecha się, ponieważ zdaje sobie sprawę, że masz problem z wydajnością pamięci.
W widoku wykresu pamięci ten wzór przypominający zęby piły bardzo wyraźnie wskazuje na potencjalnie krytyczny problem z wydajnością. W miarę wzrostu wykorzystania pamięci obszar wykresu na osi czasu również będzie się powiększał. Gdy wykres nagle spada, oznacza to, że uruchomiono moduł odzyskiwania pamięci i wyczyszczono obiekty pamięci, do których się odwołujesz.
Na takim wykresie widać, że występuje wiele zdarzeń związanych z odzyskiwaniem pamięci, co może negatywnie wpływać na wydajność aplikacji internetowych. Z tego artykułu dowiesz się, jak kontrolować wykorzystanie pamięci, aby zmniejszyć wpływ na wydajność.
Czyszczenie pamięci i koszty wydajności
Model pamięci JavaScriptu jest oparty na technologii znanej jako Garbage Collector. W wielu językach programista jest bezpośrednio odpowiedzialny za przydzielanie i zwalnianie pamięci ze sterty pamięci systemu. System odśmiecania pamięci zarządza jednak tym zadaniem w imieniu programisty, co oznacza, że obiekty nie są bezpośrednio usuwane z pamięci, gdy programista je wyrejestruje, ale później, gdy heurystyka odśmiecania pamięci uzna, że byłoby to korzystne. Ten proces decyzyjny wymaga, aby odśmiecanie pamięci przeprowadziło analizę statystyczną aktywnych i nieaktywnych obiektów, co zajmuje trochę czasu.
Odzyskiwanie pamięci jest często przedstawiane jako przeciwieństwo ręcznego zarządzania pamięcią, które wymaga od programisty określenia, które obiekty mają zostać cofnięte i zwrócone do systemu pamięci.
Proces, w którym GC odzyskuje pamięć, nie odbywa się bezpłatnie. Zwykle zmniejsza dostępną wydajność, ponieważ wymaga czasu na wykonanie pracy. Oprócz tego system sam decyduje, kiedy go uruchomić. Nie masz kontroli nad tym działaniem. Impuls GC może wystąpić w dowolnym momencie wykonywania kodu, co spowoduje zablokowanie wykonywania kodu do momentu zakończenia działania. Czas trwania tego impulsu jest zwykle nieznany i zależy od tego, jak program wykorzystuje pamięć w danym momencie.
Aplikacje o wysokiej wydajności wymagają spójnych granic wydajności, aby zapewnić użytkownikom płynne działanie. Systemy odśmiecania pamięci mogą utrudniać osiągnięcie tego celu, ponieważ mogą działać w losowych momentach i przez losowy czas, co skraca czas dostępny dla aplikacji na osiągnięcie celów skuteczności.
Zmniejsz fragmentację pamięci i ogranicz obciążenie związane z odzyskiwaniem pamięci.
Jak wspomniano, impuls GC wystąpi, gdy zestaw heurystyk określi, że jest wystarczająco dużo nieaktywnych obiektów, aby impuls był korzystny. Dlatego kluczem do skrócenia czasu, jaki moduł odzyskiwania pamięci poświęca na aplikację, jest wyeliminowanie jak największej liczby przypadków nadmiernego tworzenia i zwalniania obiektów. Ten proces częstego tworzenia i zwalniania obiektów nazywa się „fragmentacją pamięci”. Jeśli uda Ci się ograniczyć fragmentację pamięci w czasie działania aplikacji, skrócisz też czas, jaki GC poświęca na wykonywanie operacji. Oznacza to, że musisz usunąć lub zmniejszyć liczbę tworzonych i niszczonych obiektów, czyli przestać przydzielać pamięć.
Ten proces spowoduje przeniesienie wykresu pamięci z :
na:
W tym modelu wykres nie ma już kształtu piły, ale na początku gwałtownie rośnie, a potem powoli się zwiększa. Jeśli masz problemy z wydajnością z powodu fragmentacji pamięci, utwórz ten typ wykresu.
Przejście na JavaScript z pamięcią statyczną
Statyczna pamięć JavaScript to technika polegająca na wstępnym przydzielaniu na początku działania aplikacji całej pamięci, która będzie potrzebna przez cały okres jej działania, i zarządzaniu tą pamięcią podczas wykonywania, gdy obiekty nie są już potrzebne. Możemy osiągnąć ten cel w kilku prostych krokach:
- Zaimplementuj w aplikacji mechanizmy, które pozwolą określić maksymalną liczbę wymaganych obiektów pamięci aktywnej (według typu) w różnych scenariuszach użycia.
- Zaimplementuj ponownie kod, aby wstępnie przydzielić tę maksymalną ilość, a następnie ręcznie pobieraj i zwalniaj zasoby, zamiast przechodzić do pamięci głównej.
W rzeczywistości osiągnięcie celu 1 wymaga od nas wykonania części działań z celu 2, więc zacznijmy od tego.
Pula obiektów
Mówiąc prościej, pula obiektów to proces zachowywania zestawu nieużywanych obiektów tego samego typu. Gdy potrzebujesz nowego obiektu dla kodu, zamiast przydzielać nowy obiekt ze sterty pamięci systemu, możesz ponownie wykorzystać jeden z nieużywanych obiektów z puli. Gdy kod zewnętrzny zakończy pracę z obiektem, nie zwalnia go do pamięci głównej, ale zwraca do puli. Ponieważ obiekt nigdy nie jest wyrejestrowywany (czyli usuwany) z kodu, nie zostanie usunięty przez moduł czyszczenia pamięci. Wykorzystanie pul obiektów pozwala programiście odzyskać kontrolę nad pamięcią, co zmniejsza wpływ modułu odśmiecania pamięci na wydajność.
Ponieważ aplikacja obsługuje heterogeniczny zestaw typów obiektów, prawidłowe korzystanie z pul obiektów wymaga posiadania jednej puli na typ, który charakteryzuje się wysokim współczynnikiem rezygnacji podczas działania aplikacji.
var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};
//..... do some stuff with the object that we need to do
gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference
W przypadku większości aplikacji w pewnym momencie nastąpi stabilizacja pod względem konieczności przydzielania nowych obiektów. Po kilku uruchomieniach aplikacji powinno być możliwe określenie tej górnej granicy i wstępne przydzielenie tej liczby obiektów na początku działania aplikacji.
Wstępne przydzielanie obiektów
Zaimplementowanie puli obiektów w projekcie pozwoli Ci uzyskać teoretyczną maksymalną liczbę obiektów wymaganych podczas działania aplikacji. Po przetestowaniu witryny w różnych scenariuszach testowych możesz uzyskać dobry obraz wymagań dotyczących pamięci, które będą potrzebne, i możesz skatalogować te dane w jakimś miejscu i przeanalizować je, aby zrozumieć, jakie są górne limity wymagań dotyczących pamięci dla Twojej aplikacji.
Następnie w wersji aplikacji przeznaczonej do dystrybucji możesz ustawić fazę inicjowania, aby wstępnie wypełnić wszystkie pule obiektów do określonej ilości. Dzięki temu cała inicjalizacja obiektów zostanie przeniesiona na początek aplikacji, co zmniejszy liczbę alokacji, które występują dynamicznie podczas jej wykonywania.
function init() {
//preallocate all our pools.
//Note that we keep each pool homogeneous wrt object types
gEntityObjectPool.preAllocate(256);
gDomObjectPool.preAllocate(888);
}
Wybrana kwota ma duży wpływ na zachowanie aplikacji. Czasami teoretyczne maksimum nie jest najlepszą opcją. Na przykład wybranie średniej maksymalnej może skutkować mniejszym wykorzystaniem pamięci w przypadku użytkowników, którzy nie są doświadczeni.
Daleko od idealnego rozwiązania
Istnieje cała klasa aplikacji, w których statyczne wzorce wzrostu pamięci mogą być korzystne. Jednak jak zauważa Renato Mangini, inny członek zespołu Chrome DevRel, to rozwiązanie ma kilka wad.
Podsumowanie
Jednym z powodów, dla których JavaScript jest idealny do tworzenia stron internetowych, jest to, że jest to szybki, przyjemny i łatwy język, od którego można zacząć. Wynika to głównie z niskiej bariery ograniczeń składniowych i obsługi problemów z pamięcią w Twoim imieniu. Możesz skupić się na kodowaniu, a Gemini zajmie się resztą. Jednak w przypadku aplikacji internetowych o wysokiej wydajności, takich jak gry HTML5, GC może często obniżać liczbę klatek, co pogarsza wrażenia użytkownika. Dzięki starannej instrumentacji i wdrożeniu pul obiektów możesz zmniejszyć obciążenie liczby klatek i odzyskać czas na ciekawsze rzeczy.
Kod źródłowy
W internecie można znaleźć wiele implementacji pul obiektów, więc nie będę Cię zanudzać kolejną. Zamiast tego skieruję Cię do tych artykułów, z których każdy zawiera szczegółowe informacje o implementacji. Jest to ważne, ponieważ każde zastosowanie aplikacji może mieć określone potrzeby związane z implementacją.
- Pula obiektów Gamecore.js
- Beej’s Object Pools
- Emehrkay’s super simple object pool
- Pula obiektów Stevena Lamberta skupiona na grach
- Konfiguracja objectPool w RenderEngine