Введение
Итак, вы получаете электронное письмо о том, что ваша браузерная игра или веб-приложение работает плохо после определенного количества времени. Вы копаетесь в коде, не видите ничего, что выделяется, пока не откроете инструменты производительности памяти Chrome и не увидите следующее:
Один из ваших коллег усмехается, потому что понимает, что у вас проблема с производительностью, связанная с памятью.
На графике памяти этот пилообразный паттерн очень красноречиво говорит о потенциально критической проблеме с производительностью. По мере увеличения использования памяти будет расти и область диаграммы на временной шкале. Резкие падения на графике означают, что сборщик мусора удалил объекты, на которые ссылается память.
На таком графике можно увидеть, что происходит много событий сборки мусора, которые могут негативно повлиять на производительность веб-приложений. В этой статье рассказывается, как контролировать использование памяти и снизить его влияние на производительность.
Сборка мусора и снижение производительности
Модель памяти JavaScript основана на технологии, которая называется сборщиком мусора. Во многих языках программист напрямую отвечает за выделение и освобождение памяти из кучи. Однако система сборки мусора управляет этой задачей от имени программиста, то есть объекты не освобождаются из памяти напрямую, когда программист разыменовывает их, а позже, когда эвристика сборщика мусора решает, что это было бы полезно. Этот процесс принятия решений требует, чтобы сборщик мусора выполнил некоторый статистический анализ активных и неактивных объектов, на выполнение которого требуется блок времени.
Сборку мусора часто противопоставляют ручному управлению памятью, при котором программист должен сам указывать, какие объекты нужно удалить и вернуть в систему памяти.
Процесс, в котором сборщик мусора освобождает память, не является бесплатным, он обычно сокращает доступную производительность, занимая блок времени для выполнения своей работы; наряду с этим сама система принимает решение о том, когда запускать. Вы не можете контролировать это действие. Импульс GC может произойти в любой момент во время выполнения кода, что заблокирует выполнение кода до его завершения. Продолжительность этого импульса обычно неизвестна, и на его выполнение потребуется некоторое время, в зависимости от того, как ваша программа использует память в любой момент времени.
Высокопроизводительные приложения должны работать стабильно, чтобы пользователям было удобно с ними взаимодействовать. Системы сборки мусора могут помешать достижению этой цели, поскольку они могут работать в случайное время и в течение случайной продолжительности, сокращая доступное время, необходимое приложению для достижения целей по эффективности.
Уменьшите перемешивание памяти и нагрузку на сборщик мусора
Как отмечалось выше, импульс GC возникает, когда набор эвристик определяет, что неактивных объектов достаточно для того, чтобы импульс был полезен. Поэтому, чтобы сократить время, которое сборщик мусора тратит на ваше приложение, нужно по возможности избегать создания и удаления лишних объектов. Процесс частого создания и удаления объектов называется перемешиванием памяти. Если вы сможете уменьшить перемешивание памяти во время работы приложения, то сократите время, которое сборщик мусора тратит на выполнение. Это означает, что вам нужно удалить или уменьшить количество созданных и уничтоженных объектов, то есть прекратить выделение памяти.
В результате график использования памяти изменится с такого :
на это:
На графике видно, что пилообразный паттерн исчез. Вместо этого в начале наблюдается значительный рост, а затем – медленное увеличение. Если у вас возникли проблемы с производительностью из-за перемешивания памяти, создайте такой график.
Переход к JavaScript со статической памятью
Статическая память JavaScript – это метод, который включает предварительное выделение в начале работы приложения всей памяти, которая потребуется в течение его жизненного цикла, и управление этой памятью во время выполнения, когда объекты больше не нужны. Для этого нужно выполнить несколько простых шагов:
- Определите максимальное количество объектов в оперативной памяти (по типам) для разных сценариев использования.
- Перепишите код так, чтобы заранее выделять максимальный объем памяти, а затем вручную запрашивать и освобождать ее, не обращаясь к основной памяти.
На самом деле, чтобы выполнить пункт 1, нам нужно немного поработать над пунктом 2, поэтому давайте начнем с него.
Пул объектов
Простыми словами, пул объектов – это набор неиспользуемых объектов одного типа. Когда вам нужен новый объект для кода, вместо того чтобы выделять новый из системной кучи памяти, вы повторно используете один из неиспользуемых объектов из пула. Когда внешний код завершает работу с объектом, он не освобождает его в основную память, а возвращает в пул. Поскольку объект никогда не разыменовывается (то есть не удаляется) из кода, он не будет удален сборщиком мусора. Использование пулов объектов позволяет программисту контролировать память и снижает влияние сборщика мусора на производительность.
Поскольку приложение поддерживает разнородный набор типов объектов, для правильного использования пулов объектов требуется один пул на каждый тип, который испытывает высокую текучесть во время работы приложения.
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
В большинстве приложений вы в конечном итоге достигнете некоторого уровня, когда вам потребуется выделять новые объекты. Запустив приложение несколько раз, вы сможете определить это максимальное значение и предварительно выделить память для такого количества объектов при запуске приложения.
Предварительное выделение объектов
Реализация пула объектов в вашем проекте даст вам теоретический максимум для количества объектов, необходимых во время выполнения вашего приложения. Протестировав сайт в различных сценариях, вы сможете понять, сколько памяти ему требуется, зафиксировать эти данные и проанализировать их, чтобы определить верхние пределы требований к памяти для вашего приложения.
Затем в версии приложения, предназначенной для распространения, вы можете настроить этап инициализации, чтобы заранее заполнить все пулы объектов до указанного количества. Это позволит перенести инициализацию всех объектов в начало приложения и сократить количество динамических выделений памяти во время его работы.
function init() {
//preallocate all our pools.
//Note that we keep each pool homogeneous wrt object types
gEntityObjectPool.preAllocate(256);
gDomObjectPool.preAllocate(888);
}
Выбранное значение во многом зависит от поведения приложения. Иногда теоретический максимум – не лучший вариант. Например, если выбрать среднее максимальное значение, то для обычных пользователей объем потребляемой памяти может быть меньше.
Не панацея
Существует целый класс приложений, в которых статический рост памяти может быть преимуществом. Однако, как отмечает Ренато Манджини (Renato Mangini), у этого подхода есть несколько недостатков.
Заключение
Одна из причин, по которой JavaScript идеально подходит для веб-разработки, заключается в том, что это быстрый, интересный и простой язык, с которого легко начать. В основном это связано с низким барьером для синтаксических ограничений и обработкой проблем с памятью от вашего имени. Вы можете писать код, а он будет выполнять за вас рутинную работу. Однако в случае с высокопроизводительными веб-приложениями, такими как игры на HTML5, сборщик мусора может снижать частоту кадров, что негативно сказывается на удобстве использования. Тщательно настроив инструменты и внедрив пулы объектов, вы сможете снизить нагрузку на частоту кадров и освободить время для более важных задач.
Исходный код
В интернете можно найти множество реализаций пулов объектов, поэтому я не буду утомлять вас ещё одной. Вместо этого я направлю вас к этим ресурсам, каждый из которых содержит конкретные нюансы реализации. Это важно, учитывая, что каждое использование приложения может иметь определенные потребности в реализации.
- Пул объектов Gamecore.js
- Beej’s Object Pools
- Emehrkay’s super simple object pool
- Пул объектов для игр от Стивена Ламберта
- Настройка objectPool в RenderEngine