Объектные пулы в высокопроизводительном JavaScript? - PullRequest
42 голосов
/ 07 декабря 2011

Я пишу некоторый код JavaScript, который должен работать быстро и использует много недолговечных объектов. Мне лучше использовать пул объектов или просто создавать объекты так, как они мне нужны?

Я написал тест JSPerf , который предполагает, что использование пула объектов бесполезно, однако я не уверен, что тесты jsperf выполняются достаточно долго для запуска сборщика мусора в браузере. 1005 *

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

Ответы [ 5 ]

49 голосов
/ 20 апреля 2014

Позвольте мне начать с высказывания: я бы советовал не использовать пулы, если вы не разрабатываете визуализации, игры или другой вычислительно дорогой код, который на самом деле выполняет много работы. Ваше среднее веб-приложение привязано к вводу / выводу, а ваш ЦП и ОЗУ будут простаивать большую часть времени. В этом случае вы получаете гораздо больше, оптимизируя скорость ввода-вывода, а не скорость выполнения; Т.е. убедитесь, что ваши файлы загружаются быстро, и вы используете клиентскую сторону, а не серверную визуализацию + шаблонирование. Однако, если вы увлекаетесь играми, научными вычислениями или другим связанным с процессором кодом Javascript, эта статья может быть вам интересна.

Короткая версия :

В коде, критичном к производительности:

  1. Начните с использования оптимизаций общего назначения [1] [2] [3] [4] (и многое другое). Не прыгайте в лужи сразу (вы понимаете, о чем я!).
  2. Будьте осторожны с синтаксическим сахаром и внешними библиотеками, так как даже Обещания и многие встроенные модули (такие как Array.concat и т. Д.) Делают много злых вещей под капотом , включая отчисления.
  3. Избегайте неизменных (например, String), поскольку они будут создавать новые объекты во время операций по изменению состояния, которые вы над ними выполняете.
  4. Знайте свои ассигнования. Используйте инкапсуляцию для создания объектов, чтобы вы могли легко найти все распределения и быстро изменить свою стратегию распределения во время профилирования.
  5. Если вы беспокоитесь о производительности, всегда профилируйте и сравнивайте разные подходы. В идеале, вы не должны случайно верить кому-то на intarwebz (включая меня). Помните, что наши определения слов, таких как «быстрый», «долгоживущий» и т. Д., Могут значительно отличаться.
  6. Если вы решили использовать пул:
    • Возможно, вам придется использовать разные пулы для долгоживущих и недолговечных объектов, чтобы избежать фрагментации недолговечного пула.
    • Вы хотите сравнить разные алгоритмы и разную степень детализации пула (объединить целые объекты или только некоторые свойства объекта?) Для разных сценариев.
    • Объединение в пул увеличивает сложность кода и, следовательно, усложняет работу оптимизатора, потенциально снижая производительность.

Длинная версия :

Во-первых, учтите, что куча системы, по сути, такая же, как пул больших объектов. Это означает, что всякий раз, когда вы создаете новый объект (используя new, [], {}, (), вложенные функции , конкатенацию строк и т. Д.), Система будет использовать (очень сложный, быстрый и низкоуровневый алгоритм, который дает вам некоторое неиспользуемое пространство (то есть объект), гарантирует, что его байты обнуляются и возвращают его. Это очень похоже на то, что должен делать пул объектов. Однако менеджер динамической памяти Javascript использует GC для извлечения «заимствованных объектов», когда пул возвращает свои объекты практически с нулевой стоимостью, но требует, чтобы разработчик сам позаботился об отслеживании всех таких объектов.

Современные среды выполнения Javascript, такие как V8, имеют профилировщик времени выполнения и оптимизатор времени выполнения, которые в идеале могут (но не обязательно (пока)) агрессивно оптимизировать, когда он определяет участки кода, критичные для производительности. Он также может использовать эту информацию, чтобы определить время для сбора мусора. Если он понимает, что вы запускаете игровой цикл, он может просто запускать GC после каждых нескольких циклов (возможно, даже уменьшить коллекцию старого поколения до минимума и т. Д.), Тем самым фактически не давая вам почувствовать работу, которую он выполняет (однако, он все равно будет быстрее разряжайте аккумулятор, если это дорогостоящая операция). Иногда оптимизатор может даже переместить выделение в стек, и такого рода выделение в основном бесплатное и гораздо более дружественное к кешу. При этом методы оптимизации такого рода не идеальны (и на самом деле они не могут быть такими, поскольку идеальная оптимизация кода сложна с точки зрения NP, но это уже другая тема).

Давайте возьмем игры, например: Этот доклад о быстрой векторной математике в JS объясняет, как многократное распределение векторов (а в большинстве игр требуется МНОГО векторной математики) замедлило то, что должно быть очень быстрым: Векторная математика с Float32Array.В этом случае вы можете извлечь выгоду из пула, если вы используете правильный вид пула правильно.

Вот мои уроки, извлеченные из написания игр на Javascript:

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

Вместо

var x = new X(...);

используйте:

var x = X.create(...);

или даже:

// this keeps all your allocation in the control of `Allocator`:
var x = Allocator.createX(...);      // or:
var y = Allocator.create('Y', ...);

Таким образом, вы можете сначала реализовать X.create или Allocator.createX с return new X();, а затем заменить его на пул, чтобы легко сравнивать скорость.Более того, он позволяет вам быстро найти в своем коде всех выделений , чтобы вы могли просматривать их один за другим, когда придет время.Не беспокойтесь о дополнительном вызове функции, так как он будет встроен любым подходящим оптимизатором и, возможно, даже оптимизатором времени выполнения.

  • В общем, старайтесь свести создание объекта к минимуму.,Если вы можете повторно использовать существующие объекты, просто сделайте это.Возьмите 2D векторную математику в качестве примера: не делайте векторы (или другие часто используемые объекты) неизменяемыми.Несмотря на то, что неизменяемость создает более красивый и более устойчивый к ошибкам код, он имеет тенденцию быть чрезвычайно дорогим (потому что внезапно каждая векторная операция требует либо создания нового вектора, либо получения его из пула, а не просто добавления или умножения нескольких чисел).Причина, по которой в других языках можно сделать векторы неизменяемыми, заключается в том, что зачастую такое распределение может выполняться в стеке, что снижает стоимость выделения практически до нуля.Однако в Javascript -

Вместо:

function add(a, b) { return new Vector(a.x + b.x, a.y + a.y); }
// ...
var z = add(x, y);

try:

function add(out, a, b) { out.set(a.x + b.x, a.y + a.y); return out; }
// ...
var z = add(x, x, y);   // you can do that here, if you don't need x anymore (Note: z = x)
  • Не создавать временные переменные.Это делает параллельные оптимизации практически невозможными.

Избегайте:

var tmp = new X(...);
for (var x ...) {
    tmp.set(x);
    use(tmp);       // use() will modify tmp instead of x now, and x remains unchanged.
}
  • Как и временные переменные перед вашими циклами, простое объединение будет препятствовать оптимизации распараллеливания простые циклы : оптимизатору будет непросто доказать, что операциям пула не требуется определенный порядок, и, по крайней мере, ему потребуется дополнительная синхронизация, которая может не потребоваться для new (посколькувремя имеет полный контроль над тем, как распределять вещи).В случае сложных вычислительных циклов вы можете рассмотреть возможность выполнения нескольких вычислений за одну итерацию, а не только одного (который также известен как частично развернутый цикл ).
  • Если вы действительно не хотитенравится возиться, не пишите свой собственный пул.Там их уже много. Например, в этой статье перечислены целые группы.
  • Попытайтесь объединить в пул, только если вы обнаружите, что отток памяти разрушает ваш день.В этом случае убедитесь, что вы правильно профилируете свое приложение, определите узкие места и отреагируете.Как всегда: не оптимизировать вслепую.
  • В зависимости от типа алгоритма запроса к пулу, вы можете захотеть использовать разные пулы для долгоживущих и недолговечных объектов, чтобы избежать фрагментации недолговечного пула.Запросы недолговечных объектов гораздо более критичны для производительности, чем запросы долгоживущих объектов (поскольку первые могут происходить сотни, тысячи или даже миллионы раз в секунду).

Алгоритмы пула

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

  1. Связанный список: в списке хранятся только пустые объекты.Всякий раз, когда объект нужен, удалите его из списка с небольшими затратами.Положите его обратно, когда объект больше не нужен.
  2. Массив: сохранить все объекты в массиве.Всякий раз, когда нужен какой-либо объект, перебирайте все объекты пула, возвращайте первый свободный объект и устанавливайте для его флага inUse значение true.Отключите его, когда объект больше не нужен.

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

Заключительные слова

Что бы вы ни решили использовать, продолжайте профилировать, исследовать и делиться успешными подходами, чтобы заставить наш любимый код JS работать еще быстрее!

13 голосов
/ 29 июня 2013

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

В качестве конкретного примера (не обязательно для JavaScript,но в качестве общей иллюстрации), подумайте об играх с продвинутой 3D-графикой.Если одна игра имеет среднюю частоту кадров 60 кадров в секунду, это на быстрее , чем в другой игре со средней частотой кадров 40 кадров в секунду.Но если fps второй игры последовательно 40, графика выглядит плавной, тогда как если скорость первой часто намного выше 60fps, но время от времени падает до 10fps время от времени, графика выглядит неустойчивой.

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

Конечно, это не общий оператор, который охватывает все случаи.Один из сценариев, в котором объединение в пул может улучшить не только изменчивость, но и необработанную производительность, - это когда вы часто выделяете большие массивы: просто задав arr.length = 0 и повторно используя arr, вы можете улучшить производительность, избегая будущих изменений размера.Точно так же, если вы часто создаете очень большие объекты , которые имеют общую схему (т.е. они имеют четко определенный набор свойств, поэтому вам не нужно «чистить» каждый объект при возвратечто касается пула), то в этом случае вы также можете увидеть улучшение производительности от пула.

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

4 голосов
/ 09 декабря 2011

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

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

3 голосов
/ 31 марта 2014

Я думаю, что это зависит от сложности ваших объектов. Недавно я оптимизировал текстовый процессор JavaScript, который использовал объекты JS в паре с объектами DOM для каждого элемента в документе. До реализации пула объектов время загрузки моего тестового документа составляло около 480 мс. Техника объединения уменьшила это до 220 мс.

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

3 голосов
/ 21 сентября 2012

Объединение объектов может помочь, особенно если вы перемешиваете много объектов.Я недавно написал статью на эту тему, которую стоит прочитать.

http://buildnewgames.com/garbage-collector-friendly-code/

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...