Введение
Статьи 568-570 разбирали обработку видео в реальном времени, неявно подразумевая достаточную производительность — в этой статье разберём явно, как OpenCV использует многопоточность внутри своих функций по умолчанию, и как можно дополнительно ускорить вычисления через GPU, используя модули CUDA или OpenCL.
Концепция
Многие функции OpenCV (особенно из модулей imgproc и core) автоматически используют многопоточность внутри себя (через библиотеку TBB или OpenMP, в зависимости от того, с какими опциями была собрана конкретная установка OpenCV), распределяя обработку отдельных частей изображения между доступными ядрами процессора без какого-либо явного участия разработчика — это даёт автоматическое ускорение «из коробки» для многоядерных систем, но также означает, что попытка дополнительно, вручную распараллелить вызовы таких функций (например, обрабатывая несколько изображений в отдельных, явно созданных потоках одновременно) может не дать ожидаемого линейного ускорения из-за конкуренции за уже занятые этими внутренними потоками вычислительные ресурсы. GPU-ускорение через CUDA (для GPU NVIDIA, требующее OpenCV, собранной с поддержкой CUDA) или OpenCL (более универсальный, кроссплатформенный API, поддерживающий широкий спектр GPU) позволяет выполнять вычислительно интенсивные операции (особенно DNN-инференс статьи 569, и некоторые операции imgproc) непосредственно на GPU, что может дать существенное ускорение для подходящих, параллелизуемых вычислений по сравнению с выполнением исключительно на CPU.
Пример кода: Производительность
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
std::cout << "Количество потоков по умолчанию: " << cv::getNumThreads() << std::endl;
cv::setNumThreads(4); // явное ограничение количества потоков, используемых OpenCV внутри своих функций
// Проверка доступности CUDA-ускорения в текущей сборке OpenCV
std::cout << "Доступно CUDA-устройств: " << cv::cuda::getCudaEnabledDeviceCount() << std::endl;
return 0;
}
// Использование GPU через модуль CUDA (требует OpenCV, собранной с поддержкой CUDA)
#include <opencv2/opencv.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <opencv2/cudafilters.hpp>
int main()
{
cv::Mat cpuImage = cv::imread("large_image.jpg");
cv::cuda::GpuMat gpuImage; // специальная структура, представляющая данные НА GPU, а не в обычной RAM
gpuImage.upload(cpuImage); // явная передача данных с CPU на GPU — НЕ бесплатная по времени операция
cv::cuda::GpuMat gpuGray;
cv::cuda::cvtColor(gpuImage, gpuGray, cv::COLOR_BGR2GRAY); // обработка ВЫПОЛНЯЕТСЯ на GPU
cv::Mat resultOnCpu;
gpuGray.download(resultOnCpu); // явная передача результата ОБРАТНО с GPU на CPU
cv::imwrite("gpu_processed.png", resultOnCpu);
return 0;
}
// Включение OpenCL-ускорения через единый, кроссплатформенный API (T-API, Transparent API)
#include <opencv2/opencv.hpp>
int main()
{
cv::ocl::setUseOpenCL(true); // включение прозрачного использования OpenCL там, где это возможно
cv::UMat image, gray; // UMat — "универсальная" Mat, может прозрачно использовать OpenCL под капотом
cv::imread("photo.jpg").copyTo(image);
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY); // та же функция, но МОЖЕТ выполниться на GPU через OpenCL,
// ПОЛНОСТЬЮ прозрачно для кода — без явного upload/download!
return 0;
}
Пояснения к коду
cv::getNumThreads()/cv::setNumThreads() показывают прямой контроль над внутренней многопоточностью OpenCV — явное ограничение количества потоков может быть полезно, например, когда приложение само управляет распределением вычислительных ресурсов между несколькими своими собственными задачами и не хочет, чтобы OpenCV «забирал» больше потоков процессора, чем выделено для конкретной задачи обработки изображения. Пример с CUDA показывает явный, низкоуровневый подход — cv::cuda::GpuMat представляет данные, физически находящиеся в памяти GPU, и явные вызовы upload()/download() показывают необходимую, не бесплатную передачу данных между обычной памятью (RAM) и памятью GPU, что является важным практическим компромиссом — выгода от ускорения вычислений на GPU должна превышать стоимость этой передачи данных, что особенно критично для не очень «тяжёлых» по вычислениям операций или небольших изображений, где стоимость передачи может перевесить выгоду от GPU-ускорения. cv::UMat с OpenCL показывает альтернативный, более «прозрачный» подход (T-API) — тот же код (cv::cvtColor) может прозрачно выполниться либо на CPU, либо на GPU через OpenCL в зависимости от внутреннего решения OpenCV, без необходимости в явных вызовах upload/download, хотя это удобство достигается за счёт меньшего явного контроля над тем, что именно и когда реально выполняется на GPU.
Подводные камни
- Передача небольших объёмов данных на GPU и обратно для относительно простых, «лёгких» операций, где стоимость передачи данных (
upload/download) превышает выгоду от выполнения самой операции на GPU — GPU-ускорение даёт реальную выгоду преимущественно для существенных по объёму вычислений (большие изображения, сложные операции, батчевая обработка множества кадров), и для небольших изображений или простых, быстрых операций накладные расходы передачи данных между CPU и GPU памятью могут сделать GPU-версию фактически медленнее, чем эквивалентная обработка непосредственно на CPU. - Предположение о доступности CUDA-ускорения без явной проверки, особенно для приложений, распространяемых на оборудование пользователей с разными, не всегда предсказуемыми конфигурациями GPU (или вовсе без GPU NVIDIA, поддерживающего CUDA) — явная проверка
cv::cuda::getCudaEnabledDeviceCount()перед попыткой использовать CUDA-функции необходима, и приложение должно предусмотреть корректный, работающий путь выполнения на CPU как запасной вариант для систем без поддерживаемого GPU. - Ручное распараллеливание вызовов OpenCV-функций через собственные, явно созданные потоки без учёта того, что сами эти функции уже могут использовать внутреннюю многопоточность — одновременный запуск нескольких таких уже внутренне многопоточных вызовов из разных, явно созданных потоков может привести к избыточной конкуренции за доступные ядра процессора (больше активных потоков, чем реально доступных ядер), что не даёт ожидаемого, пропорционального ускорения и может даже снизить общую производительность по сравнению с более простым, последовательным выполнением тех же операций без дополнительного, избыточного уровня параллелизма.
- Игнорирование того, что не каждая функция OpenCV имеет соответствующую CUDA/OpenCL-реализацию, и попытка построить полностью GPU-ускоренный конвейер обработки может столкнуться с операциями, доступными только в CPU-версии, что требует промежуточной передачи данных обратно на CPU для этой конкретной операции, а затем снова на GPU для последующих операций — частые такие переключения между CPU и GPU в рамках одного конвейера обработки могут существенно снизить общую выгоду от GPU-ускорения из-за накопленных накладных расходов многократной передачи данных, и при проектировании GPU-ускоренного конвейера стоит явно проверить доступность нужных операций именно в выбранном (CUDA/OpenCL) ускоренном варианте.