Многопоточная программа НЕ использует 100% CPU для дорогих IEnumerables - PullRequest
0 голосов
/ 02 сентября 2018

Многопоточность с IEnumerables , которые оцениваются несколько раз параллельно и являются дорогостоящими для оценки, не использует 100% CPU . Примером является функция Aggregate () в сочетании с Concat ():

// Initialisation.
// Each IEnumerable<string> is made so that it takes time to evaluate it
// everytime when it is accessed.
IEnumerable<string>[] iEnumerablesArray = ...  

// The line of the question (using less than 100% CPU):
Parallel.For(0, 1000000, _ => iEnumerablesArray.Aggregate(Enumerable.Concat).ToList());

Вопрос: Почему параллельный код, в котором IEnumerables несколько раз параллельно оцениваются, не использует 100% ЦП? Код не использует блокировки или ожидает, поэтому это поведение является неожиданным. Полный код для симуляции это в конце поста.


Примечания и правки:

  • Интересный факт: Если код
    Enumerable.Range(0, 1).Select(__ => GenerateLongString())
    полного кода в конце изменяется на
    Enumerable.Range(0, 1).Select(__ => GenerateLongString()).ToArray().AsEnumerable(),
    , затем инициализация занимает секунды, а после этого процессор используется до 100% (проблем не возникает)
  • Интересный факт2: (из комментария) Когда метод GenerateLongString() становится менее тяжелым для GC и более интенсивным для CPU, тогда CPU переходит на 100%. Так что причина связана с реализацией этого метода. Но, что интересно, если текущая форма GenerateLongString() вызывается без IEnumerable, CPU также переходит на 100%:
    Parallel.For(0, int.MaxValue, _ => GenerateLongString());
    Так что тяжесть GenerateLongString() здесь не единственная проблема.
  • Факт 3: (из комментария) Предлагаемый Параллельный визуализатор показал, что потоки проводят большую часть своего времени в сети
    clr.dll!WKS::gc_heap::wait_for_gc_done,
    в ожидании завершения GC. Это происходит внутри string.Concat() из GenerateLongString().
  • То же поведение наблюдается при ручном запуске нескольких Task.Factory.StartNew () или Thread.Start ()
  • Такое же поведение наблюдается в Win 10 и Windows Server 2012
  • Такое же поведение наблюдается на реальной машине и виртуальной машине
  • Релиз против отладки не имеет значения.
  • .Net версия протестирована: 4.7.2

Полный код:

class Program
{
    const int DATA_SIZE = 10000;
    const int IENUMERABLE_COUNT = 10000;

    static void Main(string[] args)
    {
        // initialisation - takes milliseconds
        IEnumerable<string>[] iEnumerablesArray = GenerateArrayOfIEnumerables();

        Console.WriteLine("Initialized");

        List<string> result = null;

        // =================
        // THE PROBLEM LINE:
        // =================
        // CPU usage of next line:
        //    - 40 % on 4 virtual cores processor (2 physical)
        //    - 10 - 15 % on 12 virtual cores processor
        Parallel.For(
            0, 
            int.MaxValue, 
            (i) => result = iEnumerablesArray.Aggregate(Enumerable.Concat).ToList());

        // just to be sure that Release mode would not omit some lines:
        Console.WriteLine(result);
    }

    static IEnumerable<string>[] GenerateArrayOfIEnumerables()
    {
        return Enumerable
              .Range(0, IENUMERABLE_COUNT)
              .Select(_ => Enumerable.Range(0, 1).Select(__ => GenerateLongString()))
              .ToArray();
    }

    static string GenerateLongString()
    {
        return string.Concat(Enumerable.Range(0, DATA_SIZE).Select(_ => "string_part"));
    }
}

1 Ответ

0 голосов
/ 04 сентября 2018

Тот факт, что ваши потоки заблокированы на clr.dll!WKS::gc_heap::wait_for_gc_done, показывает, что сборщик мусора является узким местом вашего приложения. Насколько это возможно, вы должны попытаться ограничить количество выделений кучи в вашей программе, чтобы уменьшить нагрузку на gc.

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

...