#javascript #performance
#javascript #Производительность
Вопрос:
У меня есть общий вопрос, который касается того, возможно ли создать итераторы с нулевым распределением в Javascript. Обратите внимание, что под «итератором» я не женат на текущем определении итератора в ECMAScript, а просто на общем шаблоне для перебора пользовательских диапазонов.
Чтобы конкретизировать проблему, скажем, у меня есть список, подобный [5, 5, 5, 2, 2, 1, 1, 1, 1] и я хочу сгруппировать соседние повторения вместе и обработать их в форме, которая больше похожа на [5, 3], [2, 2], [1, 4]. Затем я хочу получить доступ к каждой из этих пар внутри цикла, что-то вроде «для каждой пары в сгруппированном (массиве), сделайте что-нибудь с pair». Кроме того, я хочу повторно использовать этот алгоритм группировки во многих местах и, что особенно важно, в некоторых действительно горячих внутренних циклах (думаю, миллионы циклов в секунду).
Вопрос: Существует ли шаблон итерации для выполнения этого, который имеет нулевые накладные расходы, как если бы я сам написал цикл вручную?
Вот то, что я пробовал до сих пор. Давайте предположим для конкретности, что я пытаюсь вычислить сумму всех пар. (Чтобы было ясно, я не ищу альтернативные способы написания этого кода, я ищу шаблон абстракции: код здесь только для того, чтобы привести конкретный пример.)
Встраивание кода группировки вручную.Этот метод работает наилучшим образом, но скрывает цель вычисления. Кроме того, встраивание вручную подвержено ошибкам и раздражает.
function sumPairs(array) {
let sum = 0
for (let i = 0; i != array.length; ) {
let elem = array[i ], count = 1
while (i < array.length amp;amp; array[i] == elem) { i ; count ; }
// Here we can actually use the pair (elem, count)
sum = elem count
}
return sum
}
Использование шаблона посетителя.Мы можем написать reduceGroups
функцию, которая будет вызывать заданное visitor(acc, elem, count)
для каждой пары (elem, count)
, аналогично обычному Array.reduce
методу. С этим наши вычисления становятся несколько понятнее для чтения.
function sumPairsVisitor(array) {
return reduceGroups(array, (sofar, elem, count) => sofar elem count, 0)
}
К сожалению, Firefox, в частности, по-прежнему выделяет при запуске этой функции, если определение замыкания не перемещается вручную за пределы функции. Кроме того, мы теряем способность использовать управляющие структуры, например break
, если мы не усложним интерфейс сильно.
Написание пользовательского итератора.Мы можем создать пользовательский «итератор» (не итератор ES6), который предоставляет elem
count
свойства and , empty
свойство, указывающее, что больше не осталось пар, и next()
метод, который обновляет elem
и count
до следующей пары. Потребляющий код выглядит следующим образом:
function sumPairsIterator(array) {
let sum = 0
for (let iter = new GroupIter(array); !iter.empty; iter.next())
sum = iter.elem iter.count
return sum
}
Я нахожу этот код самым простым для чтения, и мне кажется, что это должен быть самый быстрый метод абстракции. (В лучшем случае скалярная замена может полностью свернуть определение итератора в функцию. Во втором лучшем случае должно быть ясно, что итератор не экранирует цикл for , поэтому он может быть распределен по стеку). К сожалению, и Chrome, и Firefox, похоже, выделяют здесь.
Из приведенных выше подходов пользовательский итератор работает довольно хорошо в большинстве случаев, за исключением случаев, когда вам действительно нужно нажать педаль до упора в горячий внутренний цикл, и в этот момент давление GC становится очевидным.
Я бы также согласился с постпроцессором Javascript (возможно, компилятором закрытия Google?) который способен выполнить это.
Ответ №1:
Проверьте это. Я не тестировал его производительность, но она должна быть хорошей.
( ) (в основном) совместим с итераторами ES6.
(-) принесено ...GroupingIterator.from(arr)
в жертву, чтобы не создавать объект-значение (imo. garbage). Это в основном в приведенном выше пункте.
afaik, основным вариантом использования для этого в любом случае является for..of
цикл.
( ) не создано объектов (GC)
( ) объединение объектов для итераторов; (снова GC)
( ) совместимость с управляющими структурами, такими как break
class GroupingIterator {
/* object pooling */
static from(array) {
const instance = GroupingIterator._pool || new GroupingIterator();
GroupingIterator._pool = instance._pool;
instance._pool = null;
instance.array = array;
instance.done = false;
return instance;
}
static _pool = null;
_pool = null;
/* state and value / payload */
array = null;
element = null;
index = 0;
count = 0;
/* IteratorResult interface */
value = this;
done = true;
/* Iterator interface */
next() {
const array = this.array;
let index = this.index = this.count;
if (!array || index >= array.length) {
return this.return();
}
const element = this.element = array[index];
while ( index < array.length) {
if (array[index] !== element) break;
}
this.count = index - this.index;
return this;
}
return() {
this.done = true;
// cleanup
this.element = this.array = null;
this.count = this.index = 0;
// return iterator to pool
this._pool = GroupingIterator._pool;
return GroupingIterator._pool = this;
}
/* Iterable interface */
[Symbol.iterator]() {
return this;
}
}
var arr = [5, 5, 5, 2, 2, 1, 1, 1, 1];
for (const item of GroupingIterator.from(arr)) {
console.log("element", item.element, "index", item.index, "count", item.count);
}
Комментарии:
1. Спасибо за ответ. У меня есть некоторые оговорки по поводу этого подхода — хотя объединение объектов может удалить выделения, оно также может уменьшить локальность ссылок, и поэтому я ожидаю, что с точки зрения времени было бы лучше выделить, а не объединить здесь. Другое дело — использование протокола итератора — я очень подозреваю, что это может быть так же быстро, как обычные циклы — например, посмотрите на эту скрипку . На моей машине прямой цикл выполняется в 4 раза быстрее, чем итераторы в firefox, в 2 раза быстрее в Chrome. Кроме того, firefox тратит время на GC в цикле итератора.