Полное использование процессора для циклов Parallel.For - PullRequest
2 голосов
/ 20 сентября 2019

Я пишу приложение WPF, которое обрабатывает поток данных изображения с ИК-камеры.Приложение использует библиотеку классов для таких шагов обработки, как изменение масштаба или раскраска, которые я также пишу сам.Этап обработки изображения выглядит примерно так:

ProcessFrame(double[,] frame)
{
  int width = frame.GetLength(1);
  int height = frame.GetLength(0);
  byte[,] result = new byte[height, width];
  Parallel.For(0, height, row =>
  {
    for(var col = 0; col < width; ++col)
      ManipulatePixel(frame[row, col]);
  });
}

Кадры обрабатываются задачей, которая выполняется в фоновом режиме.Проблема в том, что в зависимости от того, насколько дорогостоящим является конкретный алгоритм обработки (ManipulatePixel()), приложение больше не может справиться с частотой кадров камеры.Однако я заметил, что, несмотря на то, что я использую параллельные циклы for, приложение просто не будет использовать весь доступный ЦП - на вкладке производительности диспетчера задач показано, что загрузка ЦП составляет около 60-80%.

Я использовалте же алгоритмы обработки в C ++ ранее, используя циклы concurrency::parallel_for из библиотеки параллельных шаблонов.Код C ++ использует весь процессор, который он может получить, как я и ожидал, и я также попытался PInvoking C ++ DLL из моего кода C #, выполняя тот же алгоритм, который работает медленно в библиотеке C # - он также использует весь процессордоступная мощность, использование процессора практически на 100% практически все время, и нет никаких проблем с тем, чтобы не отставать от камеры.

Аутсорсинг кода в C ++ DLL с последующим перенаправлением его обратно в C # является дополнительнымхлопот, которых я, конечно, предпочел бы избежать.Как мне сделать, чтобы мой код C # фактически использовал весь потенциал процессора?Я попытался увеличить приоритет процесса следующим образом:

  using (Process process = Process.GetCurrentProcess())
    process.PriorityClass = ProcessPriorityClass.RealTime;

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

ParallelOptions parallelOptions = new ParallelOptions();
parallelOptions.MaxDegreeOfParallelism = Environment.ProcessorCount;

, а затем передать это в цикл Parallel.For(), это никак не повлияло, но я полагаю, что это не удивительно, посколькунастройки по умолчанию уже должны быть оптимизированы.Я также попытался установить это в конфигурации приложения:

<runtime>
  <Thread_UseAllCpuGroups enabled="true"></Thread_UseAllCpuGroups>
  <GCCpuGroup enabled="true"></GCCpuGroup>
  <gcServer enabled="true"></gcServer>
</runtime>

, но на самом деле это заставляет его работать еще медленнее.


EDIT : Блок кода ProcessFrame Iцитируется изначально было на самом деле не совсем правильно.То, что я делал в то время, было:

ProcessFrame(double[,] frame)
{
  byte[,] result = new byte[frame.GetLength(0), frame.GetLength(1)];
  Parallel.For(0, frame.GetLength(0), row =>
  {
    for(var col = 0; col < frame.GetLength(1); ++col)
      ManipulatePixel(frame[row, col]);
  });
}

Извините за это, я перефразировал код в то время, и я не осознавал, что это настоящая ловушка, которая дает разные результаты.С тех пор я изменил код на то, что я изначально написал (то есть переменные ширины и высоты, установленные в начале функции, и свойства длины массива запрашиваются только один раз, а не в условных выражениях цикла for).Спасибо @Seabizkit, твой второй комментарий вдохновил меня попробовать это.Фактически изменение уже делает код работающим заметно быстрее - я не осознавал этого, потому что C ++ не знает двумерных массивов, поэтому мне все равно пришлось передавать размеры в пикселях как отдельные аргументы.Достаточно ли быстро, как есть, я пока не могу сказать.

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

Ответы [ 3 ]

4 голосов
/ 20 сентября 2019

Мне нужно было бы иметь весь ваш код и иметь возможность запускать его локально, чтобы диагностировать проблему, потому что ваша публикация лишена деталей (мне нужно было бы увидеть внутри вашей функции ManipulatePixel, а также кодчто вызывает ProcessFrame).но вот несколько общих советов, которые применимы в вашем случае.

  • 2D-массивы в .NET значительно медленнее, чем 1D-массивы и шахматные массивы, даже сегодня в .NET Core - это давняя ошибка.

  • Совместное использование буферов памяти между потоками усложняет для системы оптимизациюбезопасный доступ к памяти.

  • Избегать выделениядобавление нового буфера для каждого обнаруженного кадра - если у кадра ограниченный срок службы, рассмотрите возможность использования буферов многократного использования с использованием пула буферов.
  • Рассмотрите возможность использования функций SIMD и AVX в .NET.В то время как современные компиляторы C / C ++ достаточно умны, чтобы компилировать код для использования этих инструкций, .NET JIT не так уж и хорош - но вы можете делать явные вызовы в инструкции SMID / AVX, используя типы с поддержкой SIMD (вам нужно будет использовать .NET Core 2.0 или более позднюю версию для лучшей ускоренной функциональности)

  • Кроме того, вместо копирования отдельных байтов или скалярных значений внутри цикла for в C #, вместо этогорассмотрите возможность использования Buffer.BlockCopy для массовых операций копирования (так как они могут использовать функции аппаратного копирования памяти).

  • Относительно вашего наблюдения «80% использования ЦП» - если у вас есть цикл вЗатем запрограммируйте, что будет вызывать 100% загрузку ЦПУ в пределах временных интервалов, предоставляемых операционной системой - если вы не видите 100% использования, тогда ваш код:

    • Ваш код на самом деле работает на быстрее , чем в режиме реального времени (это хорошо!) - (если вы не уверены, что ваша программа не может следить за вводом?)
    • Тема ваших кодов (или темы) заблокированачем-то, например блокирующим вызовом ввода-вывода или неуместным Thread.Sleep.Используйте такие инструменты, как ETW, чтобы увидеть, что делает ваш процесс, когда вы считаете, что он должен быть привязан к процессору.
    • Убедитесь, что вы не используете lock (Monitor) вызовов или не используете другие потоки или синхронизацию памятипримитивы.
2 голосов
/ 20 сентября 2019

Эффективность имеет значение (это не true- [PARALLEL], но может, но не обязательно, получить выгоду от "просто " - [CONCURRENT] работа

ЛУЧШИЙ, но довольно трудный путь, если конечная производительность ДОЛЖНА:

встроенная сборка, оптимизированная согласноРазмеры строк кэша в иерархии ЦП и индексирование продолжаются в соответствии с фактической структурой памяти 2D-данных { column-wise | row-wise }. Поскольку не упоминается 2D-преобразование ядра, вашему процессу не нужно «касаться» каких-либо топологических соседейиндексация может происходить в любом порядке «через» оба диапазона 2D-домена, и ManipulatePixel() может стать более эффективным при преобразовании скорее блоков пикселей, вместо того, чтобы нести все накладные расходы для вызова процесса только для каждого изолированногоatomicised-1px (ILP + эффективность кэширования на вашей стороне).

Учитывая целевое семейство процессоров производственной платформы, лучше всего использовать (block-SIMD) векторизованные инструкции, доступные от AVX2, лучший код AVX512.ты самый вероятныйЗнайте, может использовать C / C ++ с использованием AVX-intrinsics для оптимизации производительности с проверкой сборки и, наконец, «скопировать» лучшую результирующую сборку для вашей сборки C #.Ничто не будет работать быстрее.Трюки с отображением привязки к ядру процессора и выселением / резервированием действительно являются последним средством, однако они могут действительно помочь в почти жестких производственных условиях в реальном времени (хотя жесткие R / T-системы редко разрабатываютсяв экосистеме с недетерминированным поведением)

НЕДОРОГО, шаг в несколько секунд:

Проверьте и сравните время выполнения для каждой серии кадров с перевернутой композицией перемещения более- "дорогая "-часть, Parallel.For(...{...}) внутри for(var col = 0; col < width; ++col){...}, чтобы увидеть изменение стоимости экземпляров Parallel.For() инструментовки.

Далее, если вы идете этим дешевым путем, подумайте о перефакторингеManipulatePixel() по крайней мере для использования блока данных, выровненного по схеме хранения данных и кратного длине строки кэша (для попаданий в кэш ~ 0.5 ~ 5 [ns] улучшен доступ к стоимости памяти, поскольку~ 100 ~ 380 [ns] в противном случае - здесь желание распределить работу (худшая на 1 пиксель) по всем ядрам NUMA-CPU приведет к тому, что вы потратите гораздо больше времени из-за увеличенных задержек доступа длякросс-NUMA- (нелокальные) адреса памяти, и, кроме того, никогда не используя повторно кэшированные дорогостоящие блоки данных, вы сознательно платите чрезмерные затраты из-за кросс-NUMA- (нелокальных) выборок памяти (из которых вы "используйте «всего 1px» и «выбрасывайте» все остальное в кэшированном блоке (так как эти пиксели будут повторно выбираться и манипулироваться в каком-то другом ядре ЦП в другое время - тройная трата времени - извините, что упомянулэто явно, но при бритье каждого возможного [ns] это не может произойти в производственном конвейере ))


В любом случае, позвольте мне пожелать вам настойчивости и удачи на ваших шагах вперед, чтобы получитьнужной эффективности обратно на вашу сторону.

0 голосов
/ 23 сентября 2019

Вот что я в итоге сделал, в основном основываясь на ответе Дая:

  • удостоверился, что запросил размеры в пикселях изображения один раз в начале функций обработки, а не в условном выражении цикла for.Казалось бы, с параллельными циклами это создает конкурентный доступ к этим свойствам из множества потоков, что заметно замедляет процесс.
  • убрал выделение выходных буферов в функциях обработки.Теперь они возвращают void и принимают выходной буфер в качестве аргумента.Вызывающая сторона создает только один буфер для каждого шага обработки изображения (фильтрация, масштабирование, цветность), который не изменяется по размеру, а перезаписывается с каждым кадром.формат ushort (то, что камера изначально выплевывает) был преобразован в двойной (фактические значения температуры).Вместо этого обработка применяется к необработанным данным напрямую.При необходимости будет проведено преобразование к фактическим температурам позже.

Я также безуспешно пытался использовать одномерные массивы вместо 2D, но на самом деле разницы в производительности нет.Я не знаю, связано ли это с тем, что упомянутая Dai ошибка была исправлена ​​в это время, но я не смог подтвердить, что 2D-массивы медленнее, чем 1D-массивы.

Вероятно, также стоит упомянуть функцию ManipulatePixel ()в моем первоначальном посте это был скорее заполнитель, чем реальный вызов другой функции.Вот более точный пример того, что я делаю с фреймом, включая сделанные мной изменения:

private static void Rescale(ushort[,] originalImg, byte[,] scaledImg, in (ushort, ushort) limits)
{
  Debug.Assert(originalImg != null);
  Debug.Assert(originalImg.Length != 0);
  Debug.Assert(scaledImg != null);
  Debug.Assert(scaledImg.Length == originalImg.Length);

  ushort min = limits.Item1;
  ushort max = limits.Item2;
  int width = originalImg.GetLength(1);
  int height = originalImg.GetLength(0);

  Parallel.For(0, height, row =>
  {
    for (var col = 0; col < width; ++col)
    {
      ushort value = originalImg[row, col];
      if (value < min)
        scaledImg[row, col] = 0;
      else if (value > max)
        scaledImg[row, col] = 255;
      else
        scaledImg[row, col] = (byte)(255.0 * (value - min) / (max - min));
    }
  });
}

Это всего лишь один шаг, а некоторые другие гораздо сложнее, но подход будет аналогичным.

Некоторые из упомянутых вещей, таких как SIMD / AVX или ответ user3666197, к сожалению, сейчас выходят далеко за рамки моих возможностей, поэтому я не могу это проверить.

Все еще относительно легко поставить достаточную нагрузку на обработкув поток, чтобы увеличить частоту кадров, но для моего приложения производительности должно быть достаточно.Спасибо всем, кто внес свой вклад, я отмечу ответ Дая как принятый, потому что я нашел его наиболее полезным.

...