【问题标题】:LINQ groupby with custom IEqualityComparer + combined properties - performance problems具有自定义 IEqualityComparer + 组合属性的 LINQ groupby - 性能问题
【发布时间】:2018-04-17 10:14:19
【问题描述】:

我有一个数据列表,该数据列表来自实体框架数据库查询与另一个相同类型的 IEnumerable 以及来自其他来源的内存数据。对于我们的一些客户,这个列表大约有 200000 个条目(大约一半来自 db),这使得分组操作需要非常长的时间(在我们廉价的虚拟 Windows 服务器上长达 30 分钟)。

分组操作将列表减少到大约 10000 个对象(大约 20:1)。

List的数据类基本上就是一大排Strings和Ints以及其他几个基本类型:

public class ExportData
{
  public string FirstProperty;
  public string StringProperty;
  public string String1;
  ...
  public string String27;
  public int Int1;
  ...
  public int Int15;
  public decimal Mass;
  ...
}

分组是通过一个自定义的 IEqualityComparer 完成的,基本上相当于:

  1. 如果允许按自定义逻辑对项目进行分组,这意味着两个对象的属性大约有一半是相等的,并且这些是我们从现在开始关心的唯一属性,除了 ID、Mass 和特殊的 StringProperty即使允许对项目进行分组,这仍然可能有所不同。
  2. 每个新的分组对象都应具有相关属性(在步骤 1 中相同),加上来自分组项的组合 ID 作为字符串和分组项的所有质量(十进制)属性的总和,以及应根据任何分组项中是否出现特殊字符串来设置特殊 StringProperty。

List<ExportData> exportData; //在内存列表中来自数据库+内存数据的组合数据

exportData = exportData.GroupBy(w => w, new ExportCompare(data)).Select(g =>
{
  ExportData group = g.Key;
  group.Mass = g.Sum(s => s.Mass);

  if (g.Count() > 1)
  {
    group.CombinedIds = string.Join("-", g.Select(a => a.Id.ToString()));
  }

  if (g.Any(s => s.StringProperty.Equals("AB"))) 
  {
    group.StringProperty= "AB";
  }
  else if (g.Any(s => s.StringProperty.Equals("CD"))) 
  {
    group.StringProperty= "CD";
  }
  else
  {
    group.StringProperty= "EF";
  }

  return group;
}).ToList();

以及用于完整性的自定义比较器:

public class ExportComparer : IequalityComparer<ExportData>
{
  private CompareData data;

  public ExportComparer()
  {
  }
  public ExportComparer(CompareData comparedata)
  {
    // Additional data needed for comparison logic
    // prefetched from another database
    data = comparedata;
  }
  public bool Equals(ExportData x, ExportData y)
  {
    if (ReferenceEquals(x, y)) return true;

    if (ReferenceEquals(x, null) || ReferenceEquals(y, null)) return false;

    (...) // Rest of the unit-tested and already optimized very long comparison logic
    return equality; // result from the custom comparison
  }

  public int GetHashCode(ExportData obj)
  {
    if (ReferenceEquals(obj, null)) return 0;

    int hash = 17;

    hash = hash * 23 + obj.FirstProperty.GetHashCode();
    (...) // repeated for each property used in the comparison logic
    return hash;

我该怎么做才能让这个 groupby 跑得更快?

【问题讨论】:

  • 您是否有足够的 RAM 来存储 200K 条目以不使用 HDD 作为临时空间?如果您的比较逻辑已经优化,那么除了改进硬件之外,您无能为力。
  • 你为什么要为每个元素创建一个新的ExportComparer?特别是考虑到Equals 方法不依赖于CompareData
  • 为什么不直接使用存储过程,让SQL Server对数据进行分组优化,然后懒加载结果呢?这样,您的应用程序服务器就不会受到重创,SQL Server 将执行其设计的工作,即处理此类繁重的任务。
  • 所以澄清一下,将 200k 项在内存中分组需要 30 分钟?
  • 与@VidmantasBlazevicius 建议的类似,尝试通过删除Select 成本来消除它并检查执行var grouped = exportData.GroupBy(w =&gt; w, new ExportCompare(data)).ToList(); 的时间。如果它很慢,那么问题出在你的比较器上。我怀疑是这种情况,因为这种具有内存结构的时间通常表示二次时间复杂度算法。

标签: c# asp.net performance linq


【解决方案1】:

很难建议对比较器进行优化,因为它的代码没有显示,但对Select 子句进行了优化。

现在您在该选择中使用SumCountSelectAny(2 次)。这意味着每组中的元素被评估 5 次(从它们中至少完全评估 3 次)。相反,您可以使用一次 foreach 循环,然后自己评估您的条件:

exportData.GroupBy(w => w, new ExportCompare(data)).Select(g =>
{                
    ExportData group = g.Key;
    decimal mass = 0m;
    var ids = new List<int>();
    bool anyAb = false;
    bool anyCd = false;
    // only one loop
    foreach (var item in g) {
        mass += item.Mass;
        ids.Add(item.Id);
        anyAb = anyAb || item.StringProperty.Equals("AB");
        anyCd = anyCd || item.StringProperty.Equals("CD");
    }
    group.Mass = mass;
    if (ids.Count > 1) {
        group.CombinedIds = string.Join("-", ids);
    }
    if (anyAb)
        group.StringProperty = "AB";
    else if (anyCd)
        group.StringProperty = "CD";
    else
        group.StringProperty = "EF";

    return group;
}).ToList();

现在我们只循环一次分组,这应该比重复 5 次更有效率。

【讨论】:

    【解决方案2】:

    g.Count() &gt; 1 可以优化为g.Any(),因为您并不真正关心计数,您只关心是否至少有一个元素。

    您对 AB 或 CD 的 Any/Any 调用可以在一个循环中而不是两个循环中处理。

    您可能想尝试从您的g 创建一个List 或数组,但这更多的是猜测,这取决于内部发生的情况以及您的组的构建方式,这可能是好事还是坏事。您需要进行测试和分析。

    但是,我强烈怀疑这里还有其他问题,要么您的 RAM 已用尽,要么需要优化您未显示的代码。 30分钟做一些记忆工作是疯狂的。

    【讨论】:

    • 我关心该计数行中是否有 2 个或更多项目,所以我真的不能在那里使用任何项目。 CustomComparer 自己在构造函数中给它的 CompareData 对象中的内存列表上进行一些查找。除此之外,根据输入属性,它只是在大约 20 种不同情况下的大量布尔逻辑。我可以尝试使用 equals 函数中的相关列表查找来更新问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-04
    • 2017-04-21
    • 2010-09-27
    • 1970-01-01
    • 2014-01-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多