Низкая загрузка ЦП с Parallel.ForEach (...) - PullRequest
3 голосов
/ 04 августа 2020

Я пытаюсь распараллелить задачу, но не получаю хорошей производительности. Максимум 50% загрузки процессора.

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

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Papa
{
    class Program
    {
        static void Main(string[] args)
        {
            // Build source
            var random = new Random();
            var alphabet = "abcdefghijklmnopqrstuvwxyz";

            Console.WriteLine("Building data source");
            var source = new List<string[]>();
            foreach (var i in Enumerable.Range(0, 5000000))
            {
                var words = random.Next(3, 50);
                var line = new List<string>();

                for (var j = 0; j < words; ++j)
                {
                    line.Add(
                        string.Concat(
                            Enumerable
                                .Range(0, random.Next(3, 9))
                                .Select(w => alphabet[random.Next(0, alphabet.Length - 1)])
                        )
                    );
                }

                source.Add(line.ToArray());
            }

            // Process source
            Console.WriteLine("Processing source");
            var processed = new List<string[]>();
            Parallel.ForEach(
                source,
                () => new List<string[]>(),
                (line, loop, local) =>
                {
                    var processedLine = new List<string>();
                    for (var i = 0; i < line.Count(); ++i)
                    {
                        processedLine.Add(string.Concat(line[i].Reverse()));
                    }

                    local.Add(processedLine.ToArray());
                    return local;
                },
                partitionResult =>
                {
                    lock (processed)
                    {
                        processed.AddRange(partitionResult);
                    }
                });
        }
    }
}

1 Ответ

4 голосов
/ 04 августа 2020

Parallel.ForEach будет использовать столько потоков, сколько планировщик предоставляет по умолчанию ( ссылка ). Имейте в виду, что в документации указано:

Выполняет операцию foreach (For Each в Visual Basi c) на Partitioner, в которой итерации могут выполняться параллельно, а параметры l oop могут быть настроены.

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

После запуска вашего кода я обнаружил следующее (при нажатии на параллельную часть):

enter image description here

As you can see it already utilized 15Gb of memory but it's running on all cores. I'm quite sure you run into memory/GC bottlenecks rather than CPU bottlenecks.

I run some additional performance analysis. This is what I found (String::Concat can be ignored because it's part of the initialization):

enter image description here

Line 45 is taking up ~50% of the CPU time.

введите описание изображения здесь

Я немного поигрался с кодом и внес следующие изменения:

Parallel.ForEach(
             source,
             () => new List<string[]>(),
             (line, loop, local) =>
             {
                 var length = line.Length;
                 var processedLine = new string[length];
                 for (var i = 0; i < length; ++i)
                 {
                     processedLine[i] = line[i].Reverse() + "";
                 }

                 local.Add(processedLine);
                 return local;
             },
             partitionResult =>
             {
                 lock (processed)
                 {
                     processed.AddRange(partitionResult);
                 }
             });

С удвоенным размером данных (10000000 ~ 32 ГБ данных) это показывает как минимум небольшой пик использования ЦП, но он выполняется менее чем за 7 секунд , поэтому я не совсем уверен, что TaskManager действительно правильно получает пик. Тем не менее, это также не решает проблему, и G C также работает как сумасшедший.

Моим «окончательным» выводом было бы то, что это либо (или несколько):

  • G C
  • Память
  • Кеш промахов

что вас сдерживает.

На мой взгляд, это приятно пример того, почему распараллеливание не даст в x -кратное увеличение производительности, даже если вы можете использовать x -потоки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...