为什么opencv用GPU实现比用CPU实现的慢?

部署运行你感兴趣的模型镜像

问题


打算通过OpenCV的GPU模块优化现有代码的运行效率,怀抱着美好愿望开始了代码的改写工作。改写的过程并不顺利,遇到了不少问题。例如,gpu模块提供的接口非常坑爹,相当一部分不支持浮点类型(像histogram、integral这类常用的都不支持);又如,遇到阈值判断的地方,就必须传回cpu处理,因为gpu函数都是并行处理的,每改写完一个算法模块,就测试一下运行效率,有的时候是振奋人心,有的时候则是当头棒喝——比CPU还慢?


经过一系列的google,终于找到原因:

http://stackoverflow.com/questions/12074281/why-opencv-gpu-codes-is-slower-than-cpu

http://answers.opencv.org/question/1670/huge-time-to-upload-data-to-gpu/#1676

The first gpu function call is always takes more time, because CUDA initialize context for device. The following calls will be faster.

Programming Guide的CUDA C Runtime-Initialization一节有对此说明


整段代码的第一条与GpuMa有关的语句就是所谓的first gpu function,这里CUDA需要初始化设备上下文,而该操作是非常耗费时间的!

在代码开头,或者类的构造函数,加入:

    Mat src1; src1 = Mat::zeros(cvSize(5,5),CV_16UC1);
    GpuMat src2(src1);

src1随便赋一个初值,关键是调用一次涉及GpuMat的操作(其它gpu函数也行),那么段代码就实现了CUDA的初始化。

经过实测,这里需要耗费400ms!尽管是像上面那么简单的两个语句,只要是整段代码的第一次调用gpu相关操作,默认执行CUDA的初始化!在其它机器上不一定是400ms,应该跟具体的PC硬件有关。

往后的gpu的操作都不会那么慢,但一旦涉及upload、download(显存与内存的数据传递),时间成本都达到ms级别!


总结一句,GPU的并行处理的确很快,但数据传入GPU的开销实在太大,往往影响了代码的整体效率。


现在的项目是,C++写的算法,做成dll供C#调用;理所当然的,gpu的操作都放在C++的接口I函数里;那问题就来了,每次C#调用C++的函数,都要执行CUDA的初始化,都要耗费400ms在无意义的操作上,对于算法的实时性要求简直不能忍受!除非,在C#里面先初始化CUDA,但这样可行吗?又合理吗?

现在总算搞明白,为什么网上没多少opencv gpu的资源,用的人不多嘛,实在坑爹啊!这么大的弊端,真的用过就怕了,害我浪费了那么多时间去优化,算法主体部分是快了不少,原本200ms,现在到了100ms的水平,但就这坑爹的CUDA初始化400ms就够抵消所有的成果!

以后在引入gpu之前,真的要好好审视本算法的当前运行效率,只有当前运行时间远大于CUDA初始化的耗时,才有引入的必要!的确,我是没有做好运算量的评估,现在的算法的运算量根本用不到gpu!


解决措施


问题1、CUDA初始化设备上下文非常耗时。

可以通过在程序开头进行任意一项与CUDA相关的操作,如定义一个GpuMat:

GpuMat a(10,10,CV_8U); //第一条CUDA语句,初始化设备上下文

第一条CUDA语句将会非常耗时,但之后都会恢复正常。


问题2、访存时间(latency)影响程序的整体效率。

两个途径:减少数据在CPU和GPU之间的传递次数;运算量非常小的部分不要用GPU,数据量非常大、循环次数非常多的时候才使用GPU。

前者是最直接的手段,后者则是GPU的使用原则,可见以下例子:


分别用CPU和GPU实现高斯滤波

cv::GaussianBlur(img, img, Size(11, 11), 1.5, 1.0, cv::BORDER_REFLECT); 

以上是CPU的代码

        cv::cuda::registerPageLocked(img);        //锁页内存
        gimg.upload(img);                        //上传数据至GPU
        cv::Ptr<cv::cuda::Filter> gauss = cv::cuda::createGaussianFilter(CV_32F, CV_32F, Size(11, 11), 1.5, 0, cv::BORDER_DEFAULT,-1);    //创建高斯滤波器
        gauss->apply(gimg, gimg);            //高斯滤波
        gimg.download(img);                        //下载数据至CPU
        cv::cuda::unregisterPageLocked(img);    //解除锁页

以上是GPU的代码,锁页能够加速数据在CPU和GPU之间的传递

运行结果:只计算高斯滤波函数的耗时,GPU是CPU的1/3~1/2,然而考虑上访存时间,GPU甚至比CPU还慢!


但如果进行10次高斯滤波,GPU的优势就能够得以体现:

        cv::cuda::registerPageLocked(img);        //锁页内存
        gimg.upload(img);                        //上传数据至GPU
        cv::Ptr<cv::cuda::Filter> gauss = cv::cuda::createGaussianFilter(CV_32F, CV_32F, Size(11, 11), 1.5, 0, cv::BORDER_DEFAULT,-1);    //创建高斯滤波器
        for(int i=0;i<10;i++)
            gauss->apply(gimg, gimg);            //高斯滤波
        gimg.download(img);                        //下载数据至CPU
        cv::cuda::unregisterPageLocked(img);    //解除锁页

以上是进行10次滤波的GPU代码,同时,CPU代码也执行10次滤波


运行结果:算上方寸时间,GPU的总耗时是CPU的1/5,如果执行100次滤波,则为1/10


但需要注意的是,运算量不仅跟执行次数有关,还跟数据量有关。按道理说,GPU对数据量越大的情况越有利,但在opencv的CUDA模块中,矩阵较大的数据(行列数较多)将大大增加数据传递的耗时,因为GpuMat本身对矩阵大小是有限制的,所以这又制约了CUDA并行的效率,除非使用CUDA编程合理分配数据和线程,opencv本身对大矩阵是无能为力的。

---------------------------------------------------------------------分割线-------------------------------------------------------------------


以下博文是更深层次的讨论CUDA代码优化:http://blog.youkuaiyun.com/gamesdev/article/details/17488237

该博文内容摘录如下:


执行单线程的做法,无疑会闲置非常多的并行计算资源,我们必须采用多线程,这样效率就会得到大幅度的提升。在GPU编程中,有一个叫“掩藏”(Hide)的概念。它的意思是指线程因为访问存储器或者阻塞等其它原因造成的延迟,但由于分配的线程足够多,导致整体上看GPU仍然处于忙碌的状态。

 

您可能感兴趣的与本文相关的镜像

PyTorch 2.8

PyTorch 2.8

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

实现 OpenCV 的 CUDA 模块进行 GPU 加速,可按以下步骤进行: #### 环境准备 要使用 OpenCV 的 CUDA 模块,需要确保系统已经安装了 CUDA 工具包和相应的 GPU 驱动程序,并且 OpenCV 是支持 CUDA 编译的版本。 #### 代码实现 1. **检查 CUDA 支持**:在代码中首先检查系统是否支持 CUDA。 ```python import cv2 if cv2.cuda.getCudaEnabledDeviceCount() > 0: print("CUDA is supported.") else: print("CUDA is not supported.") ``` 2. **将数据上传到 GPU**:使用 `cv2.cuda_GpuMat()` 来创建 GPU 上的矩阵对象,并使用 `upload()` 方法将数据从 CPU 上传到 GPU。 ```python # 读取图像 frame = cv2.imread('your_image.jpg') # 创建 GPU 矩阵对象 gpu_frame = cv2.cuda_GpuMat() # 将图像数据上传到 GPU gpu_frame.upload(frame) ``` 3. **在 GPU 上进行图像处理**:使用 OpenCV 的 CUDA 模块中的函数进行图像处理,例如 `cv2.cuda.cvtColor()` 进行颜色空间转换。 ```python # 在 GPU 上进行颜色空间转换 gpu_gray = cv2.cuda.cvtColor(gpu_frame, cv2.COLOR_BGR2GRAY) ``` 4. **将处理结果下载到 CPU**:使用 `download()` 方法将处理后的结果从 GPU 下载到 CPU。 ```python # 将处理后的图像数据下载到 CPU gray = gpu_gray.download() ``` 5. **使用 CUDA 特定函数**:对于一些特定的操作,如边界填充,可以使用 `cv2.cuda.copyMakeBorder()` 函数。 ```python # 在 GPU 上进行边界填充 top, bottom, left, right = 10, 10, 10, 10 borderType = cv2.BORDER_CONSTANT value = (0, 0, 0) gpu_padded = cv2.cuda_GpuMat() cv2.cuda.copyMakeBorder(gpu_frame, gpu_padded, top, bottom, left, right, borderType, value) # 将填充后的图像数据下载到 CPU padded = gpu_padded.download() ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值