JavaScript с использованием статической памяти и пулов объектов

Введение

Представьте себе следующую ситуацию: вы получаете имейл с жалобой на то, что ваша браузерная игра или приложение после некоторого отрезка времени перестают нормально работать. Вы долго копаетесь в коде, не находите ничего из ряда вон выходящего, пока не открываете инструмент мониторинга памяти в Chrome и видите следующую картину:

диаграмма1

Один из ваших коллег уже тихо посмеивается, предвкушая, как вы будете решать проблемы производительности, связанные с памятью приложения.

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

диаграмма2

На этой диаграмме чётко видно, насколько часто происходит очистка памяти. Этот процесс не может не сказываться на производительности веб-приложения. В данной статье мы поговорим о том, как можно управлять использованием памяти, уменьшая негативное воздействие на производительность приложения.

Сборка мусора и влияние этого процесса на производительность приложения

В основу Модели памяти в JavaScript заложена технология, известная под названием сборка мусора. Многие языки возлагают прямую ответственность за распределение и освобождение памяти приложения из динамической памяти системы на программиста. Система сборки мусора выполняет эту задачу вместо программиста, освобождая память от объектов не сразу же после того, как программист удаляет ссылку на них, а немного позже, когда сборщик мусора путем эвристического анализа определяет наиболее подходящий для этого момент. Для выбора такого момента требуется проведение статистического анализа активных и неактивных объектов, занимающее определённое количество времени.

Сборка мусора в информатике представляет собой одну из форм автоматического управления памятью. Сборщик мусора осуществляет попытку очистить мусор, то есть память, занятую объектами, которые больше не используются программой.

Процесс сборки мусора часто противопоставляют ручному управлению памятью, при котором программист уточняет, какие именно объекты следует открепить и вернуть в системную память.

Процесс восстановления памяти сборщиком мусора не является «бесплатным» для приложения, для него обычно используется часть доступной производительности, и на его выполнение затрачивается определённое количество времени. Кроме того, решение о необходимости запуска данного процесса принимает сама система. Это действие вы не можете контролировать. Запуск сборщика мусора может произойти в любой момент во время выполнения кода, что приведёт к его блокировке до завершения работы мусорщика. Длительность работы сборщика мусора, как правило, нельзя предсказать. Она зависит от того, как используется память в данный момент.

Для обеспечения бесперебойного взаимодействия с пользователями в высокопроизводительных приложениях накладывается ограничение последовательной работы. Системы сборки мусора могут нарушить бесперебойность работы приложения, так как они запускаются в произвольный момент и длятся произвольное количество времени, «воруя» ресурсы, которые необходимы приложению для достижения нужных показателей производительности.

Сокращение псевдо-утечек памяти, уменьшение нагрузки от сборки мусора

Как упоминалось ранее, запуск сборщика мусора происходит после того, как с помощью набора эвристических алгоритмов системой определяется наличие достаточного количества неактивных объектов и принимается решение о целесообразности запуска этого процесса. По сути, ключевой принцип сокращения времени, отбираемого сборщиком мусора у приложения, состоит в сведении к минимуму создания и удаления временных объектов. Процесс частого создания и удаления объектов называют «псевдо-утечкой памяти». Если есть хоть малейшая возможность сократить псевдо-утечку памяти при эксплуатации приложения, параллельно и время, затрачиваемое на работу сборщика мусора. Это значит,что необходимо отказаться от создания и уничтожения объектов или же свести их использование к минимуму. По существу, вам нужно прекратить распределение памяти. Это даст вам вместо такой диаграммы использования памяти:

диаграмма3

следующую картину:

диаграмма4

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

Переход к JavaScript на основе статической памяти

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

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

На практике перед тем как приступить к шагу №1, нужно частично выполнить шаг №2, потому с него и начнём.

Пул объектов

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

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

  1. Объем памяти, требуемой для пула объектов, растёт по мере увеличения количества используемых объектов.
  2. Количество объектов, создаваемых и удаляемых в конкретный отрезок времени сокращается до минимума, требуемого для приложения.

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

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... выполнение действий с объектом

gEntityObjectPool.free(newEntity); //освобождение объекта после окончания действий
newEntity = null; //удаление ссылки на этот объект

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

Предварительное определение объектов

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

Затем в коммерческой версии приложения можно запрограммировать предварительное наполнение указанного количества пулов объектов на этапе инициализации. Это позволит перенести инициализацию объектов полностью на начальную стадию работы приложения и сократить необходимость в их динамическом назначении во время эксплуатации приложения.

function init() {
  //предварительное назначение всех пулов.
  //обратите внимание, что в каждом пуле хранятся однотипные объекты
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

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

Далеко не панацея

Существует целая классификация приложений, для которых подход статического увеличения памяти может оказаться выигрышным. Однако, как отметил мой коллега из проекта по разработке Chrome Ренато Мангини (Renato Mangini), существует несколько подводных камней.

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

Заключение

Одна из причин того, что JavaScript идеально подходит для веб-разработки, состоит в том,что этот язык быстрый, интересный и простой в работе. Этим он в основном обязан низкому барьеру для синтаксических ограничений и возможности управлять памятью вместо вас. Вы можете сосредоточиться на написании кода и оставить всю грязную работу на него. Однако в случае с высокопроизводительными веб-приложениями, вроде игр на HTML5, сборщик мусора может «съесть» критически важную кадровую частоту с негативными последствиями для опыта конечного пользователя. Уделив должное внимание мониторингу и внедрив пулы объектов, можно снять это бремя с с системы и использовать её улучшенное быстродействие для чего-то поинтересней.

Исходный код

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

Дополнительные ссылки

Логотип компании «Одноклассники»

Статья переведена благодаря спонсорской поддержке компании «Одноклассники».