Qualcomm_Mobile_OpenCL.pdf 翻译-8-kernel性能优化

 

         这章将会说明一些kernel优化的小技巧。算法

8.1 kernel合并或者拆分

         一个复杂的应用程序可能包含不少步骤。对于OpenCL的移植性和优化,可能会问须要开发有多少个kernel。这个问题很难回答,由于这涉及到不少的因素。下面是一些准则:数组

  • 内存和计算之间的平衡。
  • 足够多的wave来隐藏延迟。
  •  没有寄存器溢出。

 

         上面的要求能够经过执行如下操做实现:ide

  • 若是这样作可以带来更好的数据并行,将一个大的kernel拆分红多个小的kernel。
  • 若是内存的流量可以减小并且一样能保证并行性,能够将多个kernel合并成一个kernel,例如workgroup的尺寸可以足够地大。

 

      8.2 编译选项

 

         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标准函数都有很高的精度要求。

8.3 一致性 vs. 快速 vs. vs. 内部的数学函数

         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个选项。

 

  • 使用快速函数时,在调用函数clBuildProgram时使能-cl-fast-relaxed-math。
  •  使用内部的数学函数:
    •   许多函数有内部实现,好比:native_cos, native_exp,native_exp2, native_log, native_log2, native_log10, native_powr,native_recip, native_rsqrt, native_sin, native_sqrt, native_tan ;
    •   下面使用内部数学函数的例子:

              原始的: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中的函数

低,与供应商有关

对精度损失不敏感的状况下的图像,音频,和视觉用例中

 

8.4 循环展开

         循环展开一般是一个好方法,由于它可以减小指令执行的耗时从而提升性能。Adreno编译器一般能基于试探法自动地将循环展开。然而,有时候编译器选择不将循环彻底展开,由于基于考虑到,寄存器的分配预算,或者编译器由于缺乏某些信息不能将它展开等因素。在这些状况下,开发者能够给编译器一个提示,或者手动的强制将循环展开,以下所示:

  • kernel可使用__attribute__((opencl_unroll_hint))或者__attribute__((opencl_unroll_hint(n))) 给出提示。
  • 另外,kernel能够直接使用#pragma unroll展开循环。
  • 最后一个选择是手动展开循环。

 

8.5 避免分支

        

         通常地,当在同一个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操做来替代控制流逻辑。

 

8.6 处理图像边界

         许多操做可能会获取图像边界外的像素点,好比滤波,变换等。为了更好地处理边界,能够考虑下面的选择:

  • 若是可能的话,对图像进行扩边。
  •  使用带有合适的采样器的image对象(texture引擎会自动处理这个)
  • 编写单独的kernel函数去处理边界,或者让CPU处理边界。

 

8.7 32位 vs. 64位GPU内存访问

         从Adreno A5X GPU开始,64位操做系统逐渐成为主流,并且许多的Adreno GPU支持64位操做系统。64位操做系统中最重要的改变是内存空间将能彻底覆盖4GB,并且CPU支持64位指令集。

         当GPU能够获取64位内存空间时,它的使用将会引发额外的复杂性,并且可能会影响性能。

        

8.8 避免使用size_t

         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位。在这种状况,编译器尝试使用一个短的数据类型来替代。可是,更好的方法是,给编译器提供关于数据类型的最充分的信息,而后编译器能够产生更好的优化代码。

 

8.9 通常的内存空间

         OpenCL2.0 介绍了一个新的特性,叫作通常性的内存地址空间,在这个地址空间中,指针不须要指定它的地址空间,在OpenCL2.0以前,指针必须指定它的地址空间,好比指定为是local,private,或者global。在通常性的地址空间中,指针能够动态地被指定为不一样的地址空间。

        

         这个特性下降了开发者的代码基础并且能重复使用已经存在的代码,使用通常性的内存地址空间会有轻微的性能损失,由于GPU SP硬件须要动态的指出真正的地址空间。若是开发者清楚知道变量的内存空间,建议清晰地定义内存地址。这将会减小编译器的歧义,从而会有更好的机器代码进而提高性能。

 

8.10 其余

 

         还有不少其余的优化技巧,这些技巧看起来很小,可是一样能够提升性能,这些技巧以下所示:

  • 已经计算过的数据,并且不会在kernel中被改变的。
    •   若是一个数据能够在外面host端)计算好,那么放到kernel中计算会很浪费。
    •   已经计算好的数据能够经过kernel参数传递给kernel,或者用#define的方式。

 

  • 使用快速的整型的内嵌函数。使用mul24计算24位的整型乘法,和使用mad24计算24位的整型乘加。
    •   Adreno GPU的内部硬件支持mul24,而32位的整型乘法须要用更多的指令模拟。
    •   若是是在24位范围内的整型数据,使用mul24会比直接使用32位的乘法更快。
  • 减小EFU函数
    •   好比,像r=a/select(c,d,b<T)这样的代码(其中a,b和T是浮点变量,c和d是常数),能够写成r= a * select(1/c,1/d,b<T),这样会避免EFU中倒数函数,由于1/c和1/d能够在编译器编译阶段计算出来。

 

  • 避免除法操做,特别是整型的除法。
    •   整型的除法在Adreno GPUs上是极其耗时的。
    •   不使用除法,可使用native_recip计算倒数,像8.3节描述的那样。 
  • 避免整型的模操做,这个也很耗时。

 

  • 对于常数的数组,好比说查找表,滤波tap等,在kernel的外面进行声明。

 

  • 使用mem_fence 函数来分开或者组合代码段。
    •   编译器会从全局优化的角度,使用复杂的算法产生最优的代码。
    •   mem_fonce 能够用来阻止编译器混排和混合前面或者后面的代码。
    •   mem_fonce 可让开发者单独操做代码的某个部分来进行优化和调试。

 

  •  使用位移操做替换乘法。
相关文章
相关标签/搜索