1. 程式人生 > >【平行計算-CUDA開發】GPGPU OpenCL/CUDA 高效能程式設計的10大注意事項

【平行計算-CUDA開發】GPGPU OpenCL/CUDA 高效能程式設計的10大注意事項

1.展開迴圈

如果提前知道了迴圈的次數,可以進行迴圈展開,這樣省去了迴圈條件的比較次數。但是同時也不能使得kernel程式碼太大。

  迴圈展開程式碼例子:

複製程式碼
 1 #include<iostream>
 2 using namespace std;
 3 
 4 int main(){
 5     int sum=0;
 6     for(int i=1;i<=100;i++){
 7         sum+=i;
 8     }
 9 
10     sum=0;
11     for(int i=1;i<=100;i=i+5){
12         sum+=i;
13 sum+=i+1; 14 sum+=i+2; 15 sum+=i+3; 16 sum+=i+4; 17 } 18 return 0; 19 }
複製程式碼

2.避免處理非標準化數字

  OpenCL中非標準化數字,是指數值小於最小能表示的正常值。由於計算機的位數有限,表示資料的範圍和精度都不可能是無限的。(具體可以檢視IEEE 754標準,http://zh.wikipedia.org/zh-cn/IEEE_754)

  在OpenCL中使用非標準化數字,可能會出現“除0操作”,處理很耗時間。

  如果在kernel中“除0”操作影響不大的話,可以在編譯選項中加入-cl-denorms-are-zero,如:

    clBuildProgram(program, 0, NULL, "-cl-denorms-are-zero", NULL, NULL);

3.通過編譯器選項傳輸常量基本型別資料到kernel,而不是使用private memory

  如果程式中需要給kernel 傳輸常量基本型別資料,最好是使用編譯器選項,比如巨集定義。而不是,每個work-item都定義一個private memory變數。這樣編譯器在編譯時,會直接進行變數替換,不會定義新的變數,節省空間。

  如下面程式碼所示(Dmacro.cpp):

複製程式碼
1 #include<stdio.h>
2 int main()
3 { 4 int a=SIZE; 5 printf("a=%d, SIZE=%d\n",a,SIZE); 6 return 0; 7 }
複製程式碼

  編譯:

  g++ -DSIZE=128 -o A Dmacro.cpp

 4.如果共享不重要的話,儲存一部分變數在private memory而不是local memory

   work-item訪問private memory速度快於local memory,因此可以把一部分變數資料儲存在private memory中。當然,當private memory容量滿時,GPU硬體會自動將資料轉存到local memory中。

5.訪問local memory應避免bank conflicts

   local memory被組織為一個一個的只能被單獨訪問的bank,bank之間交叉儲存資料,以便連續的32bit被儲存在連續的bank中。如下圖所示:

  (1)如果多個work-item訪問連續的local memory資料,他們就能最大限度的實現並行讀寫。

  (2)如果多個work-item訪問同一個bank中的資料,他們就必須順序執行,嚴重降低資料讀取的並行性。因此,要合理安排資料在local memory中的佈局。

  (3)特殊情況,如果一個wave/warp中的執行緒同時讀取一個local memory中的一個地址,這時將進行廣播,不屬於bank 衝突。

6.避免使用”%“操作

"%"操作在GPU或者其他OpenCL裝置上需要大量的處理時間,如果可能的話儘量避免使用模操作。

7.kernel中重用(Reuse) private memory,為同一變數定義不同的巨集

   如果kernel中有兩個或者以上的private variable在程式碼中使用(比如一個在程式碼段A,一個在程式碼段B中),但是他們可以被數值相同。

  也就是當一個變數用作不同的目的時,為了避免程式碼中的命名困惑,可以使用巨集。在一個變數上定義不同的巨集。

  如下面程式碼所示:

複製程式碼
 1 #include<stdio.h>
 2 int main(){
 3     int i=4;
 4     #define EXP i
 5             printf("EXP=%d\n",EXP);
 6     
 7     #define COUNT i
 8             printf("COUNT=%d\n",COUNT);
 9     getchar();
10     return 0;
11 }
複製程式碼

8.對於(a*b+c)操作,儘量使用 fma function

如果定義了“FP_FAST_FMAF”巨集,就可以使用函式fma(a,b,c)精確的計算a*b+c。函式fma(a,b,c)的執行時間小於或等於計算a*b+c。

9.在program file 檔案中對非kernel的函式使用inline

  inline修飾符告訴編譯器在呼叫inline函式的地方,使用函式體替換函式呼叫。雖然會使得編譯後的程式碼佔用memory增加,但是省去了函式呼叫時上下、函式呼叫棧的切換操作,節省時間。

10.避免分支預測懲罰,應該儘量使得條件判斷為真的可能性大

  現代處理器一般都會進行“分支預測”,以便更好的提前“預取”下一條要執行的指令,使得“取指令、譯碼分析、執行、儲存”儘可能的並行。

  在“分支預測”出錯時,提前取到的指令,不是要執行的指令,就需要根據跳轉指令,進行重新取指令,就是“分支預測懲罰”。

  看如下的程式碼:

複製程式碼
 1 #include<stdio.h>
 2 int main()
 3 {
 4    int i=1;
 5    int b=0;
 6    if(i == 1)
 7            b=1;
 8     else
 9         b=0;
10     return 1;
11 }
複製程式碼

  對應的彙編程式碼:

  

  (movl 賦值,cmpl 比較,jne 不等於跳轉,jmp 無條件跳轉)

  從上面的彙編指令程式碼看出,如果比較(<main+24>)結果相等,則執行<main+26>也就是比較指令的下一條指令,對應b=1順序執行;如果比較(<main+24>)結果不相等,則執行跳轉到<main+35>,不是順序執行。

  當然,有的處理器可能會根據以往“順序執行”與“跳轉執行”的比例來進行分支預測,但是這也是需要積累的過程。況且並不是,每個處理器多能這樣只能。

  最後,上面的10個tips,能過提升kernel函式的效能,但是你應該進行具體的效能分析知道程式中最耗時的地方在哪裡。當然了,只有通過實驗才能真正學會OpenCL高效能程式設計。