Kod JavaScript pamięci statycznej z pulami obiektów

Wprowadzenie

Otrzymujesz więc e-maila z informacją o słabych wynikach Twojej gry internetowej lub aplikacji internetowej po upływie określonego czasu. Przeglądasz kod i nie widzisz niczego, co się wyróżnia, a następnie po otwarciu narzędzi do sprawdzania wydajności pamięci w Chrome i wyświetlaniu tych informacji:

zrzut z osi czasu wspomnień,

Jeden z współpracowników chichocze, bo uświadamia sobie, że masz problem z pamięcią.

W widoku wykresu pamięci taki kształt zęba pióra mówi o potencjalnie krytycznym problemie z wydajnością. W miarę wzrostu wykorzystania pamięci zwiększa się też obszar wykresu w zapisie osi czasu. Gdy wykres nagle spada, jest to wystąpienie, w którym moduł pamięci masowej został uruchomiony i wyczyścił odwołania do obiektów pamięci.

Co oznaczają zęby piły

Na takim wykresie widać, że występuje wiele zdarzeń związanych z zbieraniem 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.

Czyszczenie 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 alokacji i zwrócić do systemu pamięci.

Proces, w którym GC odzyskuje pamięć, nie jest darmowy. 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. Puls GC może wystąpić w dowolnym momencie podczas wykonywania kodu, co spowoduje zablokowanie wykonania kodu do momentu jego zakończenia. Czas trwania tego pulsu jest zasadniczo nieznany; jego uruchomienie zajmie trochę czasu, w zależności od tego, jak program wykorzystuje pamięć w danym momencie.

Aplikacje o wysokości wymagają stałych granic 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 liczbę rezygnacji z pamięci i podatki za odbiór śmieci

Jak już wspomniano, puls GC będzie się pojawiać, gdy zestaw heurystyki ustali, że mamy dostatecznie dużo nieaktywnych obiektów, aby impuls byłby korzystny. Dlatego kluczem do skrócenia czasu działania funkcji Garbage Collector 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ę „oczyszczaniem pamięci”. Jeśli uda Ci się zmniejszyć ilość pamięci czyszczonej podczas 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ęć.

Ten proces spowoduje przeniesienie wykresu pamięci z tego :

zrzut z osi czasu wspomnień,

na:

Statyczna pamięć JavaScript

Jak widać w tym modelu, wykres przestał mieć ząb piórowy, ale znacznie rośnie na początku, a następnie z czasem rośnie. Jeśli masz problemy z wydajnością z powodu częstego zwolnienia pamięci, utwórz taki wykres.

Przechodzenie w stronę JavaScriptu w postaci static-memory

JavaScript pamięci statycznej to technika, która obejmuje wstępne przydzielanie, na początku aplikacji, całą pamięć potrzebną do działania i zarządzanie tą pamięcią podczas wykonywania, gdy obiekty nie są już 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.

Tak naprawdę, osiągnięcie celu 1 wymaga nieco pracy nr 2, więc zaczniemy od tego.

Obiektowy

Mówiąc w prosty sposób, pooling obiektów to proces przechowywania zestawu nieużywanych obiektów, które mają ten sam typ. Gdy potrzebujesz nowego obiektu do kodu, zamiast przydzielać nowy ze sterty pamięci systemowej, odtwórz jeden z nieużywanych obiektów z puli. 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 wywoływany (czyli usuwany) z kodu, więc nie będzie wobec niego czyszczenia pamięci. Wykorzystanie pul obiektów oddaje kontrolę nad pamięcią programistom, zmniejszając wpływ śmietnika na wydajność.

Aplikacja obsługuje zróżnicowany zestaw typów obiektów, dlatego prawidłowe korzystanie z pul obiektów wymaga posiadania 1 puli na typ, który ma wysoki współczynnik rotacji 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ś mieć już wystarczającą wiedzę na temat tego, jaka jest górna granica, i możesz wstępnie przydzielić tę liczbę obiektów na początku aplikacji.

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 dobrze poznać wymagania dotyczące pamięci, a potem skatalogować te dane i je przeanalizować, aby poznać górne limity wymagań dotyczących pamięci w przypadku Twojej aplikacji.

Następnie w udostępnianej wersji aplikacji możesz ustawić etap inicjowania, aby wstępnie wypełnić wszystkie pule obiektów do określonej kwoty. 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 maksymalnego rozmiaru może zmniejszyć zapotrzebowanie na pamięć w przypadku użytkowników o standardowych potrzebach.

Daleko od wymarzonej kuli

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

Jedną z powodów, dla których JavaScript jest idealnym językiem w internecie, jest to, że jest szybki, zabawny i łatwy na początek. 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. Jednak w przypadku wysoko wydajnych aplikacji internetowych, takich jak gry HTML5, GC często nie potrzebuje bardzo ważnej liczby klatek, co zmniejsza komfort korzystania z urządzenia. Dzięki starannemu pomiarowi i użyciu puli obiektów możesz zmniejszyć obciążenie procesora, które powoduje zmniejszenie 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 omówię poszczególne kwestie związane z implementacją, co jest ważne, ponieważ każde użycie aplikacji może mieć własne potrzeby związane z implementacją.

Pliki referencyjne