性能優化
1、線程映射
所謂線程映射是指某個線程訪問哪一部分數據,其實就是線程id和訪問數據之間的對應關系。
合適的線程映射可以充分利用硬件特性,從而提高程序的性能,反之,則會降低性能。
請參考Static Memory Access Pattern Analysis on a Massively Parallel GPU這篇paper,文中講述線程如何在算法中充分利用線程映射。這是我在google中搜索到的下載地址:http://www.ece.neu.edu/~bjang/patternAnalysis.pdf
使用不同的線程映射,同一個線程可能訪問不同位置的數據。下面是幾個線程映射的例子:
我們考慮一個簡單的串行矩陣乘法:這個算法比較適合輸出數據降維操作,通過創建N*M個線程,我們移去兩層外循環,這樣每個線程執行P個加法乘法操作。現在需要我們考慮的問題是,線程索引空間究竟應該是M*N還是N*M?
當我們使用M*N線程索引空間時候,Kernel如下圖所示:
而使用N*M線程索引空間時候,Kernel如下圖所示:
使用兩種映射關系,程序執行結果是一樣的。下面是在nv的卡GeForce 285 and 8800 GPUs上的執行結果。可以看到映射2(及N*M線程索引空間),程序的performance更高。
性能差異主要是因為在兩種映射方式下,對global memory訪問的方式有所不同。在行主序的buffer中,數據都是按行逐個存儲,為了保證合並訪問,我們應該把一個wave中連續的線程映射到矩陣的列(第二維),這樣在A*B=C的情況下,會把矩陣B和C的內存讀寫實現合並訪問,而兩種映射方式對A沒有影響(A由i3決定順序)。
完整的源代碼請從:http://code.google.com/p/imagefilter-opencl/downloads/detail?name=amduniCourseCode4.zip&can=2&q=#makechanges下載,程序中我實現了兩種方式的比較。結果確實第二種方式要快一些。
下面我們再看一個矩陣轉置的例子,在例子中,通過改變映射方式,提高了global memory訪問的效率。
矩陣轉置的公式是:Out(x,y) = In(y,x)
從上圖可以看出,無論采取那種映射方式,總有一個buffer是非合並訪問方式(注:在矩陣轉置時,必須要把輸入矩陣的某個元素拷貝到臨時位置,比如寄存器,然后才能拷貝到輸出矩陣)。我們可以改變線程映射方式,用local memory作為中間元素,從而實現輸入,輸出矩陣都是global memory合並訪問。
下面是AMD 5870顯卡上,兩種線程映射方式實現的矩陣轉置性能比較:
2、Occupancy
前面的教程中,我們提到過Occupancy的概念,它主要用來描述CU中資源的利用率。
OpenCL中workgroup被映射到硬件的CU中執行,在一個workgroup中的所有線程執行完之后,這個workgroup才算執行結束。對一個特定的cu來說,它的資源(比如寄存器數量,local memory大小,最大線程數量等)是固定的,這些資源都會限制cu中同時處於調度狀態的workgroup數量。如果cu中的資源數量足夠的的話,映射到同一個cu的多個workgroup能同時處於調度狀態,其中一個workgroup的wave處於執行狀態,當處於執行狀態的workgroup所有wave因為等待資源而切換到等待狀態的話,不同workgroup能夠從就緒狀態切換到ALU執行,這樣隱藏memory訪問時延。這有點類似操作系統中進程之間的調度狀態。我簡單畫個圖,以供參考:
- 對於一個比較長的kernel,寄存器是主要的資源瓶頸。假設kernel需要的最大寄存器數目為35,則workgroup中的所有線程都會使用35個寄存器,而一個CU(假設為5870)的最大寄存器數目為16384,則cu中最多可有16384/35=468線程,此時,一個workgroup中的線程數目(workitem)不可能超過468,
- 考慮另一個問題,一個cu共16384個寄存器,而workgroup固定為256個線程,則使用的寄存器數量可達到64個。
每個CU的local memory也是有限的,對於AMD HD 5XXX顯卡,local memory是32K,NV的顯卡local memory是32-48K(具體看型號)。和使用寄存器的情況相似,如果kernel使用過多的local memory,則workgroup中的線程數目也會有限制。
GPU硬件還有一個CU內的最大線程數目限制:AMD顯卡256,nv顯卡512。
NV的顯卡對於每個CU內的激活線程有數量限制,每個cu 8個或16個warp,768或者1024個線程。
AMD顯卡對每個CU內的wave數量有限制,對於5870,最多496個wave。
這些限制都是因為有限的資源競爭引起的,在nv cuda中,可以通過可視化的方式查看資源的限制情況。
3、向量化
向量化允許一個線程同時執行多個操作。我們可以在kernel代碼中,使用向量數據類型,比如float4來獲得加速。向量化在AMD的GPU上效果更為明顯,這是因為AMD的顯卡的stream core是(x,y,z,w)這樣的向量運算單元。
> 下圖是在簡單的向量賦值運算中,使用float和float4的性能比較。
kernel代碼為: