Производительность Javascript: reduce() vs for-loop
Я пытался этот вызов Codewars, и проблема заключалась в нахождении делителей числа, а затем вычислении суммы этих квадратов в квадрате. Я нашел два подхода к этой проблеме.
Первый подход основан на других вопросах Stackoverflow о нахождении суммы всех делителей и кажется сначала умным:
function divisorsSquared(n) {
// create a numeric sequence and then reduce it
return [...Array(n+1).keys()].slice(1)
.reduce((sum, num)=>sum+(!(n % (num)) && Math.pow(num,2)), 0);
}
Второй подход, который я использовал, заключался в использовании простого цикла for:
function divisorsSquared(n) {
var sum = 0;
for(var i = 1; i<= n; i++){
if(n % i === 0) sum += Math.pow(i,2);
}
return sum;
}
Теперь я заметил, что первый подход значительно медленнее второго и быстрый тест jsperf подтверждает это.
Мои вопросы: почему первый подход намного медленнее и какой подход предпочтительнее в производственном коде?
В Codewars я замечаю, что для многих задач существуют умные однострочные решения, использующие аналогичные методы массива. Как новичок, могут ли такие решения рассматриваться лучше, чем для циклов, даже если производительность хуже?
Ответы
Ответ 1
Функциональные или императивные подходы делают разницу в JS. Императив всегда побеждает.
Тем не менее, реальная вещь - это самый лучший алгоритм, который является победителем. Ваш код - наивный подход. Вы можете настроить его, чтобы работать намного лучше, просто проверив целые числа до квадратного корня из целевого значения, и вы получите два ответа за проверку. Если цель равна 100, если 2 - дивиденд, то 100/2 также должен быть дивидендом. Поэтому честно проверить до Math.sqrt(100) - 1
и обрабатывать 10 с осторожностью, чтобы не рассматривать его дважды.
Соответственно теперь функциональное решение с уменьшением превзойдет императивное наивное решение.
function divisorsSquared(n) {
var sn = Math.sqrt(n);
return Array.from({length:~~sn-1},(_,i) => i+1)
.reduce((s,m) => n%m ? s : s + m*m + (n/m)*(n/m), 0) + (n%sn ? 0 : sn*sn);
}
var result = 0;
console.time("functional and tuned");
result = divisorsSquared(1000000);
console.timeEnd("functional and tuned");
console.log("for input: 1000000 the result is:",result);
function dvssqr(n) {
var sum = 0;
for(var i = 1; i<= n; i++){
if(n % i === 0) sum += Math.pow(i,2);
}
return sum;
}
console.time("imperative and naive");
result = dvssqr(1000000);
console.timeEnd("imperative and naive");
console.log("for input: 1000000 the result is:",result);
Ответ 2
-
Array(n+1)
выделяет массив с n + 1 элементами, Array(n+1).keys()
возвращает итератор по индексам созданного массива, но оператор с расширением [...Iterator]
помогает "развернуть" этот итератор в еще один массив, а затем, наконец, slice(1)
приходит, чтобы скопировать второй созданный массив, начинающийся с индекса 1
, который выделяет еще один массив (третий) с отбросом числа 0
. Так что было 3 распределения, но 2 были пропущены. Ваш for-loop
не выделяет никаких массивов, это простой обход в O (n) с двумя выделениями для i
и sum
, поэтому он более эффективен
-
sum+(!(n % (num)) && Math.pow(num,2))
по существу совпадает с if(n % i === 0) sum += Math.pow(i,2);
, но подход if
является более читаемым. Если бы я был судьей, я бы выбрал второй подход, потому что он эффективнее с точки зрения памяти, но он обеспечивает читаемость.
Ответ 3
Заглядывая в код, цикл, очевидно, менее сложный и читаемый.
Предположите, что вы работаете в команде, максимальное количество членов вашей команды будет знать, что делает код сразу.
Некоторым придется искать способ reduce()
, но потом они также узнают, что происходит.
Итак, здесь цикл чтения для других легче читать и понимать.
С другой стороны, встроенные функции массива (filter(), map(), reduce())
избавят вас от написания дополнительного кода
а также медленнее в производительности.
Для новичков, я думаю, for-loops должны быть лучше над встроенными функциями массива.