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 可讓開發者單獨操做代碼的某個部分來進行優化和調試。

 

  •  使用位移操做替換乘法。
相關文章
相關標籤/搜索