Statyczna pamięć JavaScript z obiektowymi zbiornikami pamięci

Wprowadzenie

Otrzymujesz e-maila z informacją, że po pewnym czasie Twoja gra internetowa lub aplikacja internetowa działa źle. Przeglądasz kod, ale nie widzisz niczego niepokojącego. Okazuje się jednak, że problem leży w narzędziach do zarządzania pamięcią w Chrome:

zrzut z osi czasu wspomnień,

Jeden z współpracowników się śmieje, bo zdaje sobie sprawę, że masz problem z wydajnością związany z pamięcią.

W widoku wykresu pamięci ten ząbkowany wzór wyraźnie wskazuje na potencjalny poważny problem z wydajnością. W miarę wzrostu wykorzystania pamięci zwiększa się też obszar wykresu w zapisie na osi czasu. Gdy wykres nagle spada, oznacza to, że działał już wcześniej i oczyszczał odwołujące się do niego obiekty pamięci.

Co oznaczają zęby piły

Na takim wykresie widać, że występuje dużo zdarzeń związanych z usuwaniem pamięci podręcznej, co może negatywnie wpływać na wydajność aplikacji internetowych. Z tego artykułu dowiesz się, jak przejąć kontrolę nad wykorzystaniem pamięci, aby zminimalizować wpływ na wydajność.

Koszty sprzątania i wydajności

Model pamięci JavaScripta jest oparty na technologii zwanej zbieraczem śmieci. W wielu językach programista jest bezpośrednio odpowiedzialny za przydzielanie i zwalnianie pamięci z puli pamięci systemu. System Garbage Collector zarządza jednak tym zadaniem w imieniu programisty, co oznacza, że obiekty nie są bezpośrednio uwalniane z pamięci, gdy programista je odwołuje, ale dopiero później, gdy heurystyka GC uzna, że jest to korzystne. Ten proces decyzyjny wymaga, aby system GC wykonał pewną analizę statystyczną aktywnych i nieaktywnych obiektów, co zajmuje pewien czas.

Zbiórnik zbędniętych danych jest często przedstawiany jako przeciwieństwo ręcznego zarządzania pamięcią, które wymaga od programisty określenia, które obiekty mają zostać zwolnione i zwrócone do systemu pamięci.

Proces, w którym GC odzyskuje pamięć, nie jest bezpłatny. Zwykle zmniejsza dostępną wydajność, ponieważ zajmuje pewien czas. Poza tym system sam podejmuje decyzję, kiedy go uruchomić. Nie masz kontroli nad tym działaniem. Impuls GC może wystąpić w dowolnym momencie podczas wykonywania kodu, co spowoduje zablokowanie jego dalszego wykonywania do czasu jego zakończenia. Czas trwania tego impulsu jest zazwyczaj nieznany; jego wykonanie zajmie pewien czas, w zależności od tego, jak program wykorzystuje pamięć w danym momencie.

Aplikacje o wysokiej wydajności muszą mieć stałe granice wydajności, aby zapewnić użytkownikom płynne działanie. Systemy zarządzania pamięcią mogą zakłócać realizację tego celu, ponieważ mogą działać losowo w losowych odstępach czasu, co skraca czas, w którym aplikacja może osiągnąć swoje cele dotyczące wydajności.

Zmniejsz rotację pamięci i obniż opłaty za usuwanie elementów z pamięci podręcznej

Jak już wspomnieliśmy, impuls GC nastąpi, gdy zestaw heurystyk określi, że istnieje wystarczająca liczba nieaktywnych obiektów, dla których impuls byłby przydatny. Dlatego kluczem do zmniejszenia czasu, jaki zbieracz śmieci potrzebuje na działanie w aplikacji, 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ę „przekształcaniem pamięci”. Jeśli uda Ci się zmniejszyć przekształcanie pamięci w trakcie działania aplikacji, skrócisz też czas wykonywania GC. Oznacza to, że musisz usunąć lub zmniejszyć liczbę utworzonych i zniszczonych obiektów, a tak naprawdę przestać przydzielać pamięć.

W ramach tego procesu wykres pamięci zostanie przeniesiony z tego miejsca :

zrzut z osi czasu wspomnień,

na:

Statyczna pamięć JavaScript

W tym modelu widać, że wykres nie ma już kształtu piły, ale rośnie znacznie na początku, a potem powoli wzrasta w czasie. Jeśli masz problemy z wydajnością z powodu częstego zapełniania się pamięci, utwórz taki wykres.

Przejście na kod JavaScript z pamięcią statyczną

Statyczna pamięć JavaScript to technika, która polega na przedwczesnym przydzielaniu na początku aplikacji całej pamięci, która będzie potrzebna przez cały jej czas działania, oraz na zarządzaniu tą pamięcią podczas wykonywania, gdy obiekty nie są już potrzebne. Aby to osiągnąć, wykonaj te kilka prostych kroków:

  1. Przeprowadź instrumentację aplikacji, aby określić maksymalną liczbę wymaganych obiektów pamięci na żywo (według typu) w różnych scenariuszach użycia.
  2. Ponownie zaimplementuj kod, aby wstępnie przydzielić tę maksymalną kwotę, a następnie ręcznie pobrać lub zwolnić pamięć, zamiast korzystać z pamięci głównej.

W rzeczywistości osiągnięcie pierwszego celu wymaga trochę pracy nad drugim, więc zacznijmy od tego.

Obiektowy

Mówiąc w prosty sposób, pooling obiektów to proces przechowywania zestawu nieużywanych obiektów tego samego typu. Gdy potrzebujesz nowego obiektu do kodu, zamiast przydzielić nowy z systemowego pniazu pamięci, możesz wykorzystać jeden z nieużywanych obiektów z pulu. Gdy kod zewnętrzny skończy pracę z obiektem, zamiast zwracać go do pamięci głównej, zwraca go do puli. Obiekt nigdy nie jest odwoływany (czyli usuwany) z kodu, więc nie będzie usuwany przez mechanizm garbage collection. Korzystanie z pul obiektów oddaje kontrolę nad pamięcią z powrotem w ręce programisty, co zmniejsza wpływ zbieracza pamięci na wydajność.

Aplikacja obsługuje zróżnicowany zestaw typów obiektów, dlatego prawidłowe korzystanie z pul obiektów wymaga utworzenia 1 puli na typ, który charakteryzuje się dużą rotacją w czasie 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 końcu dochodzi do pewnego poziomu, w którym nie trzeba już przydzielać nowych obiektów. Po kilku uruchomieniach aplikacji powinieneś już dobrze znać ten górny limit i na początku aplikacji możesz wstępnie przydzielić tę liczbę obiektów.

Wstępna alokacja obiektów

Wdrożenie w projekcie puli obiektów zapewni teoretycznie maksymalną liczbę obiektów wymaganych w czasie działania aplikacji. Po przetestowaniu witryny w różnych scenariuszach możesz uzyskać dobre wyobrażenie o potrzebach dotyczących pamięci. Możesz też skatalogować te dane i je analizować, aby poznać górne limity wymagań pamięci dla aplikacji.

Następnie w wersji aplikacji przeznaczonej do wdrożenia możesz ustawić fazę inicjowania, aby wstępnie wypełnić wszystkie zbiory obiektów do określonej liczby. Spowoduje to przesunięcie całej inicjalizacji obiektów na początek aplikacji i zmniejszenie liczby przydziałów, 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 przez Ciebie kwota ma duży wpływ na działanie aplikacji. Czasami teoretyczny maksymalny limit nie jest najlepszym rozwiązaniem. Na przykład wybranie średniego maksimum może zmniejszyć zużycie pamięci w przypadku użytkowników, którzy nie są zaawansowani.

Nie ma cudownego rozwiązania

Istnieje cała klasyfikacja aplikacji, w których przypadku statyczne wzorce wzrostu pamięci mogą być korzystne. Jak jednak zauważa Renato Mangini, jeden z mnie współpracowników z zespołu ds. rozwoju społeczności Chrome, ma on kilka wad.

Podsumowanie

Jednym z powodów, dla których JavaScript jest idealnym językiem do tworzenia stron internetowych, jest to, że jest to szybki, przyjemny i łatwy język do nauki. Wynika to głównie z niskich wymagań dotyczących ograniczeń składni i rozwiązywania problemów z pamięcią. Możesz pisać kod i zostawić brudną robotę narzędziu. W przypadku aplikacji internetowych o wysokiej wydajności, takich jak gry w HTML5, GC może jednak często zużywać niezbędną liczbę klatek, co pogarsza wrażenia użytkownika. Dzięki starannemu pomiarowi i wprowadzeniu puli obiektów możesz zmniejszyć obciążenie procesora, które powoduje spadek liczby klatek na sekundę, i zaoszczędzić czas na bardziej interesujące zadania.

Kod źródłowy

W sieci jest wiele implementacji zbiorników obiektów, więc nie będę Cię nudzić kolejną. Zamiast tego podam Ci te, które mają specyficzne niuanse implementacji. Jest to ważne, ponieważ każda aplikacja może wymagać specyficznej implementacji.

Odniesienia