JavaScript性能:reduce() vs for循环

23

我试图完成这个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上,我注意到对于许多挑战,有聪明的单行解决方案使用类似的数组方法。作为初学者,即使性能更差,这样的解决方案是否可以认为是更好的实践而不是for循环?


6
如果您查看代码,第一个使用构造函数,然后迭代展开,然后迭代获取键,然后迭代切片,最后迭代减少...第二个则只迭代一次。 - adeneo
我之前没有想到,但现在看起来很明显。谢谢! - Miguel G.
3
在几乎所有实际的现实场景中,性能差异都是微不足道的 - 除非你试图给同事留下深刻印象,否则请使用更易读的for循环方法。如果你想要引起注意,那么你可能应该停止编程并成为吉他手或健美运动员,因为这样你会对人类造成更少的痛苦。 - JTW
3个回答

9
  • Array(n+1) 分配了一个有 n + 1 个元素的数组,Array(n+1).keys() 返回一个迭代器,用于遍历这个数组的索引。但是展开运算符 [...Iterator] 可以将这个迭代器 "解开",生成另一个数组,然后再使用 slice(1) 复制第二个创建的数组,从索引 1 开始,这样就分配了另一个数组(第三个),且舍弃了数字 0。因此,这里进行了 3 次分配,但只保留了 2 次。相比之下,您的 for-loop 不会分配任何数组,它只是简单地遍历,时间复杂度为 O(n),只需要分配 isum 两个变量,因此更加高效。
  • sum+(!(n % (num)) && Math.pow(num,2)) 本质上与 if(n % i === 0) sum += Math.pow(i,2); 相同,但使用 if 更容易理解。如果我是裁判,我会选择第二种方法,因为它更节省内存,同时也更易读。

6

浏览代码,for循环显然较不复杂,更易读。

考虑你正在一个团队中工作,大多数团队成员都会立即知道代码在做什么。 有些人需要查找reduce()方法的含义,但他们也会知道发生了什么。 因此,在这种情况下,for循环对其他人来说更容易阅读和理解。

另一方面,本机数组函数(filter(), map(), reduce())将为您节省一些额外的代码编写,并且速度较慢。

对于初学者,我认为for循环比本机数组函数更好。


1
我是一个老派的程序员,传统的For循环语句更能表达意图和目的,也更容易捕捉错误。而一行链式代码则相反,需要花费更长时间来理解它的作用和原因。 - Jeb50

3

在JS中,函数式或命令式方法会产生不同的结果。 命令式总是胜出。 然而,真正的问题是大多数时候更好的算法才是赢家。 您的代码是一种简单的方法。 您可以通过检查整数直到目标值的平方根并每次检查获得两个答案来调整它以使其工作得更好。 如果目标为100,如果2是被除数,则100/2也必须是被除数。 所以公平的做法是检查 Math.sqrt(100) - 1 并小心处理10以避免重复考虑。

因此,现在使用reduce的函数式解决方案击败了命令式简单解决方案。

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


4
有点不太真诚。如果对for循环进行相同的功能更改,它仍然比reduce运行快约两倍。因此,真正的赢家是命令式和优化过的。 - Mike
1
@Mike 我必须同意“有点”这一部分,但仅此而已。在JS中,使用for循环的相同算法应始终运行得更快,但只是稍微快一点。除了Promises之外,我认为函数式JS就是一个笑话。 - Redu
2
我在你的函数式和调优上得到了0.7分,在命令式和调优上得到了0.23分,这就是为什么我说了两次。但那只是在Chrome控制台中随口说的,现在你让我想再检查一下,因为大部分情况下,我确实认为你是对的,应该有更小的差异。不过你的观点依然成立,基本算法才是关键。从n到sqrt(n)+1是赚钱的关键。 - Mike
1
功能齐全且调整良好:0.410毫秒 对于输入:1000000,结果为:1388804117611 命令式和天真:8.090毫秒 对于输入:1000000,结果为:1388804117611 - curiosity26
我以前从未见过 ~~ 双波浪线 语法。不知道它是否会对性能产生影响。 - lys
@lys 不是在这种情况下使用,因为它只使用一次,即使没有使用,也不会有太大的区别。我想我懒得打Math.floor()。但请记住,对于正浮点数,它的行为类似于Math.floor,对于负浮点数,它的行为类似于Math.ceil,因此其更像于Math.trunc()。另一个常用的执行相同操作的运算符是 f >> 0 - Redu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接