【问题标题】:Enumerable.Range(...).Any(...) outperforms a basic loop: Why?Enumerable.Range(...).Any(...) 优于基本循环:为什么?
【发布时间】:2013-03-04 20:54:37
【问题描述】:

我正在将一个简单的素数生成单线从 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 示例的背景下进行一些优化。有人知道为什么吗?

【问题讨论】:

  • 你可以通过打破内部 for 循环来加速它
  • 乍一看,我猜你会想在if 块内添加一个break。这将大大提高性能。
  • 这就是您的 linq 查询所做的事情,当它找到您的 for 循环完成整个迭代的答案时,它将停止

标签: c# performance algorithm linq


【解决方案1】:

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

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

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

【讨论】:

  • +1 用于提及别人没有做过的事情。这是减速的一个重要部分,除了没有打破第一个发现的因素。
【解决方案2】:

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】:

    看来这是罪魁祸首:

    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 个答案,几乎都是一样的。事实上,这是解决方案的更大部分。看起来 BradM 建议在循环外计算 sqrt 也有很大的不同。这两个变化导致算法比 Enumerable()/Any() 更快。
    • @KChaloux 这通常是可以预料的。只要您在正确的时间使用正确的运算符,LINQ 就可以更轻松地编写适度有效的代码,因为它比人们通常愿意投入的工作量来实现复杂的查询操作。如果您确实(正确地)自己实现了它们,尽管您可以省略所有迭代器块、委托创建等的开销。这种开销通常不是问题,但它可能是问题,具体取决于操作的类型完成。
    【解决方案4】:

    Enumerable.Any 如果条件成功而您的循环不成功,则提前退出。

    source 的枚举在可以确定结果后立即停止。

    这是一个糟糕的基准测试示例。尝试修改你的循环,看看有什么不同:

        if (n % i == 0) { hasFactor = true; break; }
    }
    
    throw new InvalidOperationException("Cannot satisfy criteria.");
    

    【讨论】:

      【解决方案5】:

      以优化的名义,您可以通过避免 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 函数 + 向下舍入可能会导致您错过一个大素数的平方。

      【讨论】:

      • 后来我确实考虑过。请注意,这并不是说它被用于生产代码,但它很有趣。 +1 是对任何可能将其用于更严重的事情的人的好建议。
      • 一旦你开始认真优化,你最终会完全放弃这个算法,转而使用一种旨在有效查找素数的算法。总的来说,这种方法存在致命缺陷,而且只有表现如此出色。
      • 是的,我同意。我的评论更多地是为了鼓励在寻找素数的背景下思考你实际尝试做的事情,这是考虑其他算法的途径。
      【解决方案6】:

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

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

      【讨论】:

        猜你喜欢
        • 2010-09-29
        • 2012-07-04
        • 2017-03-01
        • 2019-11-12
        • 2015-10-17
        • 1970-01-01
        • 2018-11-01
        • 1970-01-01
        • 2010-10-29
        相关资源
        最近更新 更多