23

I was attempting this Codewars challenge and the problem involves finding the divisors of a number and then calculating the sum of these divisors squared. I found two approaches to this problem.

The first approach is based on another Stackoverflow questions about finding the sum of all divisors and seems clever at first:

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);
}

The second approach I used was using a simple for-loop:

function divisorsSquared(n) {
  var sum = 0;
  for(var i = 1; i<= n; i++){
     if(n % i === 0) sum += Math.pow(i,2); 
  }
  return sum;
}

Now I noticed that the first approach is significantly slower than the second and a quick jsperf test confirms this.

My questions are: Why is the first approach so much slower and what approach is preferable in production code?

On Codewars I notice that for many challenges there are clever one-line solutions using similar array methods. As a beginner, may such solutions be considered better practice than for-loops, even if performance is worse?

Community
  • 1
  • 1
Miguel G.
  • 345
  • 1
  • 2
  • 7
  • 6
    If you look at the code, the first one uses a constructor, then iterates to spread, then iterates to get the keys, then iterates to slice, then iterates to reduce ... The second one iterates ... once. – adeneo Apr 22 '17 at 06:50
  • I didn't think about that, but it seems pretty obvious now. Thanks! – Miguel G. Apr 22 '17 at 07:07
  • 3
    Performance difference is going to be inconsequential in nearly every practical real-world scenario - go with the more readable for loop approach unless you're trying to impress your co-workers, in which case you might want to stop programming and become a guitarist, or bodybuilder as you'll cause less grief for humanity with your desire for attention. – JTW Mar 12 '20 at 14:32

3 Answers3

9
  • Array(n+1) allocates an array with n + 1 elements, Array(n+1).keys() returns an iterator over the created array's indices, but the spread operator [...Iterator] helps "unwrap" this iterator into yet another array, then finally slice(1) comes along to copy the secondly created array starting at index 1 which allocates yet another array (third one) with the number 0 discarded. So that were 3 allocations but 2 were dicarded. Your for-loop does not allocate any arrays, it is a simple traversal in O(n) with only 2 allocations for i and sum, so it is more efficient
  • sum+(!(n % (num)) && Math.pow(num,2)) is essentially the same as if(n % i === 0) sum += Math.pow(i,2); but the if approach is way more readable. If I were the judge, I would pick the second approach because it is more memory efficient, yet it favors readability.
Trash Can
  • 6,608
  • 5
  • 24
  • 38
6

Looking into the code, for loop is obviously less complex and more readable.

Consider you are working within a team, maximum number of your team members will know what the code is doing right away. Some will have to look up what the reduce() method is, but then they'll also know what's going on. So here, a for loop is easier for others to read and understand.

On the other side, native array functions (filter(), map(), reduce()) will save you from writing some extra code and also slower in performance.

For a beginner, I think for-loops should be better over native array functions.

Mamun
  • 66,969
  • 9
  • 47
  • 59
  • 1
    I'm an old-school, traditional For loop tells intend and purpose, also easier to catch bu. One-line chain is the opposite plus takes much longer to understand what and why. – Jeb50 Sep 10 '19 at 18:46
3

Functional or imperative approaches makes a difference in JS. Imperative always wins. Yet, the real thing is most of time a better algorithm is the winner. Your code is a naive approach. You can tune it to work much better just by checking the integers up until the square root of the target value and you will get two answers per check. If target is 100 if 2 is a dividend then 100/2 must be a dividend too.. So it's fair to check up to Math.sqrt(100) - 1 and handle 10 with care in order to not consider it twice.

Accordingly now the functional solution with reduce beats the imperative naive solution.

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);
Redu
  • 25,060
  • 6
  • 56
  • 76
  • 4
    A bit disingenuous. If you make the same functional changes to the for loop, it will still run about twice as fast as the reduce. So the real winner is imperative and tuned. – Mike Nov 15 '18 at 21:42
  • 1
    @Mike I have to agree on the *a bit* part but that's it. In JS, same algorithm with `for` loop should always run faster but *a bit* faster. Never twice. I believe apart from Promises functional JS is a joke. – Redu Nov 15 '18 at 21:58
  • 2
    I was getting 0.7 on your functional and tuned and 0.23 on imperative and tuned, which is why I said twice. But it was off the cuff in a chrome console, and now you're making want to check again, because for the most part, I do think you're right, and there should be less of a diff. Your point stands though, the base algorithm makes the huge difference. Going from n to sqrt(n)+1 is the money maker here. – Mike Nov 16 '18 at 01:23
  • 1
    functional and tuned: 0.410ms for input: 1000000 the result is: 1388804117611 imperative and naive: 8.090ms for input: 1000000 the result is: 1388804117611 – curiosity26 Aug 29 '19 at 21:18
  • I’d never seen that `~~` [double tilda](https://stackoverflow.com/a/5971668/12137312) syntax before. wonder if that has any performance impacts – lys Apr 29 '23 at 01:00
  • @lys No.. not in this case since it's used only once and even if not, it wouldn't make big difference too. I guess I was being lazy to type Math.floor(). Though keep in mind it behaves like Math.floor for positive floats and Math.ceil for negative floats so it behaves more like Math.trunc(). One other common operator doing the same is `f >> 0`. – Redu Apr 29 '23 at 06:40