Иногда производительность падает в запросах linq к одновременной сумке - PullRequest
0 голосов
/ 01 октября 2018

В настоящее время я изучаю некоторые серьезные падения производительности в приложении.

Падения производительности странного рода - несколько итераций подряд работают довольно быстро, но есть одна итерация, которая требует гораздо больше времени дляполный.Это приложение работает с графикой, поэтому выглядит очень раздражающим.

Пожалуйста, взгляните на следующий код.

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.For, AsParallel() или ConcurrentBag, но я понятия не имею, что именно не так.

Ответы [ 2 ]

0 голосов
/ 02 октября 2018

Ваша проблема вызвана тем, что GC приостановил все потоки, чтобы выполнить свою черную магию (я профилировал ее, и результаты были очень очевидны).Причиной этого является то, что вы выделяете один ад из множества List s и ConcurrentBag s и ValueTuple s (в любом случае обернутых в списке, так что в конечном итоге в куче), которые затем выбрасываете при следующем цикле,ConcurrentBag выходит из области видимости, все содержащиеся в нем List тоже.И то же самое для ValueTuple s.

. Таким образом, вы хотите исключить все те выделения, которые вы можете сделать, например, выделив требуемое хранилище в куче заранее, избегая при этом новых экземпляров.

следующий код должен дать вам представление о том, как этого можно достичь - он, вероятно, не на 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}");
    }
}
0 голосов
/ 01 октября 2018

Если вы позвоните 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) для дальнейшего сокращения производства мусора.Вы правы, что кортеж - это значение, но проблема в другом.Когда вы добавляете списки в другую структуру, вы сохраняете указатель на них, и их устранение становится более трудным.Лучше использовать простые недолговечные структуры.И если вы можете утилизировать их, это, очевидно, лучший вариант.

...