提升你OpenCL CV算法优化能力的40条经验
(1) 定点化算法的计算一般是float,但是float的类型会占据很多内存,所以可以按原则1用half来计算。其实也可以进行定点化,将float的数据用u16表示,如cpu段不支持half,但是支持u16,所以只能进行定点化
(2) float->half
节省内存读取和存储带宽, 已经加快运算。这个过程中可能因为某些地方half存在溢出,可以想办法解决,而不是说溢出了就代表用不了half,需要向办法解决。有时候half位数不够,存储不下计算的结果,现在需要想办法排除干扰。
(3) 使用内置函数
<1> 有些成累加操作,可以转换成两个向量使用内置函数dot来实现。
<2> 对于向量中选择最大值可以使用select,不需要一个个比较。
<3>程序对精度不敏感的话,开发者可以选择使用内部的或者快速的数学函数来替代标准的数学函数
如log 用native_log替代,exp用native_exp代替,参考:https://blog.csdn.net/weixin_30266885/article/details/99808622
(4) 使用向量化操作
使用load计算过程中使用向量 如float4 half4
(5) 模块算子展开
也就是说,可能我们的模块是一个个算子进行嵌套的,这样的话如果一个kernel里面分配很多的的内存,如果循环调用了,就会连续开辟几次同一个空间。当我们把算子展开的时候,这样其实内存开辟一次就可以,如果不做模块算子展开,很多重复的内存利用,以及重复的计算就会无法避免。
(6) 进行内存复用 (删除不必要的内存,尽量少用内存开辟)
整套算法由很多的模块组成,模块内部有很多的内存重复申请。模块内部可以通过模块展开,复用内存。对于大的模块之间,我们可以事先开辟几块大的内存,在几个模块之间共用.(由于提前申请的模块有限,我们尽量满足大的内存复用)。
(7) 归约算法。
适用于求取图片最大值和最小值等。归约算法的算法复杂度:https://mp.weixin.qq.com/s/vv2RZHcinKwPyq5_qzNxTg
(8) 使用local计算(如图像直方图)
为什么使用local计算速度会快,以直方图为例,我们可以直接把获取的像素直接往全局的256的global的数组里面写,但是atomic_inc是互斥的,所以访问同256数组一个位置的线程会排队等待,但是如果我们使用4个256的local数组,工作组之间访问不用互斥。
(9) 有些参数虽然是计算的,但是每次都一样,而且每次数据都一样,可以直接在kernel里面写死。不需要通过参数传入进去。如一些滤波。
(10) 对于一些滤波相关的操作,最内层的循环是这样的两层循环,将循环展开。
for(y=-radius; y <= radius; ++y){
for(x=-radius;x<=radius;++x){
}
}
(11) 算子融合
可以通过算子融合,去除不必要的内存分配,同时两个算子合成一个算子,减少kernel的对内存的操作次数。
(12) 在不影响结果的情况下,可以更换计算的顺序,使得算子融合和内存复用更方便操作。
(13) 除了向量化之外,每次还可以多做几行,增加单个线程的输出。
(14) 对于对称的滤波算子进行行列分开计算。
(15) 利用公式推导转换计算方式,从而减少内存读取次数或者运算量。
如:<1>求图像矩阵的方差,遍历一次就可以求出。
<2> resize计算过程
(16) 对于想boxfilter这样的滤波算子,会重复的累加运算,其实运算的结果可以进行复用,我们可以申请一块临时buf,存储累加的结果进行复用,每次只需要加上新增的一行然后减去移除的一行,这样不仅减少重复内存的读取以及减少计算量。
(17) cpu和gpu代码混在一起跑的时候,尽量不用混在一起,cpu代码尽量放在一起,gpu代码放在一起。
这个是只数据有依赖关系的cpu和gpu代码(如果没有数据依赖,是否可以异构并行)
(18) 单个kernel中分配了多个同样大小的buff时,可以合并申请一个大的buff,在kernel内部做偏移计算。(local memory也是同样的道理)。
(19) 对于kernel中的一些除法操作,尽量通过公式转换消除耗时的除法操作。
(20) 对于kernel中if判断,可以通过使用比较操作,然后通过select替代。
(21) 对于查表操作,看能不能恢复表格的生成公式,如果能够找到公式,这样既不用分配内存存储表格,同时计算时查表访存效率很低,通过公式减少内存访问,同时可以向量化并行。
(22) GPU global 循环次数不能太少。
(23) local size选取合适的大小。
(24) 当平台对于内存分配比较耗时的时候,当输入和输出读和写没有冲突的时,可以使用同一块内存。
(25) 对于算法给出的C代码,我们需要首先分析C代码的逻辑,首先要做的C代码的简化。
(26) 多次内存申请合并成一次内存申请,尤其是在mtk平台上,内存申请非常耗时。
(27) 优化内存排布,有些数据排布会造成数据的反复读取和存储,以及cache命中率地的问题,我们需要调整数据分布情况。
(28)绑定大核操作,对于要提高线程的执行速度,以及不在核心之间进行切换可以进行绑定核心操作。
(29)为了减少内存拷贝,可以使用map和unmap操作。
(30) 在求图像直方图的时候,使用atomic_inc进行原子类型的加1操作。不能使用加法运算符,因为多个线程访问同一个位置,不用原子操作,结果可能有问题。
(31) 对于一些频繁使用的算子,可能需要支持各种类型 float half uchar。我们可以写一个模板然后以宏的方式展开
(32) 对于opencv函数resize 当模式是linear的时候, 可以使用read_image的来实现。
(33) 优化时找到性能消耗瓶颈所在,分析是什么造成了这一块的性能消耗,然后针对所找到的原因,然后解决这个问题。
(34) #pragma unroll就是编译器循环展开。
https://bbs.gpuworld.cn/index.php?action=printpage;topic=59012.0
(35) 利用even和odd提取向量的奇数和偶数部分。
(36) kernel有一些固定的参数可以调节这,如工作组多少以及local size的大小,这些参数大小会影响kenrel性能,可以通过调节参数来优化kernel性能。
(37) 优化CV算子,不能专门只看这一个kernel,还需要看前后kernel之间的关联,比如kernelA 和kernelB, kernelB中只有在某些条件成立的时候才会使用kernelA的计算结果,那么我们只需要在条件成立的时候才计算kernelA,其他情形可以跳过这个计算。
(38) 有时候申请两个相同大小的内存,可以将其合并成一个为通道为2的内存申请。
(39) opencl中何时调用clfinish,一般是在模块中申请内存之后再调用。
(40)对于一些比较大的表格,我们可以恢复它的生成公式,当表格比较大的时候自己生成公式比较简单的时候节省性能自己内存。但是当某些数学函数计算比较耗时的时候如cbrt和exp,我们反过来先把他们的值生成一个表格,然后实际运算中可以利用查表来加速运算。
页:
[1]