CUDA並行計算 | CUDA算法效率提升關鍵點概述

前言


  CUDA算法的效率總的來說,由存取效率和計算效率兩類決定,一個好的CUDA算法必定會讓兩類效率都達到最優化,而其中任一類效率成爲瓶頸,都會讓算法的性能大打折扣。

存取效率


  存取效率即GPU和顯存之間的數據交換效率,在上一篇博客中,我們介紹了GPU的存儲結構,對GPU的各類存儲介質有了一個初步的瞭解,其中全局內存具有最大的容量和最慢的訪問效率,且對是否對齊和連續訪問很敏感,這也是我們在前面推薦進行內存對齊的原因;共享內存訪問速度快,且對是否對齊和連續訪問不敏感,但是對Bank Conflict非常敏感,Bank Conflict的影響本文在後面會詳細介紹,靈活使用共享內存會獲得很高的存取效率,也是衆多優秀CUDA算法替代全局內存的不二選擇;寄存器具有最快的訪問速度,只對每個線程可見,線程內多使用寄存器是良好的習慣,但是需要注意一個SM內的寄存器數量有限,當單個線程的寄存器數量超過限制,會影響線程的實際佔用率,從而影響加速效果;其他存儲介質如紋理內存常量內存等較共享內存和寄存器,在速度並沒有太大優勢,但是其具有的一些特殊特性使其有時候在特定的情況下被使用以獲得更高的效率,比如紋理內存帶有的紋理緩存具備硬件插值特性,可以實現最鄰近插值和線性插值,且針對二維空間的局部性訪問進行了優化,所以通過紋理緩存訪問二維矩陣的鄰域會獲得加速,這個特性使得紋理內存在一些圖像處理算法中具有一定的優勢。

計算效率


  計算效率就是指除去內存交換過程以外的算法計算部分的效率,GPU中主要有三類基礎運算:整數運算、單精度浮點數運算和雙精度浮點數運算,其中單精度浮點運算速度最快而雙精度浮點運算速度最慢,FLOPS(floating-point operations per second, 每秒執行的浮點運算次數)也是衡量GPU運算性能的關鍵指標,如果一個程序內只有單精度浮點數運算,將發揮硬件的最大功效,因此應該儘量多使用單精度浮點數運算,而避免使用雙精度浮點運算。實際上,GPU的單核運算性能遠不及CPU,因爲單核運算速度取決於核心頻率,而GPU的核心頻率遠不及CPU,目前主流的英特爾第七代桌面級CPU的核心頻率都在3.5~4GHz左右,並支持超頻,而NVIDIA在2016年發佈的號稱地球最快顯卡NVIDIA TITAN X的核心頻率也不過是1.4GHz,和CPU差距依然較大。但是GPU的核心數是CPU所完全無法比擬的,其並行計算效率一般情況下遠遠大於CPU的單核甚至多核計算效率,核心數的優勢讓GPU的浮點運算效率遠高於CPU,所以對GPU程序來說,讓GPU利用率達到100%,讓每個線程都處於活動狀態,對提高程序的性能有着至關重要的作用。此外,在提高CPU利用率的同時,還必須關注另一個因素:分支(if、else、for、while、do、switch等語句)對計算效率的影響,由於硬件每次只能爲一個線程束獲取一條指令,若線程束中一半的線程要執行條件爲真的代碼段,一半線程要執行條件爲假的代碼段,這時有一半的線程會被阻塞,而另一半線程會執行滿足條件的那個分支,如此,硬件的利用率只達到了50%,大大影響並行性能。

性能優化要點


  在基於CUDA優化算法設計過程中,除了使算法能夠運行得到正確結果之外,更重要的是算法效率能達到理想的水平,而從上面的描述來看,要發揮CUDA算法的性能優勢必須考慮全面,留意一些性能陷阱,採用合理的算法設計方案。一般來說,優化一個CUDA算法的性能需要專注三個方面,按照重要性排序爲:

展現足夠的並行性

  爲了最大程度的利用GPU多線程的優勢,應該在GPU上安排儘量多的併發任務,以使指令帶寬和內存帶寬都達到飽和,在一個SM(流處理器)中保證有足夠多的併發線程束,這不單單是要爲GPU每個線程都安排任務,還需要檢查SM資源佔用率的限制因素(共享內存、寄存器以及計算週期等)以找到達到最佳性能的平衡點,因爲GPU的內存資源是有限的,爲每個線程分配的資源也是有限的,如果算法設計者在一個線程中使用了過多的共享內存或者寄存器,那麼併發運行的線程數必然會減少,使得SM資源的實際佔用率小於理論佔用率;另一方面可以爲每個線程/線程束分配更多獨立的工作。

優化內存訪問

  大部分GPU算法的性能瓶頸都在於內存訪問速度,由於顯存訪問的高延遲和低效率,內存訪問模式對內核性能有着顯著的影響。內存訪問優化的目標是最大限度地提高內存帶寬的利用率,重點在於優化內存訪問模式和保證充足的併發內存訪問。在GPU中,線程是以線程束爲單位執行的,一個線程束包含32個線程,所以一方面我們最好將併發線程數設置爲32的倍數,另一方面當一個線程束髮送內存請求(加載或存儲)時,都是32個線程一起訪問一個設備內存塊,因此對於全局內存來說,最好的訪問模式就是對齊和合並訪問,對齊內存訪問要求所需的設備內存的第一個地址是32字節的倍數,合併內存訪問指的是通過線程束中的32個線程來訪問一個連續的內存塊。這表示在算法設計中一定要儘量爲一個線程束的線程分配連續的內存塊,比如0~31號線程(同一個線程束)訪問影像中連續存儲的31個像素,而不是訪問不連續的31個像素,由於合併訪問對內存訪問效率影像非常大,所以我們在算法設計中建議嚴格遵守該要求。

  共享內存因爲是片上內存,所以比本地和設備的全局內存具有更高的帶寬和更低的延遲,使用共享內存有兩個主要原因:①減少全局內存的訪問次數;②通過重新安排數據佈局避免未合併的全局內存的訪問。在物理角度上,共享內存通過一種線性方式排列,通過32個存儲體(bank)進行訪問。Fermi和Kepler架構各有不同的默認存儲體模式:4字節存儲體模式和8字節存儲體模式,共享內存地址到存儲體的映射關係隨着訪問模式的不同而不同,當線程束中的多個線程在同一存儲體中訪問不同字節時,會發生存儲體衝突(Bank Conflict),由於共享內存重複請求,所以多路存儲體衝突可能要付出很大的代價,應該儘量避免存儲體衝突,每個存儲體(Bank)每個週期只能指向一次操作(一個32bit 的整數或者一個單精度的浮點型數據),一次讀或者一次寫,也就是說每個存儲體(Bank)的帶寬爲每週期 32bit,比如一個32*32的二維單精度浮點數組,每一列屬於一個Bank,如果一個線程束裏的不同線程訪問該數組裏同一列的不同數據,則會發生Bank Conflict,解決或減少存儲體衝突的一個非常簡單有效的方法是填充數組,在合適的位置添加填充字,可以使其跨不同存儲體進行訪問,從而減少延遲並提高了吞吐量。

  寄存器是GPU上最快的存儲機制,但是數量非常有限,如果一個線程使用過多的寄存器,會導致SM能夠同時啓動的線程數變少,實際上很多情況下寄存器都成爲了資源佔用率無法達到100%的主要限制條件,所以往往要注意監控寄存器的數量,當數量沒有超標時,適當的增加數量可以提升性能,而一旦數量超標,最好還是將寄存器的數量減少以保證100%的資源佔用率,這可以通過重新排列代碼的順序來實現,比如當變量的賦值和使用靠的很近時,編譯器會重複使用少量寄存器以達到減少寄存器數量的目的。

優化指令執行

  GPU屬於單指令多數據流架構,每個線程束中的所有線程在每一步都執行相同的指令,如果每個指令都能夠得到對結果有效的運算值,就能夠避免線程的浪費,而如果由於條件分支造成線程束內有不同的控制流路徑,則線程運行可能出現分化,這時線程束必須順序執行每個分支路徑,並禁用不在此執行路徑上的線程,而如果算法的大部分時間都耗在分支代碼中,必然顯著的影響內核性能,所以儘量避免使用分支是很關鍵的,或者儘量使分支有非常大的概率執行對結果有效的哪一個路徑。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章