Enumerable.Range(...)。任何(...)都优于基本循环:为什么?

时间:2013-03-04 20:54:38

标签: c# performance algorithm linq

我正在调整从Scala到C#的简单素数生成单行(在其作者的this blog评论中提到)。我想出了以下内容:

int NextPrime(int from)
{
  while(true)
  {
    n++;
    if (!Enumerable.Range(2, (int)Math.Sqrt(n) - 1).Any((i) => n % i == 0))
      return n;
  }
} 

它有效,返回我从运行博客中引用的代码得到的相同结果。事实上,它运作得相当快。在LinqPad中,它在大约1秒内产生了第100,000个素数。出于好奇,我在没有Enumerable.Range()Any()的情况下重写了它:

int NextPrimeB(int from)
{
  while(true)
  {
    n++;
    bool hasFactor = false;
    for (int i = 2; i <= (int)Math.Sqrt(n); i++)
    {
        if (n % i == 0) hasFactor = true;
    }
    if (!hasFactor) return n;
  }
}

直观地说,我希望它们能够以相同的速度运行,或者甚至让后者运行得更快。实际上,使用第二种方法计算相同的值(第100,000个素数)需要 12秒 - 这是错误的差异。

那么这里发生了什么?从根本上说,在第二种方法中会出现额外的事情,这会占用CPU周期,或者在Linq示例的背景下进行一些优化。有谁知道为什么?

6 个答案:

答案 0 :(得分:9)

对于for循环的每次迭代,您都会找到n的平方根。请改为缓存它。

int root = (int)Math.Sqrt(n);
for (int i = 2; i <= root; i++)

正如其他人提到的那样,一找到因素就打破for循环。

答案 1 :(得分:4)

Enumerable.Any如果条件成功而你的循环没有成功,则提早退出。

  

一旦确定结果,就会停止source的枚举。

这是一个糟糕的基准测试的例子。尝试修改你的循环,看看差异:

    if (n % i == 0) { hasFactor = true; break; }
}

throw new InvalidOperationException("Cannot satisfy criteria.");

答案 2 :(得分:4)

LINQ版本短路,你的循环没有。我的意思是,当你确定一个特定的整数实际上是一个因素LINQ代码停止,返回它,然后继续前进。你的代码会一直循环,直到完成。

如果您更改for以包含该短路,您应该会看到类似的效果:

int NextPrimeB(int from)
{
  while(true)
  {
    n++;
    for (int i = 2; i <= (int)Math.Sqrt(n); i++)
    {
        if (n % i == 0) return n;;
    }
  }
}

答案 3 :(得分:4)

看起来这是罪魁祸首:

for (int i = 2; i <= (int)Math.Sqrt(n); i++)
{
    if (n % i == 0) hasFactor = true;
}

找到因素后,您应该退出循环:

if (n % i == 0){
   hasFactor = true;
   break;
}

正如其他人所指出的那样,将Math.Sqrt调用移到循环外部以避免在每个循环中调用它。

答案 4 :(得分:4)

在优化的名义下,你可以通过在2之后避免偶数来更聪明一点:

if (n % 2 != 0)
{
  int quux = (int)Math.Sqrt(n);

  for (int i = 3; i <= quux; i += 2)
  {
    if (n % i == 0) return n;
  }
}

还有一些其他方法可以优化素数搜索,但这是一种更容易做到且收益很大的方法。

编辑:您可能需要考虑使用(int)Math.Sqrt(n)+ 1.FP函数+向下舍入可能会导致您错过大素数的平方。

答案 5 :(得分:2)

问题的至少一部分是执行Math.Sqrt的次数。在LINQ查询中,这执行一次,但在循环示例中,它执行了N次。尝试将其拉出到本地并重新配置应用程序。这将为您提供更具代表性的细分

int limit = (int)Math.Sqrt(n);
for (int i = 2; i <= limit; i++)