以下优化方法是翻译自arm\_mali\_bifrost\_and\_valhall\_opencl\_developer\_guide\_101574\_0403\_00\_en.这篇官方文档。链接:
Documentation - Arm Developerdeveloper.arm.com
针对biforest和valhall架构的GPU进行的优化方法。
1.通用优化方法:
- 用最好的processor来进行任务处理
- GPU是用来进行并行处理的
- CPU是用来高速串行应用
- 所有的应用包括控制部分或者计算部分,为了最佳性能请选择合适的处理器
- 控制部分用通用语言,通用处理器
- 并行计算部分用GPU
- 只编译一次程序
- 排列多个work-item
- 处理大规模数据
- 最好数据可以align到64B,对cache-line友好
- 在精度要求不高情况下,用低精度的数据类型
- image和buffer 在opencl中有区别,尽量用image
- 如果算法可以向量化,用buffer,将两个buffer,merge成一个buffer是不科学的,没有收益
- 如果算法需要插值或者自动边缘剔除用image
- 使用异步操作,CPU和GPU之间用异步
- 在操作中尽量避免处理器之间的互动
- 尽量避免在执行过程中使用clWaitForEvent等api,会block住线程,如果非要利用中间数据,使用双缓冲策略进行 -批处理提交kernel:暂时不清楚原理
2.kernel 优化
- 最小的groupsize是warp size或者是他的整数倍,如果用了barrier,小的groupsize是可以的,如果不确定是何种size比较有效,可以设置成NULL group的shape也是需要实验出来结果的
- 需要同步的情况下,通常需要特定的group size,如果不需要同步的情况下,就是不实用共享内存的情况下,性能最好的情况是使用了最大的资源数目,如果没有barrier的情况下concat两个kernel是有性能提升的
- 用大量的线程数目来掩盖指令的执行延迟
- shader core(ALU?核心?)所执行的线程取决于内核函数中使用的活跃寄存器数目,使用的寄存器越多,同时执行的线程数越少。活跃寄存器数目取决于程序的复杂程度,kernel程序越复杂,用编译器编译出来的变量数目越多。
- 为了减少寄存器数目,你可以减少你kernel中的活跃变量数目,或者使用大的NDRange,会有很多的work-items,(不清楚这个,可能是减少了每个线程的使用寄存器数目,从而增加了可以并行的线程数目)
- 使用离线编译器来产生你kernel用的统计信息
3.优化memory
- 使用线性访问和高局部性的数据结构,这些提升了可缓存性和性能,
- 使用cl\_arm\_thread\_limit\_hint这个来优化,但是没有看懂啥意思,还只支持bifrostGPU
4.代码优化
- vector load 和 vector store,每个operation 可以load 128bit得数据,不要超过128bit的数据,这会降低性能,这是为什么呢?
- 每个load 进行最多的操作,reuse data,用很多的计算指令
- 尽量避免转换到、来:float和int,会消耗很多
- 实验多次来确定最优的kernel,包括:使用小的数据type,减少load和store的数据量,
- 数据排布变换,来最大化cache使用率
- 尽量使用128bit的vector load
- 不要用divide这种数学运算
- 使用vector load 和vector store
- 利用 \_sat() functions 而不是 min() or max()
- 太多的live variable可能影响性能和每个core执行的可以并行的threads
- 不要在kernel 里面计算constants
- 使用离线编译器来进行统计
- 使用内置的函数,是快速的硬件指令
- 小心使用cache,每个线程分配的cache非常少,小心使用,最大化空间局部性和时间局部性(spatial locality and temporal locality)
- 大规模的顺序的读取、写入数据比较好
5.执行时期的优化
- cache 住binaries在存储设备上,不用每次都编译
6.减少串行计算的影响
- 使用memory map 而不是 memory copy 来交换数据,这个api值得好好看一下
- 让message尽量小
- 使用memory size 是一个power of two
- 通用处理器处理串行工作
- 利用优化过的clEnqueueFillBuffer和clEnqueueFillImage来fill buffer和Image
7.针对Bifrost and Valhall的特殊优化
- 相邻分支合并访问,在Mali Bifrost和Valhall GPU中,相邻线程组排列在一起。相邻线程中的标量指令可以并行执行,因此GPU可同时对多个数据元素进行操作。标量在线程中执行,并且必须在锁步中运行。如果着色器包含分支,例如if语句或循环,则相邻线程中的分支可以采用不同的方式。算术单元不能同时执行分支的两侧。拆分两个操作,降低了处理速度。为了避免这种性能下降,请尝试确保所有相邻线程都以相同的方式分支。下表显示了每个Mali Bifrost和Valhall GPU的相邻线程数。
- 每个线程有64个32bit的寄存器,64位的数据用两个相邻的32位的寄存器来表示,如果一个线程使用超过64个寄存器,编译器将把寄存器数据存储在memory中,减少带宽不好
- 向量化8bit和16bit数据的操作,使用fp16会比f32增加一倍,因为一个register可以存储两个数据,使用char可以存储4个char。
- 32位数据的向量化和非向量化运算是性能一样的,8和16bit的数据向量化才有意义,因为寄存器都是32位的
- 使用128bit的load和store
- 如果每相邻四线程中的所有线程都从同一缓存行加载,则加载和存储操作会更快,合并访问呗
- 如果可能使用32位的数学运算代替64bit的算数运算
- 如果系统允许,使用共享虚拟内存,就不用同步了
- 寻找计算单元和存储单元之间的平衡
作者:夏津
原文链接:https://zhuanlan.zhihu.com/p/346591412