一、原因总结
最近项目需要,发现了这个问题。网上找原因,汇总起来,有以下几点原因:
1、首先对于任何一个CUDA程序,在调用它的第一个CUDA API时后都要花费秒级的时间去初始化运行环境,后续还要分配显存,传输数据,启动内核,每一样都有延迟。这样如果你一个任务CPU运算都仅要几十毫秒,相比而言必须带上这些延迟的GPU程序就会显得非常慢。
2、其次,一个运算量很小的程序,你的CUDA内核不可能启动太多的线程,没有足够的线程来屏蔽算法执行时从显存加载数据到GPU SM中的时延,这就没有发挥GPU的真正功能。
3、数据从内存传递到显存和cudaMalloc耗时很长,NVIDIA提供的nsight中的profile可以看每一个部分的耗时。基本上OpenCV的算法都归纳为三个部分:upload(gpu::Mat), processCodeBlock, download(gpu::Mat)。你看看是不是80%以上的时间都花在第一个和最后一个上,问题就迎刃而解了。因为gpu在计算上虽然比cpu快,但实际上在使用gpu的时候有一步非常耗时,那就是将内存与显存中的数据进行互相拷贝,同时这也是使用gpu运算时逃不掉的一步。
4、GPU擅长的是大规模并行计算,比起cpu只是以巨额核心数取得优势的,单核速度其实被cpu碾压。如果数据规模小的话GPU并不能用上太多核,所以比cpu慢。减少数据在CPU和GPU之间的传递次数;运算量非常小的部分不要用GPU,数据量非常大、循环次数非常多的时候才使用GPU。
//执行这些简单算子,CPU比GPU更快
cvtColor,GaussianBlur,Canny
//执行这些耗时算子,GPU比CPU更快
HoughCircles,HoughLines,matchTemplate
5、如果问题规模较小,逻辑控制较为复杂,并行性很小优先使用CPU处理该问题,如果包含较大规模的数据处理,则考虑使用GPU进行处理。
CPU上线程是重量级实体,可以开启1~32个线程,且上下文切换较为缓慢,GPU上线程是高度轻量级的,可以开几百甚至上千个线程。
CUDA通过两种API来对设备GPU设备进行控制,包括驱动API和运行API,其中驱动API较难编程,但是设备控制能力和利用率高。两者只能选择其中一种,不能混合使用。
一个CUDA程序包含了两个部分代码,在CPU上运行的主机代码和在GPU上运行的设备代码。
6、总结一句,GPU的并行处理的确很快,但数据传入GPU和传出的开销实在太大,往往影响了代码的整体效率,运算量非常小的计算不要用GPU。
二、举例opencv
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/core/cuda.hpp>
#include <opencv2/core/ocl.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/cudaimgproc.hpp>
#include <opencv2/cudaarithm.hpp>
#include <opencv2/cudafilters.hpp>
#include <opencv2/cudawarping.hpp>
#define IMAGE_TEST_PATHNAME "D:\\test_src.jpg"
#define IMAGE_SOURCE "D:\\test_src.jpg"
#define IMAGE_TEMPLATE "D:\\test_templ.jpg"
void checkCuda() //旧版本的是cv::gpu,#include <opencv2/gpu/gpu.hpp>,已弃用
{
int64 begintime, endtime;
int num_devices = cv::cuda::getCudaEnabledDeviceCount();
if (num_devices <= 0)
{
std::cerr << "There is no cuda device" << std::endl;
return;
}
int enable_device_id = -1;
for (int i = 0; i < num_devices; i++)
{
cv::cuda::DeviceInfo dev_info(i);
if (dev_info.isCompatible())
{
enable_device_id = i;
}
}
if (enable_device_id < 0)
{
std::cerr << "GPU module isn't built for GPU" << std::endl;
return;
}
cv::cuda::setDevice(enable_device_id); //指定显卡
//有一个问题是,一般使用GPU加速的话,第一次调用GPU,会很慢很慢,一条简单的语句都用了10多秒左右。
//治标不治本的解决方法是在程序的开头加上一句cv::gpu::GpuMata(10, 10, CV_8U);
//这样会令耗时的操作放在一开头,不那么影响后面的操作时间
//为什么第一次函数调用很慢
//那是因为初始化开销;在第一个GPU函数调用Cuda Runtime API被隐式初始化;
cv::cuda::GpuMat(10, 10, CV_8U);
//测试用例
cv::Mat src_image = cv::imread(IMAGE_PATHNAME);
cv::Mat dst_image;
cv::cuda::GpuMat d_src_img(src_image); //upload src image to gpu
//或者d_src_img.upload(src_image);
cv::cuda::GpuMat d_dst_img;
begintime = cv::getTickCount();
cv::cuda::cvtColor(d_src_img, d_dst_img, cv::COLOR_BGR2GRAY); //canny
d_dst_img.download(dst_image); //download dst image to cpu
endtime = cv::getTickCount();
std::cerr << 1000 * (endtime - begintime) / cv::getTickFrequency() << std::endl;
cv::namedWindow("checkCuda", cv::WINDOW_NORMAL);
cv::imshow("checkCuda", dst_image);
}
void calcEdgesCuda()
{
cv::ocl::setUseOpenCL(false);
double start = cv::getTickCount();
cv::cuda::GpuMat gpuGray, gpuBlur, gpuEdges;
cv::Mat cpuEdges;
cv::Mat cpuFrame = cv::imread(IMAGE_PATHNAME);
cv::cuda::registerPageLocked(cpuFrame); //锁页内存
cv::cuda::GpuMat gpuFrame;
gpuFrame.upload(cpuFrame);
cv::cuda::cvtColor(gpuFrame, gpuGray, cv::COLOR_BGR2GRAY);
cv::Ptr<cv::cuda::Filter> gaussFilter = cv::cuda::createGaussianFilter(CV_8UC1, CV_8UC1, cv::Size(3, 3), 15, 15);
gaussFilter->apply(gpuGray, gpuBlur);
cv::Ptr<cv::cuda::CannyEdgeDetector> cannyEdge = cv::cuda::createCannyEdgeDetector(50, 100, 3);
cannyEdge->detect(gpuBlur, gpuEdges);
cv::cuda::GpuMat gpuLines; //This should be GpuMat...
#if 0 //find line
std::vector<cv::Vec2f> vtLines;
cv::Ptr<cv::cuda::HoughLinesDetector> hough = cv::cuda::createHoughLinesDetector(1, CV_PI / 180, 120);
hough->detect(gpuEdges, gpuLines);
hough->downloadResults(gpuLines, vtLines);
#else
cv::Ptr<cv::cuda::HoughCirclesDetector> hough1 = cv::cuda::createHoughCirclesDetector(1.5, 15, 300, 1, 1, 100);
hough1->detect(gpuEdges, gpuLines);
cv::Ptr<cv::cuda::HoughCirclesDetector> hough2 = cv::cuda::createHoughCirclesDetector(1, 15, 100, 30, 1, 100);
hough2->detect(gpuEdges, gpuLines);
#endif
gpuEdges.download(cpuEdges);
cv::cuda::unregisterPageLocked(cpuFrame); //解除锁页
std::cout << "Cuda cost time:(s)" << ((cv::getTickCount() - start) / cv::getTickFrequency()) << std::endl;
cv::namedWindow("Canny Edges Cuda", cv::WINDOW_NORMAL);
cv::imshow("Canny Edges Cuda", cpuEdges);
}
void matchTemplateCPU()
{
cv::Mat src = cv::imread(IMAGE_SOURCE, cv::IMREAD_GRAYSCALE);
cv::Mat templ = cv::imread(IMAGE_TEMPLATE, cv::IMREAD_GRAYSCALE);
cv::Mat dst;
double minVal = 0;
double maxVal = 0;
cv::Point minLoc;
cv::Point maxLoc;
double start = cv::getTickCount();
cv::matchTemplate(src, templ, dst, cv::TM_CCOEFF_NORMED); //用6种匹配方式
cv::normalize(dst, dst, 1, 0, cv::NORM_MINMAX);
cv::minMaxLoc(dst, &minVal, &maxVal, &minLoc, &maxLoc); //找到最佳匹配点
std::cout << "matchTemplateCPU cost time:(s)" << ((cv::getTickCount() - start) / cv::getTickFrequency()) << std::endl;
cv::rectangle(src, cv::Rect(maxLoc.x, maxLoc.y, templ.cols, templ.rows), 1, 8, 0);
cv::namedWindow("matchTemplateCPU", cv::WINDOW_NORMAL);
cv::imshow("matchTemplateCPU", src);
}
void matchTemplateCuda()
{
cv::Mat src = cv::imread(IMAGE_SOURCE, cv::IMREAD_GRAYSCALE);
cv::Mat templ = cv::imread(IMAGE_TEMPLATE, cv::IMREAD_GRAYSCALE);
cv::Mat dst;
double minVal = 0;
double maxVal = 0;
cv::Point minLoc;
cv::Point maxLoc;
cv::cuda::GpuMat gsrc, gtempl, gdst;
double start = cv::getTickCount();
gsrc.upload(src);
gtempl.upload(templ);
cv::Ptr<cv::cuda::TemplateMatching> matcher;
matcher = cv::cuda::createTemplateMatching(CV_8U, cv::TM_CCOEFF_NORMED);
matcher->match(gsrc, gtempl, gdst);
cv::cuda::minMaxLoc(gdst, &minVal, &maxVal, &minLoc, &maxLoc);
std::cout << "matchTemplateCuda cost time:(s)" << ((cv::getTickCount() - start) / cv::getTickFrequency()) << std::endl;
cv::rectangle(src, cv::Rect(maxLoc.x, maxLoc.y, templ.cols, templ.rows), 1, 8, 0);
cv::namedWindow("matchTemplateCuda", cv::WINDOW_NORMAL);
cv::imshow("matchTemplateCuda", src);
}
锁页能够加速数据在CPU和GPU之间的传递
cv::cuda::registerPageLocked(img);//锁页内存
gimg.upload(img);//上传数据至GPU
gimg.download(img);//下载数据至CPU
cv::cuda::unregisterPageLocked(img);//解除锁页
---
姊妹篇
参考文献
为什么opencv用GPU实现比用CPU实现的慢?_opencv在显卡上和cpu上跑程序哪个快_THMAIL的博客-CSDN博客
opencv(C++)GPU、CPU 模板匹配_opencv 操作gpu_1037号森林里一段干木头的博客-CSDN博客
cuda实现的连通域
https://docs.nvidia.com/cuda/npp/group__image__filter__label__markers.html
CV-CUDA™ is an open-source, GPU accelerated library for cloud-scale image processing and computer vision.
https://github.com/CVCUDA/CV-CUDA
《通用图形处理器设计——GPGPU编程模型与架构原理》
作者:景乃锋、柯晶、梁晓 出版社:清华大学出版社 出版时间:2022年05月