为什么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 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 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.csdn.net/gamesdev/article/details/17488237

该博文内容摘录如下:


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

 

你可能感兴趣的:(Python)