Рекомендации по сокращению активности коллектора мусора в Javascript

У меня довольно сложное приложение Javascript, у которого есть основной цикл, который называется 60 раз в секунду. Кажется, что происходит сбор мусора (основанный на "пилообразном" выходе из временной шкалы памяти в инструментах Chrome dev), и это часто влияет на производительность приложения.

Итак, я пытаюсь исследовать лучшие практики для сокращения объема работы, которую должен сделать сборщик мусора. (Большая часть информации, которую я смог найти в Интернете, что позволяет избежать утечек памяти, что представляет собой немного другой вопрос - моя память освобождается, просто потому, что происходит слишком много сборок мусора.) Я предполагаю что это в основном сводится к повторному использованию объектов как можно больше, но, конечно, дьявол находится в деталях.

Приложение структурировано в 'классах' в соответствии с John Resig Простым наследованием JavaScript.

Я думаю, что одна проблема заключается в том, что некоторые функции можно назвать тысячи раз в секунду (поскольку они используются сотни раз в течение каждой итерации основного цикла) и, возможно, локальные рабочие переменные в этих функциях (строки, массивы, и т.д.) может быть проблемой.

Я знаю об объединении объектов для более крупных/тяжелых объектов (и мы это в некоторой степени), но я ищу методы, которые можно применять по всем направлениям, особенно связанные с функциями, которые называются очень много раз в плотных петлях.

Какие методы я могу использовать для уменьшения объема работы, которую должен делать сборщик мусора?

И, возможно, также - какие методы можно использовать для определения того, какие объекты больше всего собирают мусор? (Это очень большая база кода, поэтому сравнение снимков кучи не очень плодотворно)

Ответ 1

Многие из вещей, которые вам нужно сделать, чтобы свести к минимуму GC churn, идут вразрез с тем, что считается идиоматическим JS в большинстве других сценариев, поэтому, пожалуйста, имейте в виду контекст, когда судите совет, который я даю.

Распределение происходит в современных интерпретаторах в нескольких местах:

  • Когда вы создаете объект через new или через литеральный синтаксис [...] или {}.
  • Когда вы объединяете строки.
  • Когда вы вводите область, содержащую объявления функций.
  • При выполнении действия, которое вызывает исключение.
  • Когда вы оцениваете выражение функции: (function (...) { ... }).
  • Когда вы выполняете операцию, которая привязывается к объекту как Object(myNumber) или Number.prototype.toString.call(42)
  • Когда вы вызываете встроенный, который делает любой из них под капотом, например Array.prototype.slice.
  • Когда вы используете arguments для отображения списка параметров.
  • Когда вы разбиваете строку или сопоставляете ее с регулярным выражением.

Избегайте делать это, а также пул и повторное использование объектов там, где это возможно.

В частности, обратите внимание на возможности:

  • Вытащить внутренние функции, которые не имеют ни малейших зависимостей от состояния с закрытым состоянием, в более высокий, более продолжительный срок. (Некоторый код minifiers как Closure compiler может встроить внутренние функции и может улучшить производительность вашего GC.)
  • Избегайте использования строк для представления структурированных данных или для динамической адресации. Особенно избегайте многократного разбора с использованием split или совпадений регулярных выражений, так как для каждого требуется множественное распределение объектов. Это часто происходит с ключами в таблицы поиска и динамическими идентификаторами DOM node. Например, lookupTable['foo-' + x] и document.getElementById('foo-' + x) оба связаны с распределением, так как существует конкатенация строк. Часто вы можете прикреплять ключи к долгоживущим объектам вместо повторного конкатенации. В зависимости от браузеров, которые необходимо поддерживать, вы можете использовать Map для непосредственного использования объектов в качестве ключей.
  • Избегайте перехватывать исключения на обычных кодах. Вместо try { op(x) } catch (e) { ... }, do if (!opCouldFailOn(x)) { op(x); } else { ... }.
  • Если вы не можете избежать создания строк, например. для передачи сообщения серверу используйте встроенную функцию JSON.stringify, которая использует внутренний собственный буфер для накопления содержимого вместо выделения нескольких объектов.
  • Избегайте использования обратных вызовов для высокочастотных событий и где вы можете передать в качестве обратного вызова долгоживущую функцию (см. 1), которая воссоздает состояние из содержимого сообщения.
  • Избегайте использования arguments, поскольку функции, которые используют, должны создавать объект, похожий на массив, при вызове.

Я предложил использовать JSON.stringify для создания исходящих сетевых сообщений. Разбор входных сообщений с использованием JSON.parse, очевидно, включает в себя выделение, и много для больших сообщений. Если вы можете представлять свои входящие сообщения в виде массивов примитивов, вы можете сэкономить много ассигнований. Единственное другое встроенное средство, вокруг которого вы можете построить парсер, который не выделяет, - String.prototype.charCodeAt. Парсер для сложного формата, который использует только это, будет адским, чтобы читать.

Ответ 2

Как общий принцип, вы должны кэшировать как можно больше и делать как можно меньше создания и уничтожения для каждого запуска вашего цикла.

Первое, что появляется в моей голове, - это уменьшить использование анонимных функций (если они есть) внутри основного цикла. Также было бы легко попасть в ловушку создания и уничтожения объектов, передаваемых в другие функции. Я отнюдь не эксперт по javascript, но я бы предположил, что это:

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

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

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

будет работать намного быстрее, чем это:

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

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

Есть ли время простоя вашей программы? Может быть, вам нужно, чтобы он работал плавно на секунду или два (например, для анимации), а затем у него больше времени для обработки? Если это так, я мог бы видеть, что объекты, которые обычно собираются в мусор, собираются во всей анимации и сохраняют ссылку на них в каком-то глобальном объекте. Затем, когда анимация заканчивается, вы можете очистить все ссылки и позволить сборщику мусора работать.

Извините, если все это немного тривиально по сравнению с тем, что вы уже пробовали и думали.

Ответ 3

У инструментов разработчика Chrome есть очень приятная функция для отслеживания выделения памяти. Он называется временной шкалой памяти. В этой статье описаны некоторые детали. Полагаю, это то, о чем вы говорите о "пилообразном"? Это нормальное поведение для большинства сеансов GC'ed. Распределение продолжается до тех пор, пока не будет достигнут порог использования, вызывающий сбор. Обычно существуют разные типы коллекций с разными порогами.

Memory Timeline in Chrome

Сбор мусора включается в список событий, связанных с трассировкой, вместе с их продолжительностью. На моем довольно старом ноутбуке эфемерные коллекции происходят примерно в 4 Мб и занимают 30 мс. Это 2 из ваших итераций цикла 60 Гц. Если это анимация, коллекция 30 мс, вероятно, вызывает заикание. Вы должны начать здесь, чтобы узнать, что происходит в вашей среде: где установлен порог сбора данных и как долго ваши коллекции берутся. Это дает вам ориентир для оценки оптимизации. Но вы, вероятно, не будете лучше, чем уменьшать частоту заикания, замедляя скорость распределения, удлиняя интервал между коллекциями.

Следующим шагом будет использование профилей | Record Heap Allocations, чтобы генерировать каталог распределений по типу записи. Это быстро покажет, какие типы объектов потребляют большую часть памяти в течение периода трассировки, что эквивалентно скорости распределения. Сосредоточьтесь на них в порядке убывания скорости.

Методы - это не наука о ракетах. Избегайте объектов в коробке, когда вы можете сделать это с помощью распакованного. Используйте глобальные переменные для хранения и повторного использования отдельных объектов в коробке, а не выделения свежих в каждой итерации. Объединяйте общие типы объектов в свободных списках, а не отказывайтесь от них. Результаты конкатенации строки кэша, которые могут быть повторно использованы в будущих итерациях. Избегайте распределения, чтобы возвращать результаты функции, вместо этого вместо этого устанавливая переменные в закрывающей области. Вам нужно будет рассмотреть каждый тип объекта в своем собственном контексте, чтобы найти лучшую стратегию. Если вам нужна помощь со спецификой, опубликуйте описание, описывающее детали проблемы, которую вы ищете.

Я рекомендую не изменять ваш обычный стиль кодирования во всем приложении в попытке дробовика производить меньше мусора. Это по той же причине, что вы не должны оптимизировать скорость преждевременно. Большая часть ваших усилий плюс большая часть дополнительной сложности и неясности кода будет бессмысленной.

Ответ 4

Я сделал бы один или несколько объектов в global scope (где я уверен, что сборщик мусора не может дотронуться до них), тогда я попытаюсь реорганизовать мое решение, чтобы использовать эти объекты, чтобы выполнить работу, вместо использования локальных переменных.

Конечно, этого не может быть сделано везде в коде, но, как правило, я стараюсь избегать сборщика мусора.

P.S. Это может сделать эту часть кода немного менее удобной.