Производительность 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 должны быть лучше над встроенными функциями массива.