Qualcomm_Mobile_OpenCL.pdf translation -9-OpenCL optimization study of the use case

 

         In this chapter, we will use some examples to show how to use the technology discussed in the previous section to be optimized. In addition to simply show some small fragment of code, there are two well-known image filtering processing, filtering and Epsilon Sobel filtering process before that section will be discussed step by step optimization.

      Code sample 9.1 application

      9.1.1 Lifting Scheme

         This example shows how to simplify the code to improve performance. Given a picture, it conducted box 8x8 blur filter.

 

         Before the optimization of the original kernel of code:

         __kernel void ImageBoxFilter(__read_only image2d_t source,

                                __write_only image2d_t dest,

                                sampler_t sampler)

    {

           ... // variable declaration

           for( int i = 0; i < 8; i++ )

           {

              for( int j = 0; j < 8; j++ )

              {

                  coor = inCoord + (int2) (i - 4, j - 4 );

                  // !! read_imagef is called 64 times per work item

                  sum += read_imagef( source, sampler, coor);

              }

           }

           // Compute the average

           float4 avgColor = sum / 64.0f;

           ... // write out result

}

 

         To reduce the texture access the kernel function of the above two steps. A first step of calculating an average of each work item of a 2x2, and store the result in a temporary image. The second step uses a temporary image for the final calculation.

 

         kernel modified

        

         // First pass: 2x2 pixel average

    __kernel void ImageBoxFilter(__read_only image2d_t source,

                                __write_only image2d_t dest,

                                sampler_t sampler)

    {

       ... // variable declaration

       // Sample an 2x2 region and average the results

       for( int i = 0; i < 2; i++ )

       {

           for( int j = 0; j < 2; j++ )

           {

              coor = inCoord - (int2)(i, j);

              // 4 read_imagef per work item

              sum+=read_imagef(source,sampler,inCoord-(int2)(i,j));

           }

       }

       //equivalent of divided by 4,in case compiler does not         //optimize

       float4 avgColor = sum * 0.25f;

       ... // write out result

}

 

// Second Pass: final average

__kernel void ImageBoxFilter16NSampling( __read_only                                                         image2d_t source,

                                       __write_only image2d_t dest,

                                          sampler_t sampler)

{

       ... // variable declaration

       int2 offset = outCoord - (int2)(3,3);

       // Sampling 16 of the 2x2 neighbors

       for( int i = 0; i < 4; i++ )

       {

           for( int j = 0; j < 4; j++ )

           {

              coord = mad24((int2)(i,j), (int2)2, offset);

              // 16 read_imagef per work item

              sum += read_imagef( source, sampler, coord );  }

           }

           // equivalent of divided by 16, in case compiler does not          //  optimize

           float4 avgColor = sum * 0.0625;

           ... // write out result

}

 

         Each work item algorithm modified only obtain 20 (16 + 4) th image buffer, obviously many times less than the original acquired 64 read_imagef.

9.1.2 Loads and stores to quantization

         This example shows how to load and store the quantized and better use of bandwidth in Adreno GPUs.

        

         Pre-optimized source code:

         __kernel void MatrixMatrixAddSimple( const int matrixRows,

                                          const int matrixCols,

                                          __global float* matrixA,

                                          __global float* matrixB,

                                          __global float* MatrixSum)

    {

           int i = get_global_id(0);

           int j = get_global_id(1);

       // Only retrieve 4 bytes from matrixA and matrixB.

       // Then save 4 bytes to MatrixSum.

       MatrixSum[i*matrixCols+j] =

                  matrixA[i*matrixCols+j] + matrixB[i*matrixCols+j];

    }

 

         The revised Code

    __kernel void MatrixMatrixAddOptimized2( const int rows,

                                              const int cols,

                                              __global float* matrixA,

                                              __global float* matrixB,

                                          __global float* MatrixSum)

    {

       int i = get_global_id(0);

       int j = get_global_id(1);

       // Utilize built-in function to calculate index offset

       int offset = mul24(j, cols);

       int index = mad24(i, 4, offset);

      

       // Vectorize to utilization of memory bandwidth for            //performance gain.

       // Now it retieves 16 bytes from matrixA and matrixB.

       // Then save 16 bytes to MatrixSum

       float4 tmpA = (*((__global float4*)&matrixA[index])); //                      //Alternatively

       vload and vstore can be used in here

       float4 tmpB = (*((__global float4*)&matrixB[index]));

       (*((__global float4*)&MatrixSum[index])) = (tmpA+tmpB);

       // Since ALU is scalar based, no impact on ALU operation.

}

 

         New kernel used to achieve the quantized float4 load / store. Because vectorization, the new kernel before the global work size is the kernel of 1/4.

9.1.3 use image replacement buffer

         This example is the dot product of a given vector calculation 5 million pairs each pair of vectors. Original code is the use of the object buffer, after modification, using texture objects (read_imagef) to improve access performance of frequently used data. This is a simple example, but using the technology can be applied to many similar cases, the access buffer object under these circumstances is better texture Object Access valid.

 

Before the optimization of the original kernel function

Optimized kernel function

__kernel void DotProduct(__global const

float4 *a, __global const float4

*b,__global float *result){ // a and b

contain 5 million vectors each

// Arrays are stored as linear buffer

in global memory

result[gid] = dot(a[gid], b[gid]);

}

__kernel void DotProduct(__read_only

image2d_t c, __read_only image2d_t d,

__global float *result){

// Image c and d are used to hold the data

instead of linear buffer

// read_imagef goes through the texture

engine

int2 gid = (get_global_id(0),

get_global_id(1));

result[gid.y * w + gid.x] =

dot(read_imagef(c, sampler, gid),

read_imagef(d, sampler, gid));

}

 

9.2 Epsilon filtering

         Epsilon filtering is widely used in image processing to reduce mosquito noise, which is one kind of defect occurs in the high frequency region (such as side image). This is non-linear filtering and low pass filtering based on the point of support of the spatial variation, and only a specific pixel threshold value is filtered.

         In this example, only it is used in the Epsilon filtering YUV Y channel image, because the noise can often be seen in this channel. It is assumed that the Y channel are stored continuously (in NV12 format), stored separately from the UV channel. Figure9-1 illustrates two basic steps of the filtering algorithm:

n for a pixel to be filtered, is calculated in the range of 9x9 to the center of all the pixels pixel absolute difference.

If n is less than the absolute value of a threshold value, the average of neighboring pixels to be used. Note that the threshold is usually constant in a predefined program.

 

 

 

 

 

                                                        9-1 Epsilon filtering algorithm of FIG.

 

9.2.1 The first implementation

         The program calculates the YUV image resolution of 3264x2448 (3264 is the width, height 2448), each pixel is an 8-bit data width. Herein is based on said performance data Snadragon 810 (MSM8994, Adreno 430) of the high-performance mode.

         Here are the parameters and policies implemented for the first time:

  •  使用OpenCL的image对象替代buffer对象。
    •   使用image替代buffer能够避免边界检测和充分利用Adreno GPUs中的L1 cache。
  •  使用CL_R | CL_UNORM_INT8图片格式/数据类型
    •   单个通道,比如这里的Y通道,而且读取到SP的像素点,都被Adreno GPUs内建的texture管道归一化到[0,1]。
  • 每个work item 处理一个输出的元素。
  •  使用2D的kernel,global 的worksize大小被设置为[3264,2448].

 

         在这个实现中,每个work item需要获取81个浮点类型的像素点。在Adreno A430上这个实现的性能数据作为进一步优化的基准。

9.2.2数据包优化

         通过比较计算量和数据装载量,这个例子明显是内存瓶颈。因此,主要的优化是如何提升数据的装载效率。

 

         首先需要注意到,使用32位的浮点来表示像素点是对内存的浪费。对许多图像处理算法来说,8位或者16位的数据类型就够用了。因为Adreno GPUs对16位浮点类型有内嵌的硬件支持,比如half或者fp16,所以可以使用下面的优化方法:

  •  使用16位浮点数据类型来替换32位浮点
    •   现在每个work item获取81个half 数据

 

  • 使用CL_RGBA| CL_UNORM_INT8图像类型或者数据类型
    •   使用CL_RGBA 去装载4个通道能更好的的利用TP的带宽。
    •   使用read_imagef 替代read_imageh。TP会将数据自动转换成16位half数据。
  • 每个work item
    •   每行使用3个half4向量
    •   输出一个已经被处理的像素点
    •   对于每一个输出的像素点,访问的内存数是3x9 = 27 (half4)

 

  •  性能提升1.4x

        

        

                            图9-2 使用16位 half类型(fp16)打包数据

 

      9.2.3  向量化装载/存储优化

 

         在之前的步骤中,只要计算一个输出像素,就有很多邻居像素点需要装载。可以通过额外装载一些像素点,那么久可以滤波更多的像素点,如下所示:

  • 每个work item
  • 每一行读3个half4的向量
    •   输出4个像素
    •   每个输出像素点需要获取的内存数量是:3x9/4 = 6.75(half4)
  • 全局的work size:(width/4)x height
  • 对每一行循环展开。
  •  在每一行内,使用活动窗口的方法。

        

                            图9-3 每一个work item滤波更多的像素点

 

         图9-3 阐述了通过装载额外的像素点,处理多个像素的方法。以下是几个具体的步骤:

                   Read center pixel c;//读取中心像素点c

                   For row = 1 to 9, do://从行1到行9,执行以下操作

                   read data p1;//读取数据p1

                   Perform 4 computation with pixel c;//对c执行1次计算

                   read data p2;//读取数据p2

                   Perform 4 computations with pixel c;  //对c执行4次计算

                   read data p3;//读取数据p3

                   Perform 4 computations with pixel c;  //与c执行4次计算

                   end for//结束循环

                   write results back to pixel c.//将结果写回中心像素点

 

                  通过这些步骤,性能比基准性能提高了3.4x。

 

9.2.4 进一步提升每个work item的工作量

         提升每个work item的工作量,性能可能会有所提升。下面是一些可选的操作:

  • 读取多个half4向量,提升输出的像素个数到8个。

 

n  全局的work size : width/8 xheight

n  每个work item

  • 每一行读4个half4 向量。
  • 输出8个像素
  • 每个输出像素点需要访问的内存次数为:4x9/8 = 4.5 (half4)

                  

        

                  图9-4 每个work item 处理8个像素点

 

         这些改变仅提升了很少的性能(仅提升0.1x),下面是为什么这么做不起作用的原因:

 

  • 在cache命中率上没有很大的改变,这个在之前的步骤上已经做的很好了。
  • 更多的寄存器被使用,导致了更少的wave,这将会损失平行性和延迟的隐藏性。

 

         为了实验的目的,下面的方法可以装载更多的像素点:

  • 读取更多的half4的向量,提升输出像素点的个数到16.
  • 全局的work size: width/16 xheight

 

         图9-5 表示了每个work item 会执行以下几个步骤:

  • 每行读6个half4个向量。
  • 输出16个像素点
  • 每个输出像素点需要访问的内存次数为:6x9/16 = 3.375(half4)

 

         经过这些改变后,性能从3.4x退化到0.5x。装载更多的像素点进入kernel中会引起寄存器溢出,这将会严重地损害性能。

 

 

                   图9-5 每个work item处理16个像素

9.2.5 使用本地内存优化

         本地内存比全局内存有更短的延迟,因为本地内存是片上内存。一个选择是,将像素点装载进本地内存,避免重复从全局内存中加载。而且,对于要处理的中心元素,由于9x9范围的滤波,所以它周围的元素也需要,所以如图9-6所示,装载进内存。

        

 

         图9-6 使用本地内存进行Epsilon 滤波

 

         表9-1 列出了两种情况的设置和他们的性能。整体性能比原始的要更好,然而从9.3.4节中可以看出,并没有获得最好的性能。

 

         表9-1 使用本地内存的性能

 

情况1

情况2

workgroup

8x16

8x24

本地内存大小(byte)

10x18x8=1440

10x26x8 = 2080

性能

2.4x

2.8x

 

         如7.1.1节所讨论的,本地内存使用时,通常需要使用barrier进行workgroup中work item之间的同步,这样会导致性能不比使用全局内存时的好。而且,如果同步导致许多开销的话,它可能会有更差的性能。在这种情况下,如果全局内存有高cache命中率的话,那么全局内存可能是一个更好的选择。

        

 

9.2.6 分支操作的优化

         Epsilon滤波需要进行像素之间的比较,如下:

 

                  Cond = fabs(c -p) <= (half4)(T);

       sum += cond ? p : consth0;

       cnt += cond ? consth1 : consth0;

 

         三元素符 ?: 是发生在硬件上的分支,因为同一个wave中的不是所有的fiber都会执行相同的分支。分支操作可以被ALU 操作替代,如下所示:

 

                  Cond = convert_half4(-(fabs(c -p) <= (half4)(T)));

       sum += cond * p;

       cnt += cond;

 

         这种优化方法之前就应用在9.2.2章中描述过的一个例子中,性能从3.4x提升到5.4x。

         这个操作的关键差异是,新代码是在高度并行的ALU下执行,而且在同一个wave中所有的fiber基本上执行的是同一块代码,尽管变量Cond可能有不同的值,而原来的代码会使用非常耗时的硬件逻辑来处理分支。

 

9.2.7 总结

 

         表9-2总结了优化的步骤和性能数据。最初,算法是内存瓶颈的。通过数据打包,向量化装载,它变成ALU瓶颈。总的来说,对于这个例子的关键是优化装载数据的方式。许多其他的内存瓶颈的用例可以使用相似的技术来加速。

 

表9-2 优化和性能的总结

 

步骤

优化方法

imag类型

kernel中的数据类型

向量化操作

速度提升

1

最初的GPU实现

CL_R |

CL_UNORM_INT8

float

1-pixlel/work item

 

2

在kernel中使用half类型

CL_R |

CL_UNORM_INT8

half

1.0x

3

数据打包

CL_RGBA|

CL_UNORM_INT8

1.4x

4

向量化处理

循环展开

4-pixel/work item

(halt4 output)

3.4x

5

每个work item处理更多的像素

8-pixel/work item

3.5x

6

每个work item处理更多的像素

16-pixel/work item

0.5x

4-1

使用LM

(workgroup 大小为8x16)

4-pixel/work item

 

2.4x

4-1-1

使用LM

(workgroup 大小为8x24)

2.8x

4-1-2

使用LM移除分支操作,workgroup大小为8x24

2.9x

4-2

移除分支操作

5.4x

 

         表9-3中展示了在三种分辨率的图片上使用Epsilon滤波后的OpenCL性能。从图中可以看出,越大的图像,性能提升越大。对于一个3264x2448的图像,可以看到有5.4x的性能提升,相比之言,在512x512的图像上,优化后的代码性能比最初的OpenCL代码的性能,只有4.3x的提升。这是很容易理解的,因为在不考虑任务量的情况下,消耗的时间与kernel的执行时间是正相关的,而且任务量越大,他在整个性能数据中的权重越小。

 

表9-3 不同分辨率的图片的性能统计

图片的分辨率

512x512

1920x1080

3245x2448

像素点的个数

0.26MP

2MP

8MP

设备(A430)

GPU的最初实现结果

1x

1x

1x

GPU优化后的结果

4.3x

5.2x

5.4x

 

9.3 Sobel 滤波

         Sobel滤波,也被称作Sobel操作,用在很多图像处理和计算机视觉算法的边界检测中。它使用两个3x3的kernels与原始图片结合,近似得出导数。这里有两个kernel:一个负责水平方向,一个负责垂直方向,如图9-7所示:

        

 

         图9-7 Sobel滤波的两个方向操作

 

      9.3.1 算法优化

         Sobel的滤波是一个可分离的滤波器,可以如下分解:

        

        

         图9-8 Sobel 滤波分离

 

         相比于不可分离的2D滤波,一个可分离的2D滤波器可能将复杂度从O(n2)降低到O(n)。由于2D的高复杂性和计算的耗时,理想的情况就是使用可分离的滤波器。

 

      9.3.2 数据打包的优化

         尽管可分离的滤波,已经减少了很多计算,但是每一个要被滤波的像素点所需要的像素点个数是一样的,比如对3x3kernel来说,需要8个邻居像素点加上中心像素点。可以明显地看出来,这个一个内存瓶颈的问题。所以,如果有效的将像素点装载进GPU是性能优化的关键。下面的图片中阐述了3种可以使用的选择:

 

 

 

 

        

         图9-9 每个work item处理一个像素点,每个kernel装载3x3个像素点

 

         图9-10 处理16x1 个像素点,装载18x3个像素点

 

         图9-11 处理16x2个像素点,装载18x4个像素点

 

         下面的表格中总结了每种情况下总的字节数和平均字节数。对于表9-9所述的第一个种情况,每个work item只对一个像素点进行Sobel滤波。随着每个工作项处理的像素点的个数的增加,需要装载的数据数量将会减少,如9-10和9-11所示。这将会减少全局内存到GPU的数据量,从而提升性能。

表9-4 3种情况下数据装载/存储的数量

 

1 pixel/work item

16x1 pixels/work item

16x2 pixels/work item

总的输入字节数

9

54

72

平均输入字节数

9

3.375

2.25

平均存储字节数

2

2

2

 

9.3.3 向量化的装载/存储优化

         对于16x1 和16x2 这两种情况,装载/存储的次数可以通过使用OpenCL中的向量化装载存储函数进行进一步的减少,比如float4,Int4和char4等。表9-5表示了使用了向量化的情况下,需要的装载/存储的次数(假设像素的数据类型是8-bit)。

 

         表9-5 使用向量化的装载/存储方法需要的装载和存储次数

        

 

16x1 向量化

16x2 向量化

装载

6/16=1.375

8/32=0.374

存储

2/16=0.125

4/32 = 0.125

 

         下面是一小段向量化装载的代码:

         short16 line_a = convert_short16(as_uchar16(*((__global uint4

                     *)(inputImage+offset))));

 

         如下,在边界处,需要装载两个像素

         short2 line_b = convert_short2(*((__global uchar2 *)(inputImage                     + offset +16)));

        

         注意:每个工作项处理像素点的数量提升可能会导致严重的寄存器使用空间的压力,从而导致寄存器溢出到私有内存和性能下降。

 

9.3.4 性能统计和总结

         在使用了两种优化步骤之后,可以看到性能的显著提升,如图9-12所示,图中在MSM8994(Adreno418)上的原始算法性能(每个work item处理单个像素点)被归一化为1.

        

 

         图9-12 通过使用数据打包和向量化装载/存储带来的性能提升

 

         为了总结,下面是这个用例优化的几个关键点:

  • 数据打包提升了内存访问效率
  • 向量化装载/存储是减少内存繁忙的关键点。
  • 在这种情况下,优先选择更短的数据类型比如整型或者char型。

 

         在这种情况下,没有使用本地内存。数据打包和向量化的装载/存储已经最小化了可复用数据的重叠。因此,使用本地内存并不能提升性能。

         可能还存在其他的提升性能的选项,比如使用texture来替换global buffer。

 

 

      9.4 总结

 

         在这一章节中提供了一些简单的例子和代码片段来证实了前几章说明的优化规则,并且指出了性能是如何改变的。开发者可以尝试在真实的设备上使用这些步骤。由于编译器和驱动的升级,这些结果可能不会被准确的重现。但是,一般地,通过这些优化步骤,肯定会实现同样的性能提升。

Guess you like

Origin www.cnblogs.com/xiajingwang/p/11120774.html