天天看點

《CUDA C程式設計權威指南》——3.5 展開循環

本節書摘來自華章計算機《cuda c程式設計權威指南》一書中的第3章,第3.5節,作者 [美] 馬克斯·格羅斯曼(max grossman),譯 顔成鋼 殷建 李亮,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

循環展開是一個嘗試通過減少分支出現的頻率和循環維護指令來優化循環的技術。在循環展開中,循環主體在代碼中要多次被編寫,而不是隻編寫一次循環主體再使用另一個循環來反複執行的。任何的封閉循環可将它的疊代次數減少或完全删除。循環體的複制數量被稱為循環展開因子,疊代次數就變為了原始循環疊代次數除以循環展開因子。在順序數組中,當循環的疊代次數在循環執行之前就已經知道時,循環展開是最有效提升性能的方法。考慮下面的代碼:

《CUDA C程式設計權威指南》——3.5 展開循環

如果重複操作一次循環體,疊代次數能減少到原始循環的一半:

《CUDA C程式設計權威指南》——3.5 展開循環

從進階語言層面上來看,循環展開使性能提高的原因可能不是顯而易見的。這種提升來自于編譯器執行循環展開時低級指令的改進和優化。例如,在前面循環展開的例子中,條件i< 100隻檢查了50次,而在原來的循環中則檢查了100次。另外,因為在每個循環中每個語句的讀和寫都是獨立的,是以cpu可以同時發出記憶體操作。

在cuda中,循環展開的意義非常重大。我們的目标仍然是相同的:通過減少指令消耗和增加更多的獨立排程指令來提高性能。是以,更多的并發操作被添加到流水線上,以産生更高的指令和記憶體帶寬。這為線程束排程器提供更多符合條件的線程束,它們可以幫助隐藏指令或記憶體延遲。

你可能會注意到,在reduceinterleaved核函數中每個線程塊隻處理一部分資料,這些資料可以被認為是一個資料塊。如果用一個線程塊手動展開兩個資料塊的處理,會怎麼樣?以下的核函數是reduceinterleaved核函數的修正版:每個線程塊彙總了來自兩個資料塊的資料。這是一個循環分區(在第1章中已介紹)的例子,每個線程作用于多個資料塊,并處理每個資料塊的一個元素:

《CUDA C程式設計權威指南》——3.5 展開循環
《CUDA C程式設計權威指南》——3.5 展開循環

注意要在核函數的開頭添加的下述語句。在這裡,每個線程都添加一個來自于相鄰資料塊的元素。從概念上來講,可以把它作為歸約循環的一個疊代,此循環可在資料塊間歸約:

《CUDA C程式設計權威指南》——3.5 展開循環

如下所示,全局數組索引被相應地調整,因為隻需要一半的線程塊來處理相同的資料集。請注意,這也意味着對于相同大小的資料集,向裝置顯示的線程束和線程塊級别的并行性更低。圖3-25所示為每個線程的資料通路。

《CUDA C程式設計權威指南》——3.5 展開循環
《CUDA C程式設計權威指南》——3.5 展開循環

向主函數添加下面的代碼,調用新的核函數:

《CUDA C程式設計權威指南》——3.5 展開循環

因為現在每個線程塊處理兩個資料塊,我們需要調整核心的執行配置,将網格大小減小至一半:

《CUDA C程式設計權威指南》——3.5 展開循環

現在編譯和運作這些代碼,出現以下結果:

《CUDA C程式設計權威指南》——3.5 展開循環

即使隻進行簡單的更改,現在核函數的執行速度比原來快3.42倍。可以進一步展開以産生更好的性能嗎?reduceinteger.cu檔案包含着展開的核函數中其他的兩個實作,如下所示:

《CUDA C程式設計權威指南》——3.5 展開循環

相應的結果概括如下:

《CUDA C程式設計權威指南》——3.5 展開循環

正如預想的一樣,在一個線程中有更多的獨立記憶體加載/存儲操作會産生更好的性能,因為記憶體延遲可以更好地被隐藏起來。可以使用裝置記憶體讀取吞吐量名額,以确定這就是性能提高的原因:

《CUDA C程式設計權威指南》——3.5 展開循環

結果總結如下,歸約的展開測試用例和裝置讀吞吐量之間是成正比的:

《CUDA C程式設計權威指南》——3.5 展開循環

__syncthreads是用于塊内同步的。在歸約核函數中,它用來確定線上程進入下一輪之前,每一輪中所有線程已經将局部結果寫入全局記憶體中了。

然而,要細想一下隻剩下32個或更少線程(即一個線程束)的情況。因為線程束的執行是simt(單指令多線程)的,每條指令之後有隐式的線程束内同步過程。是以,歸約循環的最後6個疊代可以用下述語句來展開:

《CUDA C程式設計權威指南》——3.5 展開循環

這個線程束的展開避免了執行循環控制和線程同步邏輯。

注意變量vmem是和volatile修飾符一起被聲明的,它告訴編譯器每次指派時必須将vmem[tid]的值存回全局記憶體中。如果省略了volatile修飾符,這段代碼将不能正常工作,因為編譯器或緩存可能對全局或共享記憶體優化讀寫。如果位于全局或共享記憶體中的變量有volatile修飾符,編譯器會假定其值可以被其他線程在任何時間修改或使用。是以,任何參考volatile修飾符的變量強制直接讀或寫記憶體,而不是簡單地讀寫緩存或寄存器。

基于reduceunrolling8,線程束的展開可以添加到歸約核函數中,如下所示:

《CUDA C程式設計權威指南》——3.5 展開循環
《CUDA C程式設計權威指南》——3.5 展開循環

因為在這個實作中,每個線程處理8個資料塊,調用這個核心的同時它的網格尺寸減小到1/8:

《CUDA C程式設計權威指南》——3.5 展開循環

這個核函數的執行時間比reduceunrolling8快1.05倍,比原來的核函數reduceneigh-bored快8.65倍:

《CUDA C程式設計權威指南》——3.5 展開循環

使用下面的指令,stall_sync名額可以用來證明,由于__syncthreads的同步,更少的線程束發生阻塞:

《CUDA C程式設計權威指南》——3.5 展開循環

結果總結如下。通過展開最後的線程束,百分比幾乎減半了,這表明__syncthreads能減少新的核函數中的阻塞。

《CUDA C程式設計權威指南》——3.5 展開循環

如果編譯時已知一個循環中的疊代次數,就可以把循環完全展開。因為在fermi或kepler架構中,每個塊的最大線程數都是1 024(參見表3-2),并且在這些歸約核函數中循環疊代次數是基于一個線程塊次元的,是以完全展開歸約循環是可能的:

《CUDA C程式設計權威指南》——3.5 展開循環
《CUDA C程式設計權威指南》——3.5 展開循環

用以下執行配置調用這個核函數:

《CUDA C程式設計權威指南》——3.5 展開循環

核心時間再次有了小小的改善,它的執行比reduceunrollwarps8快1.06倍,比原來的實作快9.16倍:

《CUDA C程式設計權威指南》——3.5 展開循環

雖然可以手動展開循環,但是使用模闆函數有助于進一步減少分支消耗。在裝置函數上cuda支援模闆參數。如下所示,可以指定塊的大小作為模闆函數的參數:

《CUDA C程式設計權威指南》——3.5 展開循環
《CUDA C程式設計權威指南》——3.5 展開循環

相比reducecompleteunrollwarps8,唯一的差別是使用了模闆參數替換了塊大小。檢查塊大小的if語句将在編譯時被評估,如果這一條件為false,那麼編譯時它将會被删除,使得内循環更有效率。例如,線上程塊大小為256的情況下調用這個核函數,下述語句将永遠是false:

《CUDA C程式設計權威指南》——3.5 展開循環

編譯器會自動從執行核心中移除它。

該核函數一定要在switch-case結構中被調用。這允許編譯器為特定的線程塊大小自動優化代碼,但這也意味着它隻對在特定塊大小下啟動reducecompleteunroll有效:

《CUDA C程式設計權威指南》——3.5 展開循環
《CUDA C程式設計權威指南》——3.5 展開循環
《CUDA C程式設計權威指南》——3.5 展開循環

注意,最大的相對性能增益是通過reduceunrolling8核函數獲得的,在這個函數之中每個線程在歸約前處理8個資料塊。有了8個獨立的記憶體通路,可以更好地讓記憶體帶寬飽和及隐藏加載/存儲延遲。可以使用以下指令檢測記憶體加載/存儲效率名額:

《CUDA C程式設計權威指南》——3.5 展開循環

表3-6總結了所有核函數的結果。在第4章,将會更加詳細地介紹全局記憶體通路,并且會對記憶體通路如何影響核心性能有更深的了解。

《CUDA C程式設計權威指南》——3.5 展開循環

繼續閱讀