FirstOrDefault()的性能表现

7

今天我用性能分析器查看了我所工作的Web应用程序的一个部分。我认为使用Union可能会导致一些延迟,但结果却让我感到惊讶。

造成延迟的原因之一是FirstOrDefault。

这是一个非常简单的LINQ查询,代码如下:

foreach(Report r in reports)
    IDTOStudy study = studies.FirstOrDefault(s => s.StudyID == r.StudyID);

我创建了一个小方法来复制我认为FirstOrDefault正在执行的行为。
private IDTOStudy GetMatchingStudy(Report report, IList<IDTOStudy> studies)
{
    foreach (var study in studies)
    if (study.StudyID == report.StudyID)
        return study;

    return null;
}

这种方法将FirstOrDefault替换为以下形式:
foreach(Report r in reports)
    IDTOStudy study = GetMatchingStudy(r, studies);

通过性能分析器查看新代码运行情况,发现FirstOrDefault的完成时间是我的新方法的两倍。这真是令人大吃一惊。

我在FirstOrDefault()查询中肯定做错了什么。是什么呢?

FirstOrDefault()是否会完成整个查询,然后选择第一个元素?

如何加快速度并使用FirstOrDefault()

注1:

我注意到另一个问题是,性能分析工具显示这两种实现都占用了我的CPU。这也是我不喜欢并且没预料到的事情。我添加的额外方法没有减少CPU使用量,只是将其持续时间缩短了一半。

注3:

将这些研究放入字典中可以极大地提高运行时间。这肯定是提交代码的形式。但这并没有回答关于FirstOrDefault的问题。

注2:

这是一个简单的控制台应用程序中请求的示例代码。在大多数情况下,我的运行显示FirstOrDefault需要更长时间。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reactive.Linq;
using System.Reactive.Concurrency;
using System.Diagnostics;

namespace TestCode
{
    public class Program
    {
        public List<IntHolder> list;

        public static void Main(string[] args)
        {
            var prog = new Program();
            prog.list = new List<IntHolder>();

            prog.Add50000Items();
            prog.list.Add(new IntHolder() { Num = 12345 });
            prog.Add50000Items();

            var stopwatch = new Stopwatch();
            stopwatch.Start();
            prog.list.FirstOrDefault(n => n.Num == 12345);
            stopwatch.Stop();

            Console.WriteLine("First run took: " + stopwatch.ElapsedTicks);
            var lookingFor = new IntHolder() { Num = 12345 };

            stopwatch.Reset();
            stopwatch.Start();
            prog.GetMatching(lookingFor);
            stopwatch.Stop();
            Console.WriteLine("Second run took: " + stopwatch.ElapsedTicks);
            Console.ReadLine();
        }

        public void Add50000Items()
        {
            var rand = new Random();

            for (int i = 0; i < 50000; i++)
                list.Add(new IntHolder() { Num = rand.Next(100000) });
        }

        public IntHolder GetMatching(IntHolder num)
        {
            foreach (var number in list)
                if (number.Num == num.Num)
                    return number;

            return null;
        }
    }

    public class IntHolder
    {
        public int Num { get; set; }
    }
}

如果您正在使用 Linq to SQL 或 EF,则 FirstOrDefault() 应该只生成 TOP 1 查询。 - Sergey Berezovskiy
@lazyberezovsky 我的猜测是第一个例子生成O(n)的查询,而第二个例子生成O(1)的查询。 - undefined
4
我的第一反应是错误出在你对代码进行分析的方式上,而不是它的执行方式。你能否提供一个示例程序,演示FirstOrDefault和你自己的同样模式的自定义方法之间的区别? - Servy
1
LINQ-to-objects比等效的手写代码慢2倍是很常见的。额外的成本主要是由于额外的间接调用(委托,接口)造成的。 - CodesInChaos
2
为什么不使用适当的数据结构进行查找,比如字典?你的两个算法都是O(reports.Count*studies.Count)。使用适当的数据结构可以将其优化为O(reports.Count)。 - CodesInChaos
显示剩余4条评论
1个回答

4

我认为正在发生的事情(尽管获取有关您特定情况的一些额外信息会很好,但我假设这是基于您的DTO类的数据库场景)如下所示:

foreach(Report r in reports)
    IDTOStudy study = studies.FirstOrDefault(s => s.StudyID == r.StudyID); //Database query happens here for each report


//The whole studies table is loaded into memory which means you only do one DB query and the actual firstordefault stuff is done in memory which is quicker than going over the network
private IDTOStudy GetMatchingStudy(Report report, IList<IDTOStudy> studies)
{
    foreach (var study in studies)
    if (study.StudyID == report.StudyID)
        return study;

    return null;
}

这意味着在第二个例子中,你已经优化了数据库往返(这是一个好主意)。你可以通过像SQL Profiler这样的工具检查后台的数据库查询来证明这个理论。

4
关于这种优化的一个注意点 - 如果你的数据库中有100万个研究,则将所有研究加载到内存中并不是好的优化方法。 - Sergey Berezovskiy
DTO研究列表已经通过SQL查询填充,并已使用ToList()强制转换为List。因此,我认为我没有在打DB - 这也是我的初始想法。澄清一下,列表中大约有20,000项研究。 - Chris
3
@lazyberezovsky 是的,绝对没错。解决这个问题的更好方法(可以很好地扩展)是在数据库端而不是应用程序端完成整个操作。 - undefined

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