高效过滤大量POCO实体的方法

4
我正在努力提高一个针对大量POCO的Linq过滤器的性能,但本地测试表明存在CPU瓶颈。
我最初尝试这样做是为了减轻SQL服务器的负担,通过检索大量结果集并将其加载到单独的处理服务器的内存中,然后在.Net中过滤此结果集。
以下是演示代码:
public class CustomClass
{
    public int Id { get; set; }
    public int OtherId { get; set;}
    public DateTime Date { get; set; }
}

public void DoStuff()
{        
    // approx 800,000 items
    List<CustomClass> allItems = _repo.GetCustomClassItemsFromDatabase();

    foreach (OtherCustomClass foo in _bar)
    {
        // original linq-to-entities query,
        // get most recent Ids that apply to OtherId
        List<CustomClass> filteredItems = (
            from item in allItems
            where item.OtherId == foo.OtherId && item.Date <= foo.Date
            group item by item.Id into groupItems
            select groupItems.OrderByDescending(i => i.Date).First()).ToList();

        DoOtherStuff(filteredItems);
    }
}

这会使我的四个核心处理器的CPU持续100%使用率1分30秒,对于生产系统来说不可行。我在VS2012中运行了性能分析器,发现30%的时间是用于get调用item.OtherId
我开始重写Linq代码为普通代码,以查看是否可以获得任何速度提升,但到目前为止我没有取得任何成功。以下是我重写的普通代码:
private List<CustomClass> FilterCustomClassByIdAndDate(
    List<CustomClass> items, int id, DateTime date)
{
    var mostRecentCustomClass = new Dictionary<int, CustomClass>();

    foreach (CustomClass item in items)
    {
        if (item.Id != id || item.Date > date) { continue; }

        CustomClass mostRecent;
        if (mostRecentCustomClass.TryGetValue(item.Id, out mostRecent) &&
            mostRecent.Date >= item.Date) 
        { continue; }

        mostRecentCustomClass[item.Id] = item;
    }

    var filteredItems = new List<CustomClass>();

    foreach (KeyValuePair<int, CustomClass> pair in mostRecentCustomClass)
    {
        filteredItems.Add(pair.Value);
    }

    return filteredItems;
}

这仍然会导致 CPU 利用率达到 100%,并且在 item.OrderId 调用上占用了 30%。有人遇到过类似的问题吗?或者有一些改进方法吗? 编辑:代码展示巨大的改进 感谢 @FastAl,这段代码在 _bar -> DoOtherStuff(filteredItems) 循环中只需不到一秒的时间就可运行:
public void DoStuff()
{        
    // approx 800,000 items
    List<CustomClass> allItems = _repo.GetCustomClassItemsFromDatabase();

    var indexedItems = new Dictionary<int, List<CustomClass>>();

    foreach (CustomClass item in allItems)
    {
        List<CustomClass> allByOtherId;

        if (!indexedItems.TryGetValue(item.OtherId, out allByOtherId)) 
        {
            allByOtherId = new List<CustomClass>();
            indexedItems[item.OtherId] = allByOtherId;
        }

        allByOtherId.Add(item);
    }

    foreach (OtherCustomClass foo in _bar)
    {
        List<CustomClass> filteredItems;

        if (!indexedItems.ContainsKey(foo.OtherId))
        {
            filteredItems = new List<CustomClass>();
        }
        else
        {
            List<CustomClass> filteredItems = (
                from item in indexedItems[foo.OtherId]
                where item.Date <= foo.Date
                group item by item.Id into groupItems
                select groupItems.OrderByDescending(i => i.Date).First())
                .ToList();
        }

        DoOtherStuff(filteredItems);
    }
}

1
你是把800,000个项目加载到内存中吗?根据你的示例,你只有一个表,但是这是真的吗? - Dilshod
1
我知道这不是你确切问题的答案,但你能否将其转移到 SQL 服务器上的存储过程中?我已经注意到通过将有问题的 LINQ 移动到存储过程并创建适当的索引可以获得巨大的性能提升。 - Matt Johnson
4
如果您正在使用数据库,为什么要将所有数据都提取到列表中,然后再对列表进行筛选呢?LINQ 的主要作用是使在数据库中进行筛选变得容易。这里的主要问题似乎是使用了列表。 - Marc Gravell
3
高效过滤是数据库的用途,要充分利用它。如果让数据库服务器提供全部 800,000 条记录而不只是你感兴趣的那几条,我怀疑这对数据库服务器并没有什么好处。我的经验是,相较于 CPU 的使用,I/O 和网络操作更容易造成数据库瓶颈。 - Jeremy Todd
@MattJohnson:我同意,数据库在过滤方面更好。然而,当使用单个sproc时,我会多次获得相同的信息,希望在保持良好性能的同时减少这种情况。也许我应该从问题中删除数据库?@Jeremy Todd:不幸的是,在此过程的这一点上,我知道我需要foo列表中所有这800,000个项目中的至少1个记录,并且数据库必须累计提供至少800,000条记录以进行各个调用。 - Laurence
显示剩余10条评论
1个回答

4

使用列表字典。

在加载项目后,遍历一次以构建列表字典。注意添加的循环和 where 子句中的更改。

请原谅我的错误,我只有4分钟;-) 要学会喜欢使用字典。它非常快,使用其中最快速的搜索/插入方法之一。这真是从微软获得的一个非常棒的小工具。

我的诚实建议-在数据库中执行。问问自己-你是否已经在那里尝试过了?我做了一段时间,但我仍然无法确定两个未知的哪一个更快,除非首先进行测试(除非显而易见,但如果是这样,您就不会在此处发布此内容)。 请再次检查DB上是否有OtherID索引,否则它将面临与您的Linq语句相同的问题(线性搜索)。

public class CustomClass
{
    public int Id { get; set; }
    public int OtherId { get; set;}
    public DateTime Date { get; set; }
}

public void DoStuff()
{        
    // approx 800,000 items
    List<CustomClass> allItems = _repo.GetCustomClassItemsFromDatabase();
    var index1 = new Dictionary <int, CustomClass>; 
    foreach (OtherCustomClass foo1 in allItems)
    {
        List<CustomClass> allOtherIDs ;
        allOtherIDs=null;
        if (!index1.TryGetValue(foo1.OtherID,allOtherIDs))
         {
            allOtherIDs=new List<CustomClass>;
            index1.add(foo1.OtherID,allOtherIDs);
        }
        allOtherIDs(foo1.OtherID)=foo1;
    }


    foreach (OtherCustomClass foo in _bar)
    {
        // original linq-to-entities query,
        // get most recent Ids that apply to OtherId
        List<CustomClass> filteredItems = (
            from item in allOtherIDs(foo.OtherID)
            where item.Date <= foo.Date
            group item by item.Id into groupItems
            select groupItems.OrderByDescending(i => i.Date).First()).ToList();

        DoOtherStuff(filteredItems);
    }
}

1
好的,哇。我进行了这个更改,对于我的 800,000 条记录,在一个核心上它从 100% CPU 的 1m30s 变成了大约 90% CPU 的 1 秒钟。字典真是太棒了!数据库版本总体上仍然稍微快一些,但只是由于从 DB 中检索记录的初始阶段;实际上,它显著加快了处理速度。 - Laurence
太好了听到这个消息!现在你知道我为什么对此如此热情了! - FastAl

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