【问题标题】:Performance of Linq query on large data setLinq 在大数据集上的查询性能
【发布时间】:2020-08-12 02:12:59
【问题描述】:

我正在运行一种方法来事务化存储在ConcurrentQueue<T> 中的数据。在 CPU 性能分析中,主要的影响似乎是:

foreach (Item inSequence in items.Where(w => w.SequenceNumber == i.SequenceNumber && w.Device == i.Device)) {}

对于 1,000 和 10,000,它实际上非常快。在 100,000 个项目时,性能变得至关重要 - 特定的 Linq 查询从占用总运行时 CPU 的 4.5% 到超过总运行时 CPU 的 58% 以上。我假设性能下降特别是由于ConcurrentQueue 的大小,但我不知道该怎么做。如果避免 Linq 查询解决了这个问题,那很好。我只是不知道该怎么做。还有其他一些性能更高的并发类型吗?

这是一个 CQ,因为数据是异步构建和读取的。然而,在这个特定的方法中,发生在数据构建之后,在数据被读回之前,它在单个线程上运行。

非常松散的样本在这里:https://dotnetfiddle.net/hjDOva

using System;
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

public class Program
{
    static int count = 100000;

    public static void Main()
    {
        var items = new ConcurrentQueue<Item>();
        var r = new Random();
        for (int i = 0; i < count; i++)
        {
            items.Enqueue(new Item());
        }

        var sw = Stopwatch.StartNew();
        foreach (Item i in items.DistinctBy(d => new { d.SequenceNumber, d.Device }))
            foreach (Item inSequence in items.Where(w => w.Device == i.Device && w.SequenceNumber == i.SequenceNumber))
            {

            }

        Console.WriteLine(sw.Elapsed);
    }
}

public static class Extensions
{
    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    {
        HashSet<TKey> seenKeys = new HashSet<TKey>();
        foreach (TSource element in source)
        {
            if (seenKeys.Add(keySelector(element)))
            {
                yield return element;
            }
        }
    }
}

public class Item
{
    #region Fields
    protected bool fixDates;
    protected string randomSerial;
    protected decimal amount;
    protected string device;
    protected DateTime depositTime;
    public int SequenceNumber = -1;
    [NonSerialized()]
    protected System.Random rnd = new Random(Int32.Parse(Guid.NewGuid().ToString().Substring(0, 8), System.Globalization.NumberStyles.HexNumber));
    #endregion

    #region Properties
    public bool FixDates
    {
        get
        {
            return this.fixDates;
        }

        set
        {
            this.fixDates = value;
        }
    }

    public string Amount
    {
        get
        {
            return this.amount.ToString();
        }

        set
        {
            this.amount = Convert.ToDecimal(value);
        }
    }

    public string RandomSerial
    {
        get { return randomSerial; }
        set { randomSerial = value; }
    }

    public string Device
    {
        get { return this.device; }
        set { this.device = value; }
    }

    public DateTime DepositTime
    {
        get { return this.depositTime; }
        set { this.depositTime = value; }
    }
    #endregion

    #region Constructors
    public Item()
    {
        fixDates = false;
        RandomSerial = Guid.NewGuid().ToString().Substring(0, 8);
        this.amount = 5.00m;
        this.device = "IC" + rnd.Next(6).ToString();
        this.depositTime = DateTime.Now;
        this.SequenceNumber = rnd.Next(10);
    }
    #endregion
}

但是它不提供 100,000 个项目所需的内存。

关于使用 CQ 的问题,是的,我知道队列对此并不理想。该工具生成数据以测试各种产品类型的进口情况。只有一个产品需要这种方法,Transactionalize()。大部分时间不使用此代码。

这是一个 CQ,因为系统会并行创建对象(当它发生时,这是一个显着的性能改进),并且在大多数情况下,它们也是并行出队的。

【问题讨论】:

  • 说实话,架构是有缺陷的。你有一个ConcurrentQueue,它是一个线程安全的 FIFO,但你想过滤它。您应该以一种又一种方式处理队列。如果您只想要任何线程安全的集合,请使用ConcurrentDictionary,将您的序列号和设备设置为Key。如果您确实必须有一个分区的 FIFO,请为每个 SequenceNumber 和 Device 创建一个 CQ。否则你正在做的事情有点违背 CQ 的目的。
  • 请向我们展示整个代码集——最好是我们可以自己运行的样本数据集。还请包括所有对象模型。从您的描述中我可以想象出哪里可能存在一些问题,但是除非我看到完整的代码,否则我无法给您答案。
  • 我猜它不是您发布的导致问题的代码,就像您在 for 循环或其他地方所做的一样。除了“hrrm,很抱歉听到这个消息
  • @zaitsman 哈哈,是的,这不会发生:)。然而,这个例子是为了说明需要更多信息的观点。其中可能包括关于适当的数据结构、模型、分析器实际在说什么、我们将其与什么进行比较等方面的长时间讨论。
  • @TheGeneral 如果队列中有 100_000 个项目的并发生产者和消费者,我仍然认为使用 CQ 过滤不是超级有效。

标签: c# performance linq


【解决方案1】:

假设下面代码的目的是分组处理项目,每个组具有相同的SequenceNumberDevice

foreach (Item i in items.DistinctBy(d => new { d.SequenceNumber, d.Device }))
    foreach (Item inSequence in items
        .Where(w => w.Device == i.Device && w.SequenceNumber == i.SequenceNumber))
    {

    }

...您可以像这样使用 Linq 方法 GroupBy 更有效地实现相同的目标:

var groups = items.GroupBy(i => (i.SequenceNumber, i.Device));
foreach (IGrouping<(string, string), Item> group in groups)
    foreach (Item inSequence in group)
    {

    }

请注意,我使用更轻量级的 ValueTuples 作为键,而不是 anonymous types,它不需要垃圾回收。

如果您还希望以后能够非常高效地搜索特定组,请使用类似的ToLookup,而不是GroupBy

【讨论】:

  • 为什么他们“不需要垃圾回收”?
  • @TheodorZoulias - 我认为使用我当地的白话是对的。传递给方法调用的值类型放在堆栈上,但是当您有一个使用值类型作为字段或属性的类时,例如System.Linq.Lookup&lt;TKey,TElement&gt;.Grouping 类,那么该类的全部内容都在堆上,以及其中包含的值类型。
  • @TheodorZoulias - 如果有 100,000 个键,那么使用值类型作为键可能会将垃圾收集项的数量从 200,000 减少到 100,000,但这并不意味着使用值类型作为键你已经消除了垃圾收集的需要。恕我直言,您在答案中的措辞具有误导性。不过,我宁愿让 Eric Lippert 在这里权衡一下。他肯定知道编译器会在这里做什么。
  • @JesseWilliams 最初您可能因为没有直接在问题中包含代码而被否决。外部链接被认为容易成为死链接,因此在问题中包含代码通常更安全。
猜你喜欢
  • 2017-07-16
  • 1970-01-01
  • 1970-01-01
  • 2014-02-08
  • 1970-01-01
  • 2013-08-04
  • 2010-10-10
  • 2021-06-26
  • 1970-01-01
相关资源
最近更新 更多