Многопоточная обработка изображений в C ++ - PullRequest
11 голосов
/ 28 ноября 2008

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

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

У меня есть несколько требований:

  • Размер исполняемого файла должен быть минимизирован. Другими словами, я не могу использовать массивные библиотеки. Какая самая легкая, переносимая библиотека потоков для C / C ++?
  • Размер исполняемого файла должен быть минимизирован. Я думал о наличии функции forEachRow (fp *), которая запускает поток для каждой строки, или даже forEachPixel (fp *), где fp работает с одним пикселем в своем собственном потоке. Какой лучше?
    • Должен ли я использовать нормальные функции или функторы или функционоиды или некоторые лямбда-функции или ... что-то еще?
    • В некоторых операциях используются оптимизации, для которых требуется информация из предыдущего обработанного пикселя. Это делает forEachRow выгодным. Будет ли лучше использовать forEachPixel даже с учетом этого?
  • Нужно ли блокировать массивы только для чтения и только для записи?
    • Входные данные считываются только из, но многие операции требуют ввода более одного пикселя в массиве.
    • Выход выводится только один раз на пиксель.
  • Скорость также важна (конечно), но оптимизация размера исполняемого файла имеет преимущество.

Спасибо.

Дополнительная информация по этой теме для любопытных: Библиотеки распараллеливания C ++: OpenMP против потоковых строительных блоков

Ответы [ 16 ]

13 голосов
/ 29 ноября 2008

Не приступайте к заправке слегка! Условия гонки могут быть основной болью в заднице, чтобы разобраться. Особенно, если у вас нет большого опыта работы с потоками! (Вас предупредили: вот драконы! Большие волосатые недетерминированные, невозможно надежно воспроизводить драконов!)

Вы знаете, что такое тупик? Как насчет Livelock?

Это говорит ...


Как ckarmann и другие уже предложили: Используйте модель рабочей очереди. Один поток на ядро ​​процессора. Разбейте работу на N кусков. Сделайте куски достаточно большими, как много рядов. Когда каждый поток становится свободным, он забирает следующий рабочий блок из очереди.

В самой простой версии IDEAL у вас есть N ядер, N потоков и N частей проблемы, причем каждый поток с самого начала точно знал, что он собирается делать.

Но на практике это обычно не происходит из-за накладных расходов на запуск / остановку потоков. Вы действительно хотите, чтобы потоки уже были созданы и ожидали действий. (Например, через семафор.)

Сама модель рабочей очереди довольно мощная. Это позволяет вам распараллеливать такие вещи, как быстрая сортировка, которая обычно не распределяется между N нитями / ядрами корректно.


Больше потоков, чем ядер? Вы просто тратите впустую. Каждый поток имеет накладные расходы. Даже при # threads = # ядрах вы никогда не достигнете идеального коэффициента ускорения Nx.

Один поток на строку будет очень неэффективным! Одна нить на пиксель? Я даже не хочу думать об этом. (Этот попиксельный подход имеет гораздо больше смысла при игре с векторизованными процессорами, как это было на старых Crays. Но не с потоками!)


Библиотеки? Какая у тебя платформа? Под Unix / Linux / g ++ я бы предложил pthreads и семафоры. (Pthreads также доступен в Windows со слоем совместимости с Microsoft. Но, черт возьми. Я не очень верю в это! Cygwin может быть лучшим выбором там.)

Под Unix / Linux, man :

* pthread_create, pthread_detach.
* pthread_mutexattr_init, pthread_mutexattr_settype, pthread_mutex_init,
* pthread_mutexattr_destroy, pthread_mutex_destroy, pthread_mutex_lock,
* pthread_mutex_trylock, pthread_mutex_unlock, pthread_mutex_timedlock.
* sem_init, sem_destroy, sem_post, sem_wait, sem_trywait, sem_timedwait.

Некоторым людям нравятся переменные условия pthreads. Но я всегда предпочитал семафоры POSIX 1003.1b. Они обрабатывают ситуацию, когда вы хотите дать сигнал другому потоку ДО , он начинает ждать немного лучше. Или где другой поток сигнализируется несколько раз.

О, и сделайте себе одолжение: оберните ваши потоки / мьютекс / семафор pthread в пару классов C ++. Это сильно упростит дело!


Нужно ли блокировать массивы только для чтения и только для записи?

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

Письмо почти такое же. Обычно, пока только один поток записывает в каждую конкретную область памяти, все в порядке. Но есть случаи, когда это не так!

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

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

Если вы не говорите о гигабайтах данных, биты памяти очень быстрые. Та пара микросекунд времени исполнения просто не стоит кошмара отладки.

Если бы вы разделяли одну общую область данных между потоками, используя мьютексы, неэффективность столкновения / ожидания мьютексов накапливалась бы и снижала вашу эффективность!


Посмотрите, чистые границы данных - это сущность хорошего многопоточного кода. Когда ваши границы не ясны, вот когда вы попадаете в беду.

Точно так же важно, чтобы все на границе было мьютексированным! И чтобы области мьютекса были короткими!

Старайтесь не блокировать более одного мьютекса одновременно. Если вы блокируете более одного мьютекса, всегда блокируйте их в одном и том же порядке!

По возможности используйте мьютексы с проверкой ошибок или рекурсивными. БЫСТРЫЕ мьютексы просто напрашиваются на неприятности, с очень небольшим фактическим (измеренным) увеличением скорости.

Если вы попали в тупиковую ситуацию, запустите его в gdb, нажмите Ctrl-C, посетите каждый поток и проследите. Таким образом, вы можете быстро найти проблему. (Livelock гораздо сложнее!)


Последнее предложение: создайте его однопоточным, затем начните оптимизацию. В одноядерной системе вы можете получить больше скорости от таких вещей, как foo [i ++] = bar ==> * (foo ++) = bar, чем от многопоточности.


Приложение: Что я сказал о , обеспечивающих короткие области мьютекса выше? Рассмотрим два потока: (учитывая глобальный общий мьютекс-объект класса Mutex.)

/*ThreadA:*/ while(1){  mutex.lock();  printf("a\n");  usleep(100000); mutex.unlock(); }
/*ThreadB:*/ while(1){  mutex.lock();  printf("b\n");  usleep(100000); mutex.unlock(); }

Что будет?

В моей версии Linux один поток будет работать непрерывно, а другой голодать. Очень редко они меняются местами, когда происходит изменение контекста между mutex.unlock () и mutex.lock ().


Приложение: В вашем случае это вряд ли проблема. Но с другими проблемами можно заранее не знать, сколько времени займет выполнение определенного рабочего блока. Разбивка проблемы на 100 частей (вместо 4 частей) и использование рабочей очереди для разделения ее на 4 ядра сглаживает такие расхождения.

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

10 голосов
/ 28 ноября 2008

Если ваш компилятор поддерживает OpenMP (я знаю, что VC ++ 8.0 и 9.0 , как и gcc), это может сделать такие вещи намного проще.

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

OpenMP позволяет вам легко адаптировать количество созданных потоков к числу доступных процессоров. Его использование (особенно в случаях обработки данных) часто подразумевает простое добавление нескольких #pragma omp в существующий код и предоставление компилятору возможности создавать потоки и синхронизацию.

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

Для OpenMP нет необходимости делать что-то особенное в отношении функторов / функциональных объектов. Напишите это любым способом, который имеет для вас наибольшее значение. Вот пример обработки изображения из Intel (переводит rgb в оттенки серого):

#pragma omp parallel for
for (i=0; i < numPixels; i++)
{
   pGrayScaleBitmap[i] = (unsigned BYTE)
       (pRGBBitmap[i].red * 0.299 +
        pRGBBitmap[i].green * 0.587 +
        pRGBBitmap[i].blue * 0.114);
}

Это автоматически разделяется на столько потоков, сколько у вас процессоров, и назначает часть массива каждому потоку.

6 голосов
/ 28 ноября 2008

Я бы порекомендовал boost::thread и boost::gil (общее изображение libray). Поскольку там задействовано довольно много шаблонов, я не уверен, что размер кода все еще будет приемлем для вас. Но это часть повышения, поэтому, наверное, стоит посмотреть.

2 голосов
/ 28 ноября 2008

Как идея левого поля ...

На каких системах вы это используете? Думал ли ты об использовании графического процессора на своих ПК?

У Nvidia есть CUDA API для такого рода вещей

1 голос
/ 28 сентября 2012

Проверьте Создание сети обработки изображений пошаговое руководство по MSDN, которое объясняет, как использовать библиотеку параллельных шаблонов для создания параллельного конвейера обработки изображений.

Я бы также предложил Boost.GIL , который генерирует высокоэффективный код. Для простого многопоточного примера, проверьте gil_threaded от Victor Bogado. Сеть обработки изображений с использованием Dataflow.Signals и Boost.GIL также объясняет интересующую модель потока данных.

1 голос
/ 29 ноября 2008

Чтобы оптимизировать простые преобразования изображений, вам лучше использовать SIMD векторную математику, чем пытаться выполнять многопоточность вашей программы.

1 голос
/ 28 ноября 2008

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

1 голос
/ 28 ноября 2008

Не думаю, что вы хотите иметь одну ветку на строку. Строк может быть много, и вы потратите много ресурсов памяти / ЦП, просто запуская / уничтожая потоки и переключая ЦП с одного на другой. Более того, если у вас есть P-процессоры с ядром C, вы, вероятно, не получите большого выигрыша с более чем потоками C * P.

Я бы посоветовал вам использовать определенное количество клиентских потоков, например N потоков, и использовать основной поток вашего приложения для распределения строк по каждому потоку, или они могут просто получить инструкцию из «очереди заданий». Когда поток завершил работу со строкой, он может проверить в этой очереди другую строку.

Что касается библиотек, вы можете использовать boost :: thread, который является довольно переносимым и не слишком тяжелым.

0 голосов
/ 09 января 2015

Вы также можете использовать библиотеки, такие как IPP или Cassandra Vision C ++ API, которые в основном гораздо более оптимизированы, чем ваш собственный код.

0 голосов
/ 15 января 2010

Я думаю, что карта / сокращение рамки будет идеальной вещью для использования в этой ситуации. Вы можете использовать потоковую передачу Hadoop для использования вашего существующего приложения C ++.

Просто внедрите карту и уменьшите количество рабочих мест.

Как вы сказали, вы можете использовать манипуляции на уровне строк в качестве задачи карты и комбинировать манипуляции на уровне строк с конечным изображением в задаче сокращения.

Надеюсь, это полезно.

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