到并发包的linq查询中偶尔的性能下降

时间:2018-10-01 11:28:22

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

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

性能下降是一种奇怪的现象-连续几次迭代的工作速度非常快,但是一次迭代需要花费更多的时间才能完成。 该应用程序可以处理图形,因此看起来很烦人。

请看下面的代码。

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的方式有问题,但是我不知道到底是哪里出了问题。

2 个答案:

答案 0 :(得分: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进行的操作),以进一步减少垃圾产生。您是对的,元组是一个值,但是问题出在其他地方。当您将列表添加到另一个结构中时,您将保持指向它们的指针,并且将其删除会更加困难。最好使用简单的短期结构。而且,如果您可以回收它们,那显然是最好的选择。

答案 1 :(得分:1)

您的问题是由GC挂起所有线程以执行其不可思议的操作引起的(我对此进行了分析,结果非常明显)。这样做的原因是,您要分配很多<th> <input class="form-control partQuantity partQtyInput" data-outrightp="$12.57" type="text" placeholder="1"> </th> <td>000-126042</td> <td>Nipple Filter</td> <td>$12.57</td> <td class="extendedPrice">$12.57</td> ListConcurrentBag(分配在列表中,所以最终还是放在堆上)的地狱,然后抛弃下一个循环。 ValueTuple超出范围,其中也包含所有ConcurrentBag。对于List也是如此。

因此,您要消除所有可以执行的分配,例如通过预先在堆上分配所需的存储空间,从而避免了新的实例化。

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

ValueTuple