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

95

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

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

Приложение структурировано по «классам» в соответствии с «Простым наследованием JavaScript» Джона Ресига .

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

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

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

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

UpTheCreek
источник
2
У вас есть пример кода, который вы можете нам показать? Тогда будет легче ответить на вопрос (но также потенциально менее общий, поэтому я не уверен)
Джон Дворжак
2
Как насчет остановки выполнения функций тысячи раз в секунду? Неужели это единственный способ подойти к этому? Этот вопрос кажется проблемой XY. Вы описываете X, но на самом деле ищете решение Y.
Travis J
2
@TravisJ: Он запускает его только 60 раз в секунду, что является довольно распространенной скоростью анимации. Он не просит делать меньше работы, а просит сделать ее более эффективной при сборке мусора.
Bergi
1
@Bergi - «некоторые функции могут вызываться тысячи раз в секунду». Это один раз в миллисекунду (возможно, хуже!). Это совсем не обычное явление. 60 раз в секунду не должно быть проблемой. Этот вопрос слишком расплывчатый и вызывает только мнения или предположения.
Travis J
4
@TravisJ - Это совсем не редкость в игровых фреймворках.
UpTheCreek

Ответы:

128

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

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

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

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

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

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

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

Майк Сэмюэл
источник
Вам не кажется, что JSON.parsed-объекты занимают меньше (или равно) места, чем строка сообщения?
Берги
@Bergi, Это зависит от того, требуют ли имена свойств отдельного выделения, но парсер, который генерирует события вместо дерева разбора, не делает посторонних выделений.
Майк Сэмюэл
Фантастический ответ, спасибо! Приносим извинения за истечение срока награды - я был в поездке в то время, и по какой-то причине я не мог войти в SO с моей учетной записью gmail на моем телефоне ....: /
UpTheCreek
Чтобы компенсировать неудачное время с наградой, я добавил еще одну, чтобы пополнить ее (200 - это минимум, который я мог дать;) - Хотя по какой-то причине мне нужно подождать 24 часа, прежде чем я награду ее (хотя Я выбрал «вознаградить существующий ответ»). Завтра будет твоим ...
UpTheCreek
@UpTheCreek, не беспокойтесь. Я рад, что ты нашел это полезным.
Майк Сэмюэл
12

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

Хронология памяти в Chrome

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

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

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

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

Ген
источник
Правильно, это то, что я имею в виду под пилой. Я знаю, что всегда будет какая-то пилообразная диаграмма, но меня беспокоит то, что с моим приложением частота пилообразной формы и «обрывы» довольно высоки. Интересно, что GC события не показывают на мой график - только события , которые появляются на панели «записей» (в середине) являются: request animation frame, animation frame fired, и composite layers. Понятия не имею, почему я не вижу, GC Eventкак вы (это последняя версия Chrome, а также канарейка).
UpTheCreek
4
Я пробовал использовать профилировщик с «распределением кучи записей», но пока не нашел его очень полезным. Возможно, это потому, что я не знаю, как им правильно пользоваться. Кажется, он полон ссылок, которые для меня ничего не значат, например, @342342и code relocation info.
UpTheCreek
По поводу «преждевременная оптимизация - корень всех зол»: поймите. Не следуйте слепо. В определенных сценариях, таких как программирование игр и мультимедиа, производительность имеет первостепенное значение, и у вас будет много «горячего» кода. Так что да, вам придется изменить свой стиль программирования.
snarf
9

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

Первое, что приходит мне в голову, - это уменьшить использование анонимных функций (если они у вас есть) внутри вашего основного цикла. Также было бы легко попасть в ловушку создания и уничтожения объектов, которые передаются другим функциям. Я ни в коем случае не эксперт по 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
    });
}

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

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

Крис Б.
источник
Это. Кроме того, функции, упомянутые в других функциях (которые не являются IIFE), также являются распространенным злоупотреблением, которое сжигает много памяти и его легко пропустить.
Esailija
Спасибо, Крис! К сожалению, у меня нет простоев: /
UpTheCreek
4

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

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

PS Это может сделать эту конкретную часть кода немного менее удобной в обслуживании.

Махди
источник
GC последовательно извлекает мои глобальные переменные области видимости.
VectorVortec