前言
如何對現有的程式進行並行優化,是 GPU 並行程式設計技術最為關注的實際問題。本文將提供幾種優化的思路,為程式並行優化指明道路方向。
優化前準備
首先,要明確優化的目標 - 是要將程式提速 2 倍?還是 10 倍?100倍?也許你會不假思索的說當然是提升越高越好。
但這裡存在一個優化成本的問題。在同樣的技術水平硬體水平下,提升 2 倍也許只要一個下午的工作量,但提高 10 倍可能要考慮到更多的東西,也許是一週的工作量。提高 100 倍, 1000 倍需要的成本,時間就更多了。
然後,需要將這個問題進行分解。通常來說先對資料集進行分解,然後將任務進行分解。這裡要從資料集這樣的矩陣角度來分析資料,將輸入集和輸出集中各個格點的對應關係找出來,然後分派給各個塊,各個執行緒。
策略一:識別程式碼中的瓶頸所在
分析程式效率的瓶頸所在一方面靠的是分析。這種方式對於程式碼結構比較簡單的程式非常有用,但對於實際應用中的複雜專案,人腦分析往往會導致錯誤的結論 - 也許你費盡心思想出來了瓶頸,然後對它做了優化,之後卻發現效率僅僅提升了 1%。
因此更有效的方法是使用分析工具來找出瓶頸,可以使用 CUDA Profiler 或者 Parallel Nsight。
使用 Parallel Nsight 分析並行程式的方法請參考我的這篇文章:(準備中...)
還有一點要特別說明的是,在 GPU 進行資料處理的時候,CPU 可以考慮做點別的事情,比如去伺服器取數之類的,這樣就將 CPU 並行和 GPU 並行結合起來了,程式效率自然會大大提高。
策略二:合理的利用記憶體
首先,要靈活的使用顯示卡中的各類記憶體結構,如共享記憶體,常量記憶體等。特別要注意共享記憶體的使用,它的速度可是接近一級快取的。
此外,必要時對多個核心函式進行融合。因為這樣可以避免啟動新的核心函式時需要進行的資料傳遞問題,還可以重用前面的任務遺留下的一些有用的資料。不過,如果是對別人寫的多個核心函式進行融合的話,一定要注意其中隱含的同步問題 - 上個核心函式的程式碼徹底執行完畢之後,下個核心函式才會開始執行。
然後,對於資料的訪問應該採取合併訪問的方式 - 儘量使用 cudaMalloc 函式。一次訪問的資料應當大於 128 位元組,這樣才能充分地利用顯示卡的頻寬。
策略三:傳輸過程的優化
前面的文章已經提到過很多次了,資料在記憶體和視訊記憶體之間進行交換是非常費時的。
對於這樣的問題,首先我們可以以鎖頁記憶體的方式使用主機端記憶體。所謂鎖頁記憶體,是指該區域記憶體和顯示卡的傳遞不需要 CPU 來干預,如果某區域不宣告為鎖頁記憶體,那麼在記憶體往視訊記憶體中或者視訊記憶體往記憶體中傳遞資料前,會發生一些開銷不小的鎖定操作(表示該區域記憶體正在和視訊記憶體發生資料傳遞,CPU勿擾)。
使用方法是呼叫 cudaHostAlloc 函式。這個函式的功能不單單是宣告鎖頁記憶體那麼簡單。通過設定函式的引數,該函式還能實現很多非常實用的功能,個人非常推薦。
然後,還需要重點推薦的是零複製記憶體。它是一種特殊的鎖頁記憶體,一種特殊的記憶體對映。它允許你將主機記憶體對映到 GPU 的記憶體空間。如果你的程式是計算密集型的,那麼這個機制就會非常有用,它會自動將資料傳輸和計算重疊。具體用法請參考我的這 篇文章。
策略四:執行緒結構佈局的優化
建立科學的計算網格,通過設定合適的維數,塊數,以及塊內執行緒數來儘量實現合併的記憶體訪問,保證最大的記憶體頻寬。
要學會靈活使用多維度的計算網格,而不是僅僅侷限於一維。多維計算網格的使用請參考我的這篇文章。
尤其在單維度的塊數受到限制的時候,多維網格就必須被考慮進來了。
策略五:從演算法本身進行任務級的分解
將演算法的步驟分解各個不相關的部分,步驟內採用GPU並行,這幾個步驟則採用CPU並行。
策略六:靈活使用 CUDA C 的一些庫還有 API
CUDA C 提供了很多實用的 API,且提供相當多的C++支援 (非全部)。能大大地提高開發效率。如原子操作函式等等,很方便。
CUDA 提供了許多實用的庫:如 cuBlas cuSparse等,不在此一一介紹。尤其是 Thrust 庫,簡直就是 STL 的並行實現,拿來直接用非常方便。
小結
優化思路可以說是 CUDA 並行程式設計最為核心,也是最為關鍵所在。
本文僅僅是提供優化的總體策略和思路,至於具體的實現方法,請參考相關資料實現之。