问题
打算通过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->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
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仍然处于忙碌的状态。