这章将会说明一些kernel优化的小技巧。算法
一个复杂的应用程序可能包含不少步骤。对于OpenCL的移植性和优化,可能会问须要开发有多少个kernel。这个问题很难回答,由于这涉及到不少的因素。下面是一些准则:数组
上面的要求能够经过执行如下操做实现:ide
OpenCL支持一些编译选项,参考文献的《The OpenCLSpecification》的5.6.4节中进行了定义。编译选项能够经过APIsclCompileProgram和clBuildProgram传递。多个编译选项能够结合,以下所示。函数
clBuildProgram( myProgram,性能
numDevices,优化
pDevices,ui
“-cl-fast-relaxed-math ”,spa
NULL,操作系统
NULL );指针
经过这些选项,开发者可以针对他们本身的需求使能某些功能。好比,使用-cl-fast-relaxed-math,kernel会编译成使用快速数学函数而不是OpenCL标准函数,每个OpenCL的说明中OpenCL标准函数都有很高的精度要求。
OpenCL标准在OpenCL C语言中定义了许多数学函数,默认状况下,由于OpenCL规范说明书的要求,全部的数学函数都必须知足IEEE 754 单精度的浮点精度数学要求。Adreno GPU有一个内嵌的硬件模块,EFU(elementary function unit 基本函数单元),来加速一些初级的数学函数。对于许多EFU不能直接支持的数学函数,能够经过结合EFU和ALU操做来优化,或者经过编译器使用复杂的算法来模拟进行优化。表8-1展现了OpenCL-GPU 数学函数的列表,并按照他们的相对性能来分类的。使用更好性能的函数是个较好的方法,好比使用A类中的函数
表8-1 OpenCL数学函数的性能(符合IEEE 754标准)
类别 |
实现 |
函数(可参考OpenCL标准获取更多细节) |
A |
仅简单使用ALU指令 |
ceil,copysign,fabs,fdim, floor,fmax, fmin, fract,frexp,ilogb, mad, maxmag,minmag,modf,nan,nextafter,rint,round,trunk |
B |
仅使用EFU,或者EFU机上简单的ALU指令 |
asin,asinpi,atan,atanh,atanpi,cosh,exp,exp2,rsqrt,sqrt,tanh |
C |
ALU,EFU,和位操做的结合 |
acos,acosh, acospi,asinh, atan, atan2pi,cbrt,cos,cospi,exp10,expml,fmod,hypot,ldexp,log,log10,loglp,log2,logb,pow,remainder,remquo,sin,sincos,sinh,sinpi |
D |
复杂的软件模拟 |
erf,erfc,fma,lgamma,lgamma_r,pown,powr,rootn,tan,tanpi,tgamma |
另外,若是应用程序对精度不敏感的话,开发者能够选择使用内部的或者快速的数学函数来替代标准的数学函数。表8-2 总结了使用数学函数时的3个选项。
原始的:int c = a/b ;// a和b都是整数。
使用内部指令:
int c =(int)native_divide((float)(a)),(float)(b));
表8-2 基于精度/性能的数学函数选择
数学函数 |
定义 |
怎么使用 |
精度要求 |
性能 |
典型应用 |
标准 |
符合IEEE754单精度浮点要求 |
默认 |
严格 |
低 |
科学计算,对精度敏感的状况下 |
快速 |
低精度的快速函数 |
kernel编译选项 -cl-fast-relaxed-math |
中等 |
中等 |
许多图像,音频和视觉的用例中 |
内部 |
直接使用硬件计算 |
使用native_function替换kernel中的函数 |
低,与供应商有关 |
高 |
对精度损失不敏感的状况下的图像,音频,和视觉用例中 |
循环展开一般是一个好方法,由于它可以减小指令执行的耗时从而提升性能。Adreno编译器一般能基于试探法自动地将循环展开。然而,有时候编译器选择不将循环彻底展开,由于基于考虑到,寄存器的分配预算,或者编译器由于缺乏某些信息不能将它展开等因素。在这些状况下,开发者能够给编译器一个提示,或者手动的强制将循环展开,以下所示:
通常地,当在同一个wave中的work item有不一样的执行路径时,那么GPU就不是那么高效率。对于某些分支,一些work time必须执行,从而致使较低的GPU使用率,就像图8-1所示。并且,像if-else的条件判断代码一般会引发硬件的控制流逻辑,这个是很是耗时的。
图8-1 绘图表示出如今两个wave中的分支状况
有一些方法能够用来避免或者减小分支和条件判断。在算法层面,一种方法是将进入同一分支的work item组成一个不可分的wave。在kernel层面,一些简单的分叉/条件判断能够转变成快速的ALU操做。在9.2.6节中一个例子中,有耗时的控制流逻辑的一个三元操做被转变成一个ALU操做。其余的方式是使用相似于select函数,这个可能会使用快速的ALU操做来替代控制流逻辑。
许多操做可能会获取图像边界外的像素点,好比滤波,变换等。为了更好地处理边界,能够考虑下面的选择:
从Adreno A5X GPU开始,64位操做系统逐渐成为主流,并且许多的Adreno GPU支持64位操做系统。64位操做系统中最重要的改变是内存空间将能彻底覆盖4GB,并且CPU支持64位指令集。
当GPU能够获取64位内存空间时,它的使用将会引发额外的复杂性,并且可能会影响性能。
64位的内存地址在许多状况下会提高编写OpenCL kernel的复杂度,开发者必需要当心。强烈建议避免在kernels中定义size_t类型的变量。对于64位操做系统,在kernel中定义成size_t的变量可能会被当成64位长度的数据。Adreno GPUs必须使用32位寄存器来模拟64位。所以,size_t类型的变量会须要更多的寄存器资源,从而由于可用的wave变少和更小的workgroup大小致使性能退化。因此,开发者应该使用32位或者更短的数据类型来替代size_t.
对于OpenCL中返回size_t的内嵌函数,编译器会根据它所知道的信息尝试推导并限制数据范围。好比, get_local_id返回的数据类型为size_t,尽管local_id永远不会超过32位。在这种状况,编译器尝试使用一个短的数据类型来替代。可是,更好的方法是,给编译器提供关于数据类型的最充分的信息,而后编译器能够产生更好的优化代码。
OpenCL2.0 介绍了一个新的特性,叫作通常性的内存地址空间,在这个地址空间中,指针不须要指定它的地址空间,在OpenCL2.0以前,指针必须指定它的地址空间,好比指定为是local,private,或者global。在通常性的地址空间中,指针能够动态地被指定为不一样的地址空间。
这个特性下降了开发者的代码基础并且能重复使用已经存在的代码,使用通常性的内存地址空间会有轻微的性能损失,由于GPU SP硬件须要动态的指出真正的地址空间。若是开发者清楚知道变量的内存空间,建议清晰地定义内存地址。这将会减小编译器的歧义,从而会有更好的机器代码进而提高性能。
还有不少其余的优化技巧,这些技巧看起来很小,可是一样能够提升性能,这些技巧以下所示: