В результате длительного использования даже самых лучших программных продуктов постепенно выявляются те или иные недостатки.
Библиотека не стала исключением Примитивы производительности Intel (IPP).
К моменту выхода версии 8.0 возникло несколько проблем, некоторые из которых были связаны с функциями обработки 2D-изображений.
Для их решения в IPP 8.0 многие функции обработки изображений сведены в общий шаблон, позволяющий обрабатывать изображения поблочно ( плитка ), и, следовательно, эффективно распараллеливать код, содержащий вызовы функций IPP, на уровне приложения.
Новый API соответствующих функций IPP поддерживает границы нескольких типов, не использует внутреннее динамическое выделение памяти, позволяет разбивать изображения на фрагменты произвольного размера и обрабатывать эти фрагменты самостоятельно; упрощает использование и повышает производительность ряда функций.
В этой статье подробно обсуждается новый API и приводятся примеры использования.
1 Причины перехода на новый API Как уже говорилось выше, командой разработчиков IPP была проделана большая работа по изменению интерфейса двумерных функций и приведению его к единому стандарту «API с поддержкой границ» («новый API»).
За что? Чтобы ответить на этот вопрос, опишем проблемы, которые присутствовали в предыдущих версиях.
1.1 Проблемы эффективности использования OpenMP в IPP
Итак, первая проблема — как использовать IPP в многопоточных приложениях.Иногда задачу параллельной обработки изображений эффективнее решить на верхнем уровне, т.е.
на уровне приложения.
В этом случае приложение само разбивает изображение на блоки и создает потоки для их параллельной обработки.
Многопоточная версия IPP использует OpenMP с отключенным вложенным параллелизмом.
вложенная резьба ).
Если приложение использует другой инструмент многопоточности или даже OpenMP, но другой версии, то при вызове функции IPP из потока такого приложения по очереди будет создано несколько потоков, как показано на рис.
1.
Рисунок 1. Двухуровневое распараллеливание (вложенные потоки)
Такое двухуровневое распараллеливание вместо желаемого ускорения может привести к снижению производительности (на английском языке).
переподписка ), что можно объяснить тем, что различные инструменты распараллеливания работают одновременно и конкурируют за ресурсы — очереди, семафоры, физические потоки, выталкивают данные из конкурирующих потоков из кэша и т. д. Пользовательское приложение, в отличие от IPP, имеет информацию о том, как полученное изображение будет обрабатываться дальше, и может формировать параллельные блоки таким образом, что оставшиеся в кэше данные используются после предыдущего этапа обработки, а не считываются из памяти (в Английский).
местоположение кэша данных ).
Поясним подробнее, что это значит. IPP чаще всего используется в клиентских машинах — настольных компьютерах, ноутбуках, топология которых в целом выглядит так: Рис.
2
Рис.
2: Современная топология аппаратного обеспечения, нумерация потоков аппаратного обеспечения.
IPP также используется в сегменте HPC, и предполагается, что разница между системами HPC и клиентскими системами заключается только в количестве X-процессоров.
Нумерация потоков на рисунке произвольная и узнать физический номер потока можно через идентификатор его APIC (на английском языке).
Усовершенствованный программируемый контроллер прерываний ), который уникален для каждого потока.
Соответствие между номером логического и физического потока назначается операционной системой или используемым инструментом распараллеливания.
Если вы запустите задачу с интенсивными вычислениями в системе Windows и посмотрите ее в диспетчере задач, вы заметите, что система иногда переключает ее с одного ядра на другое.
Следовательно, нет никакой гарантии, что поток будет выполняться на одном и том же ядре в двух последовательных параллельных регионах.
Для обозначения соответствия потока с определенным логическим номером и физическим (англ.
HW-резьба от слова аппаратное обеспечение ) Для потока есть специальный термин - аффинити.
Если привязка установлена, операционная система будет запускать логический поток в последовательных параллельных регионах одного и того же потока аппаратного обеспечения.
В идеальной, полностью чистой системе указание сходства помогло бы решить проблемы с производительностью, связанные с переключением логических потоков с одного HW-потока на другой.
Однако в реальной современной системе выполняются сотни, а возможно, и тысячи процессов.
В любой момент каждый такой процесс можно вывести из состояния сна и запустить в каком-нибудь HW-потоке.
Предположим, что в каком-то приложении тщательно рассчитывался объем вычислений для каждого потока, и при этом в соответствии с установленной affinity каждый поток приложения запускался на соответствующем ему HW-потоке.
На картинке — зелёный.
Через некоторое время операционная система может запустить другой процесс, например, в HW-потоке №3, который показан красным.
Поскольку все потоки первого приложения жестко привязаны к HW-потокам, поток №3 будет ждать освобождения ресурсов другим процессом, и операционная система не сможет переключить его с HW-потока №3, хотя потоки №0 -2, возможно, уже завершен и будет простаивать.
Рассматриваем идеальную ситуацию и видим, что в первом случае фиксированной аффинити будет потрачено 9 условных единиц времени, а в случае, когда аффинити не указана, будет потрачено 7 единиц.
Таким образом, конечным результатом этой фиксированной настройки привязки является то, что производительность в некоторых случаях будет хорошей, а в других - очень плохой.
Теперь давайте рассмотрим, какие проблемы могут возникнуть, когда affinity наоборот не установлена, а операционная система может перераспределять потоки по своему усмотрению.
Давайте рассмотрим простой пример реализации фильтра Собеля.
Он состоит из нескольких последовательных этапов, каждый из которых можно распараллелить: Примените вертикальный фильтр Собеля к исходному изображению и получите временное изображение A (ippiFilterSobelVertBorder_8u16s_C1R) Примените горизонтальный фильтр Собеля к исходному изображению и получите временное изображение B (ippiFilterSobelHorizBorder_8u16s_C1R) Мы вычисляем квадрат каждого элемента A и снова сохраняем результат в A (ippiSqr_16s_C1IRSfs) Мы вычисляем квадрат каждого элемента B и снова сохраняем результат в B (ippiSqr_16s_C1IRSfs) Получаем сумму изображений А и Б, результат можем сохранить еще раз в А (ippiAdd_16s_C1IRSfs) Последний шаг — извлечение квадратного корня из A (ippiSqrt_16u_C1IRSfs) и преобразование в 8u (ippiConvert_16s8u_C1R).
Рис.
3 этапа реализации фильтра Собеля, которые могут иметь внутреннее распараллеливание.
Каждую из внутренних функций можно распараллелить.
Однако такой подход кажется неэффективным, поскольку имеется 7 последовательных вызовов функций, каждый из которых имеет точки синхронизации для создания/завершения параллельных регионов.
Перечисленные функции IPP используют простой шаблон разделения изображения на блоки в параллельной области.
Рис.
4 Стандартный метод блокировки в функциях IPP Рассмотрим, например, последовательный вызов ippiFilterSobelVertBorder_8u16s_C1R (src-> A) и ippiSqr_16s_C1IRSfs (A-> A2)).
Сначала для функции ippiFilterSobelVertBorder_8u16s_C1R создается первая параллельная область (область №1), затем вторая (область №2).
Рис.
5 Распределение логических потоков по HW-потокам в двух последовательных параллельных регионах.
Логический поток №0 будет выполняться в физическом потоке №4, и поэтому часть данных функции SobelVert будет храниться в кэшах L2 и L3, связанных с физическим потоком №4. В следующей параллельной области №2, созданной функцией Sqr, разделение изображения на блоки будет осуществляться по той же схеме, что и в первой функции, поэтому логический поток №0 будет ждать появления данных в кэше.
Однако ОС запустила этот логический поток №0 на физическом №3. Данных в кэше не будет и между кэшами этих потоков начнется интенсивный обмен данными, что приведет к замедлению времени выполнения приложения.
Другая проблема была связана с тем, что до выхода IPP версии 8.0 gold у пользователей возникала ситуация, когда приложение и библиотека использовали одну и ту же версию OpenMP, но разные модели компоновки.
Например, статическая версия IPP также использовала статическую библиотеку OpenMP, а пользователь в своем приложении использовал динамическую версию.
В этом случае OpenMP обнаруживает конфликт и выдает предупреждение о возможном ухудшении производительности.
IPP 8.0 теперь использует только динамическую версию OpenMP, поэтому после перехода на эту версию пользователи библиотеки больше не должны сталкиваться с подобными проблемами.
Результатом рассмотренных выше проблем является то, что, несмотря на повсеместное распространение многоядерности, функции с новым API имеют только обычную однопоточную реализацию.
Однако при этом учитывается возможность обработки изображений в параллельном режиме.
Однако для функций IPP, которые уже присутствовали в IPP, была сохранена многопоточная реализация OpenMP.
1.2 Проблемы границ при последовательной обработке изображений
Вторая проблема предыдущей версии API возникает, когда изображение последовательно обрабатывается несколькими фильтрами.Подразумевается, что все необходимые пиксели по краям области интереса (сокращенно ROI от англ.
Region of Interest) доступны в памяти.
Следовательно, чтобы получить выходное изображение шириной dstWidth и высотой dstHeight и фильтр размером kernelWidth X kernelHeight, необходимо подать входное изображение шириной dstWidth+kernelWidth-1 и высотой dstHeight+kernelHeight-1, рис.
6б) и 6в).
Рис.
6 Необходимость дополнительных рядов пикселей вокруг ROI для последовательного применения нескольких фильтров в старом API. А если последовательно применить 2,.
,N фильтров с размерами ядра kwidth1 X kHeight1,.
, kwidthN X kHeightN, то размер входного изображения соответственно должен быть (dstWidth+kWidth1+…+kWidthN – N) X (dstHeight+kHeight1+…+kHeightN-N), рис.
6а).
В этом примере два фильтра размером 5x5 и 3x3 используются для получения результирующего изображения размером 6x6 пикселей.
Первое изображение имеет размер 6+5-1+3-1 X 6+5-1+3-1, т.е.
12 X 12 пикселей, а изображение, полученное после первого фильтра, имеет размер 8x8. Применив к нему следующий фильтр 3x3, мы получим итоговое изображение 6x6. Для создания границ изображения можно использовать функции IPP ippiCopyReplication(Mirror/Const)Border, но это очень затратно с точки зрения производительности и дополнительной памяти, необходимой для нового изображения с рамкой.
Новый API позволяет избежать вызова таких функций и предоставляет на выбор три варианта обработки пикселей, находящихся за пределами ROI — по-прежнему считать их доступными в памяти, подставлять вместо них постоянное значение или использовать копию пикселей, лежащих на границе изображения.
Более подробная информация будет рассмотрена ниже.
1.3 Прямой порядок коэффициентов фильтра и фиксированный якорь
В предыдущей версии API 2D Filter использовался обратный порядок коэффициентов по формуле, Где
– значения ядра,
,
- горизонтальный и вертикальный размер ядра,
, координата якоря.
y Да якоря,
, координата якоря.
x Икс якоря Термин «якорь» относится к положению некоторой фиксированной ячейки внутри ядра, которая объединяется с обрабатываемым в данный момент пикселем.
Таким образом, с помощью якоря можно указать расположение ядра относительно пикселя.
Для упрощения использования функций подаваемые на вход коэффициенты фильтра теперь используются в прямом порядке по формуле
,
.
Рис.
6. демонстрирует различия обратного и прямого порядка использования коэффициентов.
Рис.
6 Прямой и обратный порядок коэффициентов Также в новом API было удалено понятие якоря (рис.
7).
Теперь он находится в фиксированной позиции Q=anchor.x=(kernelWidth-1)/2, P=anchor.y=(kernelHeight-1)/2.
Рис.
7. Фиксированное положение якоря x=(kw-1)/2, y=(kh-1)/2. В принципе, якорь определяет смещение в памяти от подаваемого указателя на ROI, поэтому в новом API в случае «нестандартного» значения якоря достаточно просто сместить ROI в нужную сторону, см.
рис.
8, на котором показано необходимое значение для различных значений привязки в старом API, смещение ROI в новом API. Серым цветом показана та же область памяти, а синим — указатель на область интереса, введенную в функцию.
В старом оно неизменно, а в новом смещается.
Рис.
8 Сдвиг рентабельности инвестиций для «нестандартного» значения привязки Для масок с четной шириной (высотой) при указании типа границы, расположенной в памяти справа (снизу) от изображения, должно быть доступно на одну строку пикселей больше.
Рис.
9. Для маски с четной шириной (высотой) справа (снизу) необходимо на несколько больше пикселей.
Помимо простоты использования, прямой порядок позволяет снизить накладные расходы внутри функции, связанные с перестановкой коэффициентов в обратном порядке.
Часто коэффициенты ядра симметричны, и в таком транспонировании нет необходимости.
1.4 Нет выделения внутренней динамической памяти
Функции с новым API не используют внутреннее распределение кучи с помощью операции malloc. Потому что при выделении такой памяти в каком-либо из потоков все остальные потоки останавливаются.Память теперь должна выделяться вне функций обработки, а требуемый размер требуемых буферов можно получить с помощью вспомогательных функций, таких как ippiGetBufferSize. 2 Операция разрушения/расширения морфологии изображения Давайте рассмотрим использование функций с новым API на примере операции морфологии.
Подробное описание свойств этой операции можно найти в литературе и Интернете.
В общих чертах, результатом этой операции является минимальный или максимальный элемент в некоторой окрестности пикселя.
В первом случае операция называется Erode, рис.
11, во втором — Dilate, рис.
12.
Рис.
10 Исходное изображение
Рис.
11 Операция Эрод
Рис.
12 Операция Расширение На первый взгляд кажется, что название не соответствует полученному результату, однако все правильно.
Так как операция Erode выбирает минимальное значение, а черный формируется меньшими значениями, чем белый, то соседние с буквами пиксели заменяются черными и возникает эффект утолщения букв.
В случае операции Dilate, наоборот, края букв заменяются белыми пикселями и буквы становятся тоньше.
3 Общие принципы работы новых функций API с поддержкой границ
3.1 Пример кода морфологической операции — Dilate
Для того, чтобы получить результат, показанный на рисунке 12, необходимо использовать следующие функции- IppStatus ippiMorphologyBorderGetSize_32f_C1R (int roiWidth, IppiSize MaskSize, int* pSpecSize, int* pBufferSize)
- IppStatus ippiMorphologyBorderInit_32f_C1R ( int roiWidth, const Ipp8u* pMask, IppiSize MaskSize, IppiMorphState* pMorphSpec, Ipp8u* pBuffer)
- IppStatus ippiDilateBorder_32f_C1R (const Ipp32f* pSrc, int srcStep, Ipp32f* pDst, int dstStep, IppiSize roiSize, IppiBorderType borderType, Ipp32f borderValue, const IppiMorphState* pMorphSpec, Ipp8u* pBuffer)
Фрагмент кода, использующий эти функции, выглядит следующим образом.
Лист. 1 стандартная последовательность вызова функций
И весь код приведен в Sheet. 2 Лист. 2 Код обработки изображенияippiMorphologyBorderGetSize_32f_C1R(roiSize.width, maskSize, &specSize, &bufferSize); pMorphSpec = (IppiMorphState*)ippsMalloc_8u(specSize); pBuffer = (Ipp8u*)ippsMalloc_8u(bufferSize); ippiMorphologyBorderInit_32f_C1R(roiSize.width, pMask, maskSize, pMorphSpec, pBuffer); ippiDilateBorder_32f_C1R(pSrc, srcStep, pDst, dstStep, roiSize, border, borderValue, pMorphSpec, pBuffer);
#define WIDTH 128
#define HEIGHT 128
int morph_dilate_st()
{
IppiSize roiSize = { WIDTH, HEIGHT };
IppiSize maskSize = { 3, 3 };
IppStatus status;
int specSize = 0, bufferSize = 0;
IppiMorphState* pMorphSpec;
Ipp8u* pBuffer;
Ipp32f* pSrc;
Ipp32f* pDst;
int srcStep, dstStep;
Ipp8u pMask[3 * 3] = { 1, 1, 1, 1, 1, 1, 1, 1, 1 };
IppiBorderType border = ippBorderRepl;
int borderValue = 0;
int i, j;
pSrc = ippiMalloc_32f_C1(WIDTH, HEIGHT, &srcStep);
pDst = ippiMalloc_32f_C1(WIDTH, HEIGHT, &dstStep);
for (j = 0; j<HEIGHT; j++){
for (i = 0; i<WIDTH; i++){
pSrc[WIDTH*j + i] = rand();//make image
}
}
status = ippiMorphologyBorderGetSize_32f_C1R(roiSize.width, maskSize, &specSize, &bufferSize);
if (status != ippStsOk){
printf("ippiMorphologyBorderGetSize_32f_C1R / %s\n", ippGetStatusString(status));
return -1;
}
pMorphSpec = (IppiMorphState*)ippsMalloc_8u(specSize);
pBuffer = (Ipp8u*)ippsMalloc_8u(bufferSize);
status = ippiMorphologyBorderInit_32f_C1R(roiSize.width, pMask, maskSize, pMorphSpec, pBuffer);
if (status != ippStsOk){
printf("ippiMorphologyBorderInit_32f_C1R / %s\n", ippGetStatusString(status));
ippsFree(pMorphSpec);
ippsFree(pBuffer);
return -1;
}
status = ippiDilateBorder_32f_C1R(pSrc, srcStep, pDst, dstStep, roiSize, border, borderValue, pMorphSpec, pBuffer);
if (status != ippStsOk){
printf("ippiMorphologyBorderGetSize_32f_C1R / %s\n", ippGetStatusString(status));
ippsFree(pMorphSpec);
ippsFree(pBuffer);
return -1;
}
ippsFree(pMorphSpec);
ippsFree(pBuffer);
ippiFree(pSrc);
ippiFree(pDst);
return 0;
}
3.2 Общий шаблон API с поддержкой границ
В этом примере и в целом вызов всех функций с помощью нового API осуществляется по следующему шаблону.
Рис.
13 Общий шаблон вызова функций IPP
3.3 ippGetSize — расчет размеров буфера
Функции, возвращающие размер буферов, содержат в своих именах суффикс GetSize. IppStatus ippiMorphologyBorderGetSize_32f_C1R(int roiWidth, IppiSize MaskSize, int* pSpecSize, int* pBufferSize) Функции могут иметь различные входные параметры, характеризующие операцию и изображение, а также возвращать размер используемой внутренней структуры данных pSpecSize(pStateSize) и размер буфера, используемого функцией pBufferSize. Поскольку новый API устранил внутреннее динамическое выделение памяти, для выделения необходимой памяти следует использовать либо инструменты операционной системы malloc, new и т. д. или использовать IPP-функции ippMalloc/ippiMalloc, которые удобны тем, что выделяют память и строки изображения по адресу, с выравниванием, оптимальным для используемой архитектуры.На x86 в большинстве случаев для повышения производительности желательно, чтобы все массивы начинались с границы 64 байта.
Указанные функции IPP распределяют память, выровненную таким образом.
А функция ippiMalloc также возвращает шаг между строками изображения, так что начало каждой строки изображения также будет выровнено.
Вполне возможно, что приложение будет использовать подряд несколько функций IPP. В этом случае можно выбрать один буфер, самый большой из всех, и скормить его всем функциям последовательно, см.
рис.
ниже.
Такой подход уменьшит фрагментацию памяти и с большой вероятностью выделенный буфер будет повторно использован и, следовательно, окажется в кеше.
Рис.
14 Использование буфера максимально необходимого размера позволяет уменьшить фрагментацию памяти и «прогреть» буфер
3.4 ippInit — инициализация внутренней структуры
Инициализация внутренних структур осуществляется функциями, содержащими суффикс Init IppStatus ippiMorphologyBorderInit_32f_C1R( int roiWidth, const Ipp8u* pMask, IppiSize MaskSize, IppiMorphState* pMorphSpec, Ipp8u* pBuffer ) Внутренняя структура данных содержит таблицы коэффициентов, указателей и других заранее вычисленных данных.IPP использует следующее правило: если в имени параметра используется слово Spec, то содержимое этой структуры является константой от вызова до вызова функции.
Поэтому его можно использовать одновременно в нескольких потоках.
Если используется слово State, то имеется в виду какое-то состояние, например, линия задержки в фильтре, и такая структура не может быть разделена между потоками.
Содержимое pBuffer определенно может измениться, поэтому для каждого потока необходимо выделять отдельный буфер.
3.5 ippFuncBorder – функция обработки
Функции прямой обработки нового API содержат в своем названии слово Border. IppStatus ippiDilateBorder_32f_C1R (const Ipp32f* pSrc, int srcStep, Ipp32f* pDst, int dstStep, IppiSize roiSize, IppiBorderType borderType, Ipp32f borderValue, const IppiMorphState* pMorphSpec, Ipp8u* pBuffer)3.6 Поддерживаемые типы бордюров
Новый API использует три типа границ изображений.ippBorderInMem, ippBorderConst, ippBorderRepl, см.
рис.
15
Рисунок 15. Три типа границы
В первом случае функция ippBorderInMem делает доступными необходимые пиксели как внутри изображения, так и в памяти.
Второй, ippBorderConst, использует фиксированное значение borderValue. Для многоканальных изображений используется массив значений.
Формирование недостающих пикселей в случае ippBorderRepl показано на рис.
15с).
Угловые пиксели изображения дублируются на две внешние стороны угла, а остальные граничные пиксели дублируются только на соответствующую им сторону от границы.
К указанным типам границ можно применять модификаторы со следующими значениями: ippBorderInMemTop = 0x0010,
ippBorderInMemBottom = 0x0020,
ippBorderInMemLeft = 0x0040,
ippBorderInMemRight = 0x0080
Эти модификаторы предназначены для поблочной обработки изображения.
Они объединяются с параметром borderType с помощью знака «|».
операция.
4. Использование функций API с поддержкой границ для внешнего распараллеливания функций.
4.1 Разделение изображения на блоки
Новый API позволяет обрабатывать изображения в несколько потоков блоками.Чтобы правильно соединить полученные отдельные блоки изображения в единое выходное изображение, необходимо применить модификаторы, соответствующие положению блока к основному типу границы.
Например, если потоков 16 и используется тип границы ippBorderRepl, то значение borderType должно формироваться следующим образом (рис.
16).
Рис.
16 Обработка изображений в 16 потоков на блок Номер блока 0 расположен в верхней левой части изображения.
Поэтому необходимые для обработки, но недостающие пиксели слева и сверху блока будут дублироваться граничными, а необходимые пиксели справа и снизу блока будут загружены из соседних блоков.
1 , 4 И 5 , согласно модификаторам ippBorderInMemRight|ippBorderInMemBottom. Блоки 1 , 2 не имеют пикселей только сверху, а с остальных трех сторон и углов окружены соседними блоками, в которых лежат нужные пиксели, поэтому указываются три модификатора ippBorderInMemBottom|ippBorderInMemRight|ippBorderInMemLeft. Для блоков 3,4,8,7,Б,В,Г,Е,Е модификаторы задаются аналогичным образом.
Блоки 5,6,9 расположены внутри изображения и поэтому для них можно указать все 4 модификатора, но проще использовать отдельное значение типа границы ippBorderInMem. Возврат к номеру блока 0 , комбинация модификаторов ippBorderInMemBottom|ippBorderInMemRight позволяет функции сделать вывод, что ей необходимо использовать пиксели из блока номер 5. А для номера блока С пиксели из блока будут дублироваться в этой области Д , но справа и сверху будут использоваться пиксели из блока 9 .
Вся логика такого чтения пикселей из памяти или дублирования пикселей построена таким образом, что результат обработки изображения как в целом, так и по блокам полностью идентичен.
Этот подход также предполагает, что при указании модификатора ippBorderInMem* память действительно доступна.
В некоторых особых случаях возможна следующая ситуация, как на рис.
17.
Рис.
17 Изображение за пределами поля Если разбить изображение размером 1х2 на два блока по одному пикселю и при этом использовать маску 5х5, то при обработке левого блока и указании для него модификатора ippBorderInMemRight как и должно быть перерасход выделенной памяти может произойти с непредсказуемыми последствиями.
Поэтому при разделении изображения на блоки и использовании модификатора ippBorderInMem следует убедиться, что пиксели kernelWidth/2 в памяти расположены в правильном направлении.
4.2 Разбиение изображения на полосы
На рис.16 показан общий случай разделения изображения на блоки для параллельной обработки и, вообще говоря, реализация разбиения на блоки может быть не такой уж простой задачей и может быть эффективнее разбивать изображение не на блоки, а на полосы, как это показано на рис.
18. Пограничный API также допускает такую блокировку.
Теги: #Intel #IPP #C++ #обработка изображений #openmp #Высокая производительность #программирование #Обработка изображений #Параллельное программирование
-
Покажи Мне Док
19 Oct, 24 -
Сохраняйте Api Простым
19 Oct, 24 -
Проект 2045 Это Аватар
19 Oct, 24 -
Определение Длины Дорожек На Доске
19 Oct, 24