免费做网站软件下载/网站运营策划书
OpenCL内核性能优化
- 8 内核性能优化
- 8.1 内核融合或分裂
- 8.2编译器选项
- 8.3 Conformant vs. fast vs. vs. native math functions
- 8.4循环展开
- 8.5 避免分支
- 8.6 处理图像边界
- 8.7 32位与64位GPU内存访问
- 8.8 避免使用size_t
- 8.9 通用内存地址空间
- 8.10 其它
8 内核性能优化
本节介绍有关内核优化的技巧。
8.1 内核融合或分裂
一个复杂的应用程序可能包含许多阶段。对于OpenCL的移植和优化,人们可能会问应该开发多少个内核。这很难回答,因为涉及到很多因素。以下是一些高层次的标准:
- 内存和计算之间的良好平衡
- 高频以消除延迟
- 没有寄存器溢出
要实现这些目标,可以采取以下措施:
- 将一个大内核分解成多个小内核,如果这样做能产生更好的数据并行化。
- 将多个内核融合为一个内核(内核融合),如果内存流量可以减少,并行化可以维护,例如,工作组大小相当大。
8.2编译器选项
OpenCL支持在OpenCL规范5.6.4节中定义的一些编译器选项。编译器选项通过API clCompileProgram和clBuildProgram传递进来。多个选项可以组合:
clBuildProgram( myProgram, numDevices, pDevices, “-cl-fast-relaxed-math ”, NULL, NULL );
有了这些选项,开发人员可以根据自己的目的启用一些功能。例如,使用-cl-fast-relaxed-math允许使用快速数学来构建内核,而不是使用符合OpenCL的数学,后者对OpenCL规范有更高的精度要求。
8.3 Conformant vs. fast vs. vs. native math functions
OpenCL标准在OpenCL C语言中定义了许多数学函数,默认情况下,所有的数学函数必须满足IEEE 754单精度浮点数学要求,这是OpenCL规范所要求的。Adreno图形处理器有一个内置的硬件模块,基本功能单元(EFU),以加速一些原始数学函数。许多不是由EFU直接支持的数学函数,要么通过结合EFU和ALU操作进行优化,要么由编译器使用复杂的算法进行模拟。表8-1显示了OpenCL-GPU数学函数的列表,根据它们的相对性能分类。最好使用高性能的函数,例如a类中的函数。
或者,如果应用程序对精度不敏感,开发人员可以选择使用本机或快速数学函数,而不是一致的数学函数。表8-2总结了使用数学函数的三种选择。
-
对于快速数学,在clBuildProgram调用中启用-cl-fast- relaxation -math。
-
使用本地数学函数:
-
具有本地实现的数学函数有native_cos、native_exp、native_exp2、native_log、native_log2、native_log10、native_power、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.4循环展开
循环展开通常是一个很好的实践,因为它可以降低指令执行成本并提高性能。Adreno编译器通常可以根据一些启发式自动展开循环。但是,根据寄存器分配预算等因素,编译器也可能选择不完全展开循环,或者编译器由于缺乏某些知识而不能展开循环。在这些情况下,开发人员可以给编译器一个提示,或者手动强制它展开循环,如下所示:
- 内核可以使用__attribute__((opencl_unroll_hint))或__attribute__((opencl_unroll_hint(n)))给出提示。
- 或者,内核可以使用#pragma unroll指令来展开循环。
- 最后一个选项是手动展开循环
8.5 避免分支
通常,当同一频度中的工作项遵循不同的执行路径时,gpu的效率是不高的。对于不同的分支,一些工作项可能必须被掩盖,从而导致较低的GPU占用率,如图所示。另外,条件检查代码(如if-else)通常会调用昂贵的控制流硬件逻辑。
有一些方法可以避免或减少分支和条件检查。在算法级别上,可以将属于一个分支的工作项分组为一个非分支项。在内核级别,一些简单的分支/条件检查操作可以转换为快速的ALU操作。9.2.6小节展示了一个例子,其中由昂贵的控制流逻辑处理的三元操作被转换为ALU操作。另一种方法是使用像select这样的函数,它可以使用快速的ALU操作而不是控制流逻辑。
8.6 处理图像边界
许多操作可以访问图像边界之外的像素,如过滤、变换等。为了更好地处理边界问题,应考虑以下方案:
- 如果可能的话,将图像放在前面。
- 使用适当的采样器(纹理引擎自动处理)的图像对象。
- 编写单独的内核来处理边界,或者让CPU处理边界。
8.7 32位与64位GPU内存访问
从Adreno A5x图形处理器,64位操作系统正在成为主导,许多Adreno图形处理器支持64位操作系统。64位操作系统中最重要的变化是内存空间可以大大超过4GB,并且CPU支持64位指令集。
虽然GPU可以访问64位内存空间,但它的使用会带来额外的复杂性,并可能导致性能损失。
8.8 避免使用size_t
64位内存地址在很多情况下会给OpenCL内核编译带来复杂性,开发人员需要小心。强烈建议避免在内核中将变量定义为size_t类型。对于64位操作系统,在内核中定义为size_t的变量可能必须被视为64位长。Adreno gpu必须使用两个32位寄存器来模拟64位。因此,使用size_t类型的变量需要更多的寄存器资源,这通常会由于较少的活动波和较小的工作组规模而导致性能下降。因此,开发人员应该使用32位或更短的数据类型,而不是size_t。
对于OpenCL中返回size_t的内置函数,编译器可能会尝试根据其知识派生和限制作用域。例如,get_local_id以size_t的形式返回结果,但是local_id永远不会超过32位。在这种情况下,编译器使用短数据类型代替。但是,为编译器提供有关数据类型的最佳知识通常是一种良好的实践,以便它能够生成最佳的代码。
8.9 通用内存地址空间
OpenCL 2.0引入了一个称为通用内存地址空间的新特性,其中指针不需要指定其地址空间。在OpenCL 2.0之前,指针必须指定它的内存地址空间,可以是本地的、私有的或全局的。使用通用内存地址空间,可以动态地将指针分配给不同的内存地址空间。
虽然这个特性允许开发人员减少他们的代码库并重用现有代码,但是使用通用内存地址空间可能会有轻微的性能损失,因为GPU SP硬件需要动态地计算出真正的内存空间。如果开发人员清楚地知道内存地址空间,建议准确地定义内存地址空间。这将减少编译器的歧义,并产生更好的机器码和改进的性能。
8.10 其它
以下是许多其他优化技巧,它们看起来很小,但可以提高内核性能:
-
预先计算在内核中不会改变的值。
- 计算一个可以在内核外预先计算的值是很浪费的。
- 预先计算的值可以通过内核参数或使用#define传递给内核。
-
使用快速整数内置函数。使用mul24进行24位整数乘法,使用mad24进行24位整数乘法和累加。
- Adreno图形处理器有本地硬件支持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)重写,它使用1/c和1/d的避免了1/EFU函数,可以在编译时由编译器优化产生。
-
避免除法操作,尤其是整数除法。
- 整数除法在Adreno gpu上是非常昂贵的。
- 不使用除法,而是使用native_recip执行一个交互操作,如8.3章节所述。
-
避免整数模块操作,这是昂贵的。
-
对于常量数组,如查找表、过滤器点击等,请在内核范围之外声明它们。
-
使用mem_fence函数分割/分组代码段。
- 编译器有复杂的算法来从全局优化的角度生成最优的代码。
- mem_fence可以用来防止编译器打乱/混合前后的代码。
- mem_fence允许开发人员操作代码的一部分以进行分析和调试。
-
使用位移位运算代替乘法运算