Позвольте мне начать с высказывания: я бы советовал не использовать пулы, если вы не разрабатываете визуализации, игры или другой вычислительно дорогой код, который на самом деле выполняет много работы. Ваше среднее веб-приложение привязано к вводу / выводу, а ваш ЦП и ОЗУ будут простаивать большую часть времени. В этом случае вы получаете гораздо больше, оптимизируя скорость ввода-вывода, а не скорость выполнения; Т.е. убедитесь, что ваши файлы загружаются быстро, и вы используете клиентскую сторону, а не серверную визуализацию + шаблонирование. Однако, если вы увлекаетесь играми, научными вычислениями или другим связанным с процессором кодом Javascript, эта статья может быть вам интересна.
Короткая версия :
В коде, критичном к производительности:
- Начните с использования оптимизаций общего назначения [1] [2] [3] [4] (и многое другое). Не прыгайте в лужи сразу (вы понимаете, о чем я!).
- Будьте осторожны с синтаксическим сахаром и внешними библиотеками, так как даже Обещания и многие встроенные модули (такие как
Array.concat
и т. Д.) Делают много злых вещей под капотом , включая отчисления.
- Избегайте неизменных (например,
String
), поскольку они будут создавать новые объекты во время операций по изменению состояния, которые вы над ними выполняете.
- Знайте свои ассигнования. Используйте инкапсуляцию для создания объектов, чтобы вы могли легко найти все распределения и быстро изменить свою стратегию распределения во время профилирования.
- Если вы беспокоитесь о производительности, всегда профилируйте и сравнивайте разные подходы. В идеале, вы не должны случайно верить кому-то на intarwebz (включая меня). Помните, что наши определения слов, таких как «быстрый», «долгоживущий» и т. Д., Могут значительно отличаться.
- Если вы решили использовать пул:
- Возможно, вам придется использовать разные пулы для долгоживущих и недолговечных объектов, чтобы избежать фрагментации недолговечного пула.
- Вы хотите сравнить разные алгоритмы и разную степень детализации пула (объединить целые объекты или только некоторые свойства объекта?) Для разных сценариев.
- Объединение в пул увеличивает сложность кода и, следовательно, усложняет работу оптимизатора, потенциально снижая производительность.
Длинная версия :
Во-первых, учтите, что куча системы, по сути, такая же, как пул больших объектов. Это означает, что всякий раз, когда вы создаете новый объект (используя 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
(посколькувремя имеет полный контроль над тем, как распределять вещи).В случае сложных вычислительных циклов вы можете рассмотреть возможность выполнения нескольких вычислений за одну итерацию, а не только одного (который также известен как частично развернутый цикл ). - Если вы действительно не хотитенравится возиться, не пишите свой собственный пул.Там их уже много. Например, в этой статье перечислены целые группы.
- Попытайтесь объединить в пул, только если вы обнаружите, что отток памяти разрушает ваш день.В этом случае убедитесь, что вы правильно профилируете свое приложение, определите узкие места и отреагируете.Как всегда: не оптимизировать вслепую.
- В зависимости от типа алгоритма запроса к пулу, вы можете захотеть использовать разные пулы для долгоживущих и недолговечных объектов, чтобы избежать фрагментации недолговечного пула.Запросы недолговечных объектов гораздо более критичны для производительности, чем запросы долгоживущих объектов (поскольку первые могут происходить сотни, тысячи или даже миллионы раз в секунду).
Алгоритмы пула
Если вы не напишите очень сложный алгоритм запросов к пулу, вы, как правило, застряли с двумя или тремя вариантами.Каждый из этих вариантов быстрее в некоторых и медленнее в других сценариях.Наиболее часто встречаются следующие:
- Связанный список: в списке хранятся только пустые объекты.Всякий раз, когда объект нужен, удалите его из списка с небольшими затратами.Положите его обратно, когда объект больше не нужен.
- Массив: сохранить все объекты в массиве.Всякий раз, когда нужен какой-либо объект, перебирайте все объекты пула, возвращайте первый свободный объект и устанавливайте для его флага
inUse
значение true.Отключите его, когда объект больше не нужен.
Поиграйте с этими опциями.Если ваша реализация связанного списка не достаточно сложна, вы, вероятно, обнаружите, что решение на основе массива быстрее для недолговечных объектов (что и имеет значение для производительности пула), учитывая, что в массиве нет долгоживущих объектов, что вызываетпоиск свободного объекта стал излишне долгим.Если вам обычно требуется выделять более одного объекта за раз (например, для ваших частично развернутых циклов), рассмотрите вариант массового выделения, который выделяет (небольшие) массивы объектов, а не только один, чтобы уменьшить накладные расходы на поиск для нераспределенных объектов.Если вам действительно нужен быстрый пул (и / или вы просто хотите попробовать что-то новое), посмотрите на , как реализованы системные кучи , которые быстры и позволяют выделять различные размеры.
Заключительные слова
Что бы вы ни решили использовать, продолжайте профилировать, исследовать и делиться успешными подходами, чтобы заставить наш любимый код JS работать еще быстрее!