Najlepsze praktyki ograniczania aktywności Garbage Collector w Javascript

Mam dość złożoną aplikację Javascript, która ma pętlę główną, która jest wywoływana 60 razy na sekundę. Wydaje się, że dzieje się wiele śmieci (w oparciu o wyjście "sawtooth" z osi czasu pamięci w narzędziach deweloperskich Chrome) - a to często wpływa na wydajność aplikacji.

Więc, staram się zbadać najlepsze praktyki w celu zmniejszenia ilości pracy, że Śmieciarz ma do zrobienia. (Większość informacji, które udało mi się znaleźć w sieci unikanie wycieków pamięci, co jest nieco innym pytaniem - moja pamięć się uwalnia, chodzi tylko o to, że za dużo śmieci się dzieje.) Zakładam, że sprowadza się to głównie do ponownego wykorzystania obiektów w jak największym stopniu, ale oczywiście diabeł tkwi w szczegółach.

Aplikacja jest skonstruowana w "klasach" wzdłuż linii proste dziedziczenie JavaScript Johna Resiga .

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

Jestem świadomy łączenia obiektów dla większych / cięższych obiektów( i używamy tego do pewnego stopnia), ale szukam technik, które mogą być stosowane na całym świecie, 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 to musi zrobić Śmieciarz?

A być może także - jakie techniki można zastosować, aby zidentyfikować, które przedmioty są najczęściej zbierane? (Jest to bardzo duża baza kodowa, więc porównywanie migawek sterty nie było zbyt owocne)

Author: UpTheCreek, 2013-08-21

4 answers

Wiele rzeczy, które musisz zrobić, aby zminimalizować odpływ GC, jest sprzeczne z tym, co jest uważane za idiomatyczne JS w większości innych scenariuszy, więc proszę pamiętać o kontekście oceniając porady, które udzielam.

Alokacja odbywa się we współczesnych interpretatorach w kilku miejscach:

  1. podczas tworzenia obiektu przez new lub przez składnię literalną [...] lub {}.
  2. kiedy łączysz struny.
  3. po wprowadzeniu zakresu zawierającego deklaracje funkcji.
  4. kiedy wykonujesz akcję, która uruchamia wyjątek.
  5. gdy oceniasz wyrażenie funkcji: (function (...) { ... }).
  6. gdy wykonujesz operację, która zmusza do obiektu jak Object(myNumber) lub Number.prototype.toString.call(42)
  7. kiedy zadzwonisz do budynku, który robi to pod maską, jak Array.prototype.slice.
  8. gdy używasz arguments do odzwierciedlenia listy parametrów.
  9. gdy dzielisz łańcuch lub dopasowujesz Wyrażenie regularne.

Unikaj tego, i łącz i używaj obiektów, gdzie możliwe.

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

  1. wyciągnij funkcje wewnętrzne, które nie mają lub mają kilka zależności od stanu zamkniętego, do wyższego, dłuższego zakresu. (Niektóre minifiery kodu, takie jak Closure compiler mogą wbudować funkcje wewnętrzne i mogą poprawić wydajność GC.)
  2. Unikaj używania ciągów znaków do reprezentowania danych strukturalnych lub do adresowania dynamicznego. Szczególnie unikaj wielokrotnego parsowania za pomocą split lub dopasowania wyrażeń regularnych, ponieważ każdy z nich wymaga wielu przydziałów obiektów. Często zdarza się to 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) dotyczą alokacji, ponieważ istnieje konkatenacja łańcuchowa. Często można dołączać Klucze do długotrwałych obiektów zamiast ponownego łączenia. W zależności od przeglądarek, które musisz obsługiwać, możesz używać Map aby używać obiektów jako kluczy bezpośrednio.
  3. unikaj wyłapywania WYJĄTKÓW na zwykłych ścieżkach kodu. Zamiast try { op(x) } catch (e) { ... }, do if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Jeśli nie możesz uniknąć tworzenia łańcuchów, np. aby przekazać wiadomość do serwera, użyj wbudowanego JSON.stringify, który używa wewnętrznego bufora natywnego do gromadzenia zawartości zamiast przydzielania wielu obiektów.
  5. Unikaj używania wywołań zwrotnych dla zdarzeń o wysokiej częstotliwości i tam, gdzie możesz, przekazuj jako wywołanie zwrotne długotrwałą funkcję (Patrz 1), która odtwarza stan z treści wiadomości.
  6. Unikaj używania arguments, ponieważ funkcje, które używają, muszą tworzyć tablicę podobną do obiekt po wywołaniu.

Zasugerowałem użycie JSON.stringify do tworzenia wychodzących wiadomości sieciowych. Parsowanie wiadomości wejściowych za pomocą JSON.parse oczywiście wiąże się z alokacją, a wiele z nich dla dużych wiadomości. Jeśli możesz reprezentować przychodzące wiadomości jako tablice prymitywów, możesz zapisać wiele przydziałów. Jedynym wbudowanym elementem, wokół którego można zbudować parser, który nie przydziela, jest String.prototype.charCodeAt. Parser dla złożonego formatu, który używa tylko, że będzie piekielnie czytać chociaż.

 100
Author: Mike Samuel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-23 22:33:34

Narzędzia programistyczne Chrome mają bardzo ładną funkcję śledzenia alokacji pamięci. To się nazywa oś czasu pamięci. Ten artykuł opisuje kilka szczegółów. Przypuszczam, że to jest to, o czym mówisz "sawtooth"? Jest to normalne zachowanie dla większości uruchomień GC ' ed. Alokacja odbywa się do momentu osiągnięcia progu wykorzystania, co powoduje uruchomienie kolekcji. Zwykle istnieją różne rodzaje zbiorów w różnych progach.

Timeline pamięci w Chrome

Zbiórki śmieci są zawarte na liście zdarzeń związanych ze śladem wraz z ich czasem trwania. Na moim dość starym notebooku zbiory efemeryczne występują przy około 4Mb i zajmują 30ms. to są 2 iteracje pętli 60Hz. Jeśli jest to animacja, Kolekcje 30ms prawdopodobnie powodują jąkanie się. Powinieneś zacząć od tego, co dzieje się w Twoim środowisku: gdzie znajduje się próg kolekcji i jak długo trwa kolekcja. Daje to punkt odniesienia do oceny optymalizacji. Ale ty prawdopodobnie nie będzie lepiej niż zmniejszenie częstotliwości jąkania przez spowolnienie szybkości alokacji, wydłużenie odstępu między zbiorami.

Następnym krokiem jest użycie funkcji profile | Record Heap Allocations do wygenerowania katalogu przydziałów 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. Skup się na nich w kolejności malejącej.

Techniki nie są naukami rakietowymi. Unikaj pudełkowych obiektów, gdy możesz zrobić z nie pudełkowym. Używaj zmiennych globalnych do przechowywania i ponownego wykorzystywania pojedynczych obiektów pudełkowych zamiast przydzielania nowych w każdej iteracji. Grupowanie typowych obiektów na wolnych listach zamiast ich porzucania. Wyniki konkatenacji łańcuchów pamięci podręcznej, które mogą być wielokrotnego użytku w przyszłych iteracjach. Unikaj alokacji tylko po to, aby zwracać wyniki funkcji, ustawiając zmienne w otaczającym zakresie. Będziesz musiał wziąć pod uwagę każdy typ obiektu w jego własny kontekst, aby znaleźć najlepszą strategię. Jeśli potrzebujesz pomocy w szczegółach, Opublikuj edycję opisującą szczegóły wyzwania, na które patrzysz.

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

 11
Author: Gene,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-30 19:38:56

Ogólnie rzecz biorąc, chcesz buforować jak najwięcej i robić jak najmniej tworzenia i niszczenia dla każdego uruchomienia pętli.

Pierwszą rzeczą, która pojawia się w mojej głowie, jest ograniczenie korzystania z funkcji anonimowych (jeśli masz takie) wewnątrz głównej pętli. Łatwo też wpaść w pułapkę tworzenia i niszczenia przedmiotów, które są przekazywane do innych funkcji. W żadnym wypadku nie jestem ekspertem w 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);
}

Będzie działać znacznie szybciej niż to:

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

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

Czy kiedykolwiek był jakiś przestój dla Twojego programu? Może potrzebujesz go, aby działał płynnie przez sekundę lub dwie (np. dla animacji), a potem ma więcej czasu na przetworzenie? W takim przypadku mogłem zobaczyć, jak obiekty, które normalnie byłyby śmieciami zbieranymi w całej animacji, zachowują odniesienie do nich w jakimś globalnym obiekcie. Po zakończeniu animacji możesz wyczyścić wszystkie odniesienia i pozwolić, aby garbage collector wykonał swoją pracę.

Sorry if this is all a bit trywialne w porównaniu z tym, co już próbowałeś i myślałeś.

 9
Author: Chris B,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-21 18:23:39

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óbowałbym refaktorować 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 ogólnie to mój sposób na uniknięcie garbage collector.

P. S. Może to sprawić, że ta konkretna część kodu będzie trochę mniej łatwa do utrzymania.

 5
Author: Mahdi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-23 18:41:30