【问题标题】:Occasional performance drops in linq queries to concurrent bag对并发包的 linq 查询偶尔会出现性能下降
【发布时间】:2019-03-06 12:07:05
【问题描述】:

我目前正在调查应用程序中的一些严重性能下降情况。

性能下降是一种奇怪的类型 - 连续几次迭代工作得非常快,但是有一个迭代需要更多时间才能完成。 这个应用程序使用图形,所以看起来很烦人。

请看下面的代码。

while (true)
{
    var rng = new Random(1);
    var concurrenBag = new ConcurrentBag<ICollection<(int X, int Y)>>();
    Parallel.For(0, 20000, i =>
    {
        var entry = new List<(int X, int Y)>(); // essentially, this is what's going on:
        var r = rng.Next(0, 3);                 // around 20k handlers return coordinates of pixels to redraw
        for (var j = 0; j < r; j++)             // sometimes there are null entries, sometimes 1, more often 2
        {                                       // all entries are added to concurrent bag
            entry.Add((j, j * j));
        }                         

        if (entry.Count == 0)     
            entry = null;         

        concurrenBag.Add(entry);  
    });

    var sw = Stopwatch.StartNew();
    var results = concurrenBag.ToList().AsParallel().Where(x => x != null).SelectMany(x => x).Distinct().ToList();  // this is where severe performance drops occur from time to time
    var time = sw.ElapsedMilliseconds;

    Console.WriteLine($"CB count: {concurrenBag.Count:00000}, result count: {results.Count:00}, time: {time:000}");
    //Thread.Sleep(1000);
}

此代码产生以下结果:

CB count: 20000, result count: 02, time: 032        <- this is fine, initialization and stuff       
CB count: 20000, result count: 02, time: 004
CB count: 20000, result count: 02, time: 014        <- this is not fine
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 004
CB count: 20000, result count: 02, time: 004
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 015        <- every couple of frames it happens again
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 019
CB count: 20000, result count: 02, time: 004
CB count: 20000, result count: 02, time: 004
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 014
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 004
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 008
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 004
CB count: 20000, result count: 02, time: 011
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 003
CB count: 20000, result count: 02, time: 004

我相信你明白了。在实际应用中,每次“好的”迭代大约需要 10-15 毫秒,而那些缓慢的迭代每 6-8 次迭代就会发生一次,最多需要 150 毫秒或类似的时间。

老实说,我认为我的业务逻辑出了点问题,但是您可以运行上面的示例并获得完全相同的结果。我现在猜测是我使用Parallel.ForAsParallel()ConcurrentBag 的方式有问题,但我不知道到底出了什么问题。

【问题讨论】:

  • 检查了垃圾收集器?
  • @TomTom 不,我没有;D
  • 您究竟想在实际应用程序中实现什么?我有一种感觉,你想在这里有一个并行的生产者/消费者模式,但最终首先收集了大量数据,然后再进行处理。这会给你 GC 压力......第二个循环是否应该不与第一个循环并行运行并在添加项目时对其进行处理?这将使您的内存占用更少。
  • @DmitryVolkov a ConcurrentBag 相当于并发集合。它适用于线程本地存储,这意味着您在访问由不同线程创建的任何存储桶时会付出代价。
  • 附带说明,ConcurrentBag&lt;T&gt; 是一个very specialized 集合。在这种情况下,ConcurrentQueue&lt;T&gt; 会更好地为您服务,因为它保留了入队项目的顺序(而且它也会稍微快一些)。

标签: c# performance linq task-parallel-library plinq


【解决方案1】:

您的问题是由 GC 暂停所有线程以执行其黑魔法引起的(我对其进行了分析,结果非常明显)。这样做的原因是您分配了一大堆 Lists 和 ConcurrentBags 和 ValueTuples (包装在列表中,因此最终在堆上)然后在下一个循环中丢弃. ConcurrentBag 超出范围,所有 Lists 也包含在其中。 ValueTuples 也是如此。

所以你想消除所有你可以做的分配,例如通过预先在堆上分配所需的存储空间,从而避免新的实例化。

以下代码应该让您了解如何实现这一点 - 它在语义上可能不是 100% 等效的,但我假设您无论如何都无法将其复制/粘贴到您的解决方案中,因为它基于简化示例:

// type that simply serves as a data container (replacing the ValueTuple)
private class Data : IEquatable<Data>
{
    private const int maxNumberOfRandom = 3;

    public readonly int[] Values1 = new int[maxNumberOfRandom];
    public readonly int[] Values2 = new int[maxNumberOfRandom];
    public bool IsNull { get; set; }

    public bool Equals(Data other)
    {
        return CompareArrays(Values1, other.Values1) && CompareArrays(Values2, other.Values2);
    }

    private static bool CompareArrays(int[] values, int[] otherValues)
    {
        for (var i = 0; i < maxNumberOfRandom; i++)
        {
            if (values[i] != otherValues[i])
            {
                return false;
            }
        }
        return true;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = Values1.GetHashCode();
            hashCode = (hashCode * 397) ^ Values2.GetHashCode();
            return hashCode;
        }
    }
}

static void Main(string[] args)
{
    const int count = 20000;

    var list = new List<Data>(count);
    // initialization loop to provision the required memory on the heap
    for (int i = 0; i < count; i++)
    {
        list.Add(new Data());
    }

    while (true)
    {
        var rng = new Random(1);

        Parallel.For(0, 20000, i =>
        {
            // Random isn't thread-safe: https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.7.2#the-systemrandom-class-and-thread-safety
            int r;
            lock (rng)
            {
                r = rng.Next(0, 3);
            }

            if (r == 0)
            {
                // we can do index-based access here so no need for locking
                list[i].IsNull = true;
            }
            else
            {
                // we can do index-based access here so no need for locking
                var data = list[i];
                data.IsNull = false;

                int j;
                for (j = 0; j < r; j++)
                {
                    data.Values1[j] = j;
                    data.Values2[j] = j * j;
                }
                Array.Clear(data.Values1, j, data.Values1.Length - j);
                Array.Clear(data.Values2, j, data.Values2.Length - j);
            }
        });

        var sw = Stopwatch.StartNew();
        var results = list.ToList().AsParallel().Where(x => !x.IsNull).Distinct().ToList();
        var time = sw.ElapsedMilliseconds;

        Console.WriteLine($"CB count: {list.Count:00000}, result count: {results.Count:00}, time: {time:000}");
    }
}

【讨论】:

    【解决方案2】:

    如果您在测量部分之前调用GC.Collect(),则问题基本消失。看来您在垃圾收集方面遇到了问题。尽量减少垃圾和不那么复杂的一次性结构。这是重新设计解决方案的一种方法:

    var results = new HashSet<(int X, int Y)>();
    object resultLockObj = new object();
    var rng = new Random(1);
    var sw = new Stopwatch();
    
    while (true)
    {
        results.Clear();
        sw.Restart();
    
        Parallel.For(0, 20000, i =>
        {
            var entry = new List<(int X, int Y)>(); // essentially, this is what's going on:
            var r = rng.Next(0, 3);                 // around 20k handlers return coordinates of pixels to redraw
            for (var j = 0; j < r; j++)             // sometimes there are null entries, sometimes 1, more often 2
            {                                       // all entries are added to concurrent bag
                entry.Add((i, j * j));
            }
    
            if (entry.Count == 0)
            {
                entry = null;
            }
    
            if (entry != null)
            {
                lock (resultLockObj)
                {
                    foreach (var x in entry)
                    {
                        results.Add(x);
                    }
                }
            }
        });
    
        var time = sw.ElapsedMilliseconds;
    
        Console.WriteLine($"Result count: {results.Count:00000}, time: {time:000}");
        //Thread.Sleep(1000);
    }
    

    编辑

    我做了些微改动。 (j, j * j) 现在是 (i, j * j),因此结果中没有重复项,消除它们后也没有性能提升。而不是每次我都清除它时创建 HashSet(这是你不能用 ConcurrentBag 做的)来进一步减少垃圾的产生。你是对的,元组是一个值,但问题出在其他地方。当您将列表添加到另一个结构中时,您会保留指向它们的指针,并且它们的消除更加困难。最好使用简单的短寿命结构。如果你能回收它们,那显然是最好的选择。

    【讨论】:

    • 嘿安东宁!感谢您的回答!看来性能下降已经减少了!在我接受您的答案之前,让我在实际应用程序中检查您的方法;D
    • 安东尼,有问题。在您的解决方案中,您正在使用 HashSet 消除性能下降,这依赖于它在重复项上的性能。确实,在上面的示例中,ConcurrentBag 中有 99.99% 的重复项,但这只是一个糟糕的示例。在实际应用中大约有 1% 的重复项,因此这次重新设计并没有什么特别之处。
    • 顺便说一下,Antonín,您说的是垃圾收集,但那些(int X, int Y) 不是valuetuple 吗?我以为它们不会被 GC 处理。
    猜你喜欢
    • 2014-02-15
    • 2014-02-18
    • 2020-05-19
    • 2014-12-25
    • 2012-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-03
    相关资源
    最近更新 更多