Najlepsze praktyki dotyczące ograniczania aktywności modułu wyrzucania elementów bezużytecznych w JavaScript

96

Mam dość złożoną aplikację JavaScript, która ma główną pętlę wywoływaną 60 razy na sekundę. Wydaje się, że odbywa się dużo wyrzucania elementów bezużytecznych (na podstawie danych wyjściowych „piłokształtnych” z osi czasu pamięci w narzędziach deweloperskich Chrome) - co często wpływa na wydajność aplikacji.

Więc próbuję zbadać najlepsze praktyki, aby zmniejszyć ilość pracy, którą musi wykonać odśmiecacz. (Większość informacji, które udało mi się znaleźć w sieci, dotyczy unikania wycieków pamięci, co jest nieco innym pytaniem - moja pamięć się uwalnia, po prostu dzieje się za dużo śmieci). że sprowadza się to głównie do ponownego wykorzystania obiektów w jak największym stopniu, ale oczywiście diabeł tkwi w szczegółach.

Struktura aplikacji jest podzielona na `` klasy '' zgodnie z prostym dziedzictwem JavaScript Johna Resiga .

Myślę, że jednym problemem jest to, że niektóre funkcje można wywoływać tysiące razy na sekundę (ponieważ są używane setki razy podczas każdej iteracji głównej pętli) i być może lokalne zmienne robocze w tych funkcjach (łańcuchy, tablice itp.) może być problem.

Zdaję sobie sprawę z łączenia obiektów dla większych / cięższych obiektów (i używamy tego w pewnym stopniu), ale szukam technik, które można zastosować na całej planszy, szczególnie w odniesieniu do funkcji, które są wywoływane bardzo wiele razy w ciasnych pętlach .

Jakich technik mogę użyć, aby zmniejszyć ilość pracy, którą musi wykonać odśmiecacz?

A może także - jakie techniki można zastosować, aby określić, które obiekty są najczęściej zbierane jako śmieci? (Jest to bardzo duża baza kodu, więc porównywanie migawek stosu nie było zbyt owocne)

UpTheCreek
źródło
2
Czy masz przykład swojego kodu, który mógłbyś nam pokazać? Pytanie będzie wtedy łatwiejsze do odpowiedzi (ale też potencjalnie mniej ogólne, więc nie jestem pewien)
John Dvorak
2
Co powiesz na zatrzymanie uruchamiania funkcji tysiące razy na sekundę? Czy to naprawdę jedyny sposób, aby do tego podejść? To pytanie wydaje się być problemem XY. Opisujesz X, ale naprawdę szukasz rozwiązania dla Y.
Travis J
2
@TravisJ: Uruchamia go tylko 60 razy na sekundę, co jest dość powszechną szybkością animacji. Nie prosi o mniej pracy, ale o to, jak to zrobić bardziej efektywnie przy zbieraniu śmieci.
Bergi
1
@Bergi - „niektóre funkcje można wywołać tysiące razy na sekundę”. To jest raz na milisekundę (prawdopodobnie gorzej!). To wcale nie jest powszechne. 60 razy na sekundę nie powinno być problemem. To pytanie jest zbyt niejasne i będzie rodzić tylko opinie lub domysły.
Travis J
4
@TravisJ - Nie jest to wcale rzadkie w frameworkach do gier.
UpTheCreek

Odpowiedzi:

131

Wiele rzeczy, które musisz zrobić, aby zminimalizować rezygnację z GC, jest sprzecznych z tym, co jest uważane za idiomatyczny JS w większości innych scenariuszy, więc proszę, miej na uwadze kontekst podczas oceniania rad, które daję.

Alokacja odbywa się u współczesnych tłumaczy w kilku miejscach:

  1. Podczas tworzenia obiektu za pomocą newlub za pomocą składni literału [...]lub {}.
  2. Kiedy łączysz ciągi.
  3. Po wprowadzeniu zakresu zawierającego deklaracje funkcji.
  4. Kiedy wykonujesz akcję, która wyzwala wyjątek.
  5. Podczas oceny wyrażenie funkcyjne: (function (...) { ... }).
  6. Kiedy wykonujesz operację, która wymusza na Object jak Object(myNumber)lubNumber.prototype.toString.call(42)
  7. Kiedy dzwonisz do wbudowanego oprogramowania, które robi to pod maską, na przykład Array.prototype.slice.
  8. Kiedy używasz argumentsdo refleksji nad listą parametrów.
  9. Kiedy dzielisz ciąg lub dopasowujesz go do wyrażenia regularnego.

Unikaj robienia tego, a jeśli to możliwe, łącz obiekty i wykorzystuj je ponownie.

W szczególności zwróć uwagę na możliwości:

  1. Wyciągnij funkcje wewnętrzne, które nie mają żadnych zależności od stanu zamkniętego lub mają ich niewiele, do wyższego, dłuższego zakresu. (Niektóre minifier kodu, takie jak kompilator Closure, mogą wbudować funkcje wewnętrzne i mogą poprawić wydajność GC).
  2. Unikaj używania ciągów do reprezentowania danych strukturalnych lub do dynamicznego adresowania. Szczególnie unikaj wielokrotnego analizowania przy użyciu splitlub dopasowań wyrażeń regularnych, ponieważ każdy z nich wymaga alokacji wielu obiektów. Dzieje się tak często w przypadku kluczy do tabel wyszukiwania i dynamicznych identyfikatorów węzłów DOM. Na przykład, lookupTable['foo-' + x]i document.getElementById('foo-' + x)oba wiążą alokację ponieważ istnieje powiązanie ciąg. Często można dołączyć klucze do długotrwałych obiektów zamiast ponownego łączenia. W zależności od przeglądarek, które chcesz obsługiwać, możesz mieć możliwość Mapbezpośredniego używania obiektów jako kluczy.
  3. Unikaj przechwytywania wyjątków w normalnych ścieżkach kodu. Zamiast tego try { op(x) } catch (e) { ... }zrób if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Kiedy nie możesz uniknąć tworzenia łańcuchów, np. W celu przekazania wiadomości do serwera, użyj wbudowanego narzędzia, JSON.stringifyktóre używa wewnętrznego natywnego bufora do gromadzenia zawartości zamiast przydzielania wielu obiektów.
  5. Unikaj używania wywołań zwrotnych w przypadku zdarzeń o wysokiej częstotliwości i tam, gdzie możesz, przekazywać jako wywołanie zwrotne długotrwałą funkcję (patrz 1), która odtwarza stan z treści wiadomości.
  6. Unikaj używania argumentsod funkcji, które używają, które muszą tworzyć obiekt podobny do tablicy po wywołaniu.

Zasugerowałem użycie JSON.stringifydo tworzenia wychodzących wiadomości sieciowych. Przetwarzanie komunikatów wejściowych przy użyciu JSON.parseoczywiście wiąże się z alokacją, a wiele z nich w przypadku dużych wiadomości. Jeśli możesz reprezentować wiadomości przychodzące jako tablice prymitywów, możesz zaoszczędzić wiele przydziałów. Jedyną inną wbudowaną wersją, wokół której można zbudować parser, który nie przydziela, jest String.prototype.charCodeAt. Parser dla złożonego formatu, który używa tylko tego, co będzie piekielne do czytania.

Mike Samuel
źródło
Nie sądzisz, że JSON.parseobiekty d przydzielają mniej (lub równo) miejsca niż łańcuch wiadomości?
Bergi
@Bergi, To zależy od tego, czy nazwy właściwości wymagają oddzielnych alokacji, ale parser, który generuje zdarzenia zamiast drzewa parsowania, nie ma żadnych dodatkowych przydziałów.
Mike Samuel
Fantastyczna odpowiedź, dziękuję! Wiele przeprosin za wygasającą nagrodę - podróżowałem w tym czasie iz jakiegoś powodu nie mogłem zalogować się do SO za pomocą mojego konta Gmail na moim telefonie ....: /
UpTheCreek
Aby nadrobić mój zły czas z nagrodą, dodałem dodatkową, aby ją doładować (200 to minimum, które mogłem dać;) - Z jakiegoś powodu wymaga to odczekania 24 godzin przed przyznaniem jej (chociaż Wybrałem „nagradzaj istniejącą odpowiedź”). Będzie twój jutro ...
UpTheCreek
@UpTheCreek, bez obaw. Cieszę się, że znalazłeś to przydatne.
Mike Samuel
12

Te narzędzia deweloperskie Chrome mają bardzo ładny funkcję śledzenia dla alokacji pamięci. Nazywa się to Oś czasu pamięci. W tym artykule opisano kilka szczegółów. Przypuszczam, że właśnie o tym mówisz, czy „piłokształtny”? Jest to normalne zachowanie dla większości środowisk uruchomieniowych z GC. Alokacja przebiega do momentu osiągnięcia progu użycia wyzwalającego kolekcję. Zwykle istnieją różne rodzaje kolekcji na różnych progach.

Oś czasu pamięci w Chrome

Wyrzucanie elementów bezużytecznych znajduje się na liście zdarzeń skojarzonej ze śledzeniem wraz z ich czasem trwania. Na moim raczej starym notatniku kolekcje efemeryczne pojawiają się z prędkością około 4 MB i trwają 30 ms. To są 2 z twoich iteracji pętli 60 Hz. Jeśli jest to animacja, prawdopodobnie zacinają się kolekcje 30 ms. Powinieneś zacząć tutaj, aby zobaczyć, co dzieje się w twoim środowisku: gdzie jest próg zbierania i jak długo trwa twoje zbieranie. Daje to punkt odniesienia do oceny optymalizacji. Ale prawdopodobnie nie zrobisz nic lepszego niż zmniejszenie częstotliwości jąkania poprzez spowolnienie tempa alokacji, wydłużenie odstępu między kolekcjami.

Następnym krokiem jest użycie Profiles | Funkcja Record Heap Allocations umożliwia generowanie katalogu alokacji według typu rekordu. To szybko pokaże, które typy obiektów zużywają najwięcej pamięci w okresie śledzenia, co jest równoważne szybkości alokacji. Skoncentruj się na nich w kolejności malejącej stawki.

Techniki te nie są fizyką rakietową. Unikaj przedmiotów w pudełkach, jeśli możesz to zrobić z rozpakowanym. Użyj zmiennych globalnych, aby przechowywać i ponownie używać pojedynczych obiektów, zamiast przydzielać nowe w każdej iteracji. Połącz popularne typy obiektów na bezpłatnych listach, zamiast je porzucać. Wyniki konkatenacji ciągów pamięci podręcznej, które prawdopodobnie będą ponownie używane w przyszłych iteracjach. Unikaj alokacji tylko po to, aby zwrócić wyniki funkcji, ustawiając zmienne w otaczającym zakresie. Aby znaleźć najlepszą strategię, będziesz musiał rozważyć każdy typ obiektu w jego własnym kontekście. Jeśli potrzebujesz pomocy ze szczegółami, opublikuj edycję opisującą szczegóły wyzwania, na które patrzysz.

Odradzam wypaczanie twojego normalnego stylu kodowania w całej aplikacji, próbując wyprodukować mniej śmieci. Z tego samego powodu nie powinieneś przedwcześnie optymalizować prędkości. Większość twojego wysiłku plus znaczna część dodatkowej złożoności i niejasności kodu będzie bez znaczenia.

Gen
źródło
Racja, to właśnie mam na myśli, mówiąc o zębie piły. Wiem, że zawsze będzie jakiś wzór piłokształtny, ale martwię się, że w mojej aplikacji częstotliwość piłokształtnych i „klify” są dość wysokie. Co ciekawe, imprezy GC nie pojawiają się na mojej osi czasu - tylko zdarzenia, które pojawiają się w okienku 'Records (środkowy) są: request animation frame, animation frame fired, i composite layers. Nie mam pojęcia, dlaczego nie widzę GC Eventtak, jak ty (to jest na najnowszej wersji chrome, a także canary).
UpTheCreek
4
Próbowałem użyć programu profilującego z „alokacjami sterty rekordów”, ale jak dotąd nie uważałem go za zbyt przydatny. Może to dlatego, że nie wiem, jak go właściwie używać. Wydaje się, że jest pełen odniesień, które nic dla mnie nie znaczą, jak @342342i code relocation info.
UpTheCreek,
Jeśli chodzi o „przedwczesna optymalizacja jest źródłem wszelkiego zła”: zrozum. Nie idź na ślepo. W niektórych scenariuszach, takich jak programowanie gier i multimediów, wydajność jest najważniejsza i będziesz mieć dużo „gorącego” kodu. Więc tak, będziesz musiał dostosować swój styl programowania.
warkocz
9

Zgodnie z ogólną zasadą chciałbyś przechowywać jak najwięcej pamięci podręcznej i robić jak najmniej tworzenia i niszczenia dla każdego uruchomienia pętli.

Pierwszą rzeczą, która przychodzi mi do głowy, jest ograniczenie użycia anonimowych funkcji (jeśli takie masz) w głównej pętli. Łatwo byłoby też wpaść w pułapkę tworzenia i niszczenia obiektów, które są przekazywane do innych funkcji. W żadnym wypadku nie jestem ekspertem od javascript, ale wyobrażam sobie, że to:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

działałby znacznie szybciej niż to:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

Czy Twój program ma kiedykolwiek przestoje? Może potrzebujesz, aby działał płynnie przez sekundę lub dwie (np. W przypadku animacji), a potem miał więcej czasu na przetworzenie? Jeśli tak jest, mógłbym zobaczyć, jak pobiera się obiekty, które normalnie byłyby zbierane jako śmieci przez całą animację i zachowuje odniesienie do nich w jakimś obiekcie globalnym. Następnie, gdy animacja się zakończy, możesz wyczyścić wszystkie odniesienia i pozwolić modułowi odśmiecania pamięci wykonać swoją pracę.

Przepraszam, jeśli to wszystko jest trochę trywialne w porównaniu z tym, co już próbowałeś io czym myślałeś.

Chris B.
źródło
To. Ponadto funkcje wymienione w innych funkcjach (które nie są IIFE) są również powszechnym nadużyciem, które powoduje spalanie dużej ilości pamięci i łatwo je przeoczyć.
Esailija
Dzięki Chris! Niestety nie mam żadnych przestojów: /
UpTheCreek
4

Zrobiłbym jeden lub kilka obiektów w global scope(gdzie jestem pewien, że garbage collector nie może ich dotykać), a następnie spróbuję refaktoryzować moje rozwiązanie, aby użyć tych obiektów do wykonania zadania, zamiast używać zmiennych lokalnych .

Oczywiście nie można tego zrobić wszędzie w kodzie, ale generalnie jest to mój sposób na uniknięcie garbage collectora.

PS Może to sprawić, że ta konkretna część kodu będzie nieco trudniejsza do utrzymania.

Mahdi
źródło
GC konsekwentnie usuwa moje globalne zmienne zakresu.
VectorVortec