上一篇介紹了CUDA記憶體空間。GPU對片外DRAM的通路往往是訪存性能的瓶頸。[1]第四章的後半部分,通過Global記憶體為例,說明了GPU通路DRAM的工作模式以及在該模式下,如何高效的使用DRAM記憶體。同樣的内容也可以參考[2]的5.3.2一節。
參考文獻:
[1] PROFESSIONAL CUDA C Programming. John Cheng, Max Grossman, Ty McKercher.
[2] CUDA C PROGRAMMING GUIDE
記憶體通路模式
GPU對片外DRAM的通路延遲大,帶寬低,導緻其成為很多應用的性能瓶頸。是以對DRAM通路的進一步優化可以有效改善程式性能。優化之前,首先看一下GPU對記憶體的通路模式。

如上圖所示,DRAM記憶體的讀寫在實體上是從片外DRAM->片上Cache->寄存器。 其中,片外DRAM到片上Cache是主要性能瓶頸。DRAM到Cache之間的一次傳輸(transaction)設計為32,64或者128位元組,并且記憶體位址按照32,64或者128位元組的間隔對齊。
後文中transaction特指裝置記憶體(DRAM)到片記憶體儲(Cache)的傳輸。
以讀資料為例,DRAM資料首先進入L2 Cache,之後根據GPU架構的不同,有些會繼續傳輸到L1 Cache,最後進入線程寄存器。L2 cache是所有SM共有,而L1 cache是SM私有。如果使用了L1+L2 Cache,那麼執行一次DRAM到Cache之間的傳輸(transaction),是128位元組。如果僅使用L2 Cache,那麼就是32位元組。是否啟用L1 Cache取決于GPU架構與編譯選項 。
關閉L1 cache
-Xptxas -dlcm=cg
打開L1 cache
-Xptxas -dlcm=ca
由于采用SIMT的架構,GPU對記憶體的通路指令是由warp發起的,即warp中每個線程同時執行記憶體操作指令,不過每個線程所通路的資料位址可以不同,GPU會根據這些不同的位址發起一次或多次DRAM->Cache的傳輸(transaction),直到所有線程都拿到各自所需的資料(Cache->Registers)。顯然,我們可以通過減少DRAM->Cache的transaction次數來優化程式性能。
回憶門:Stalled warp。執行模型一文中提到,warp執行記憶體指令時會有很長的延遲,此時warp進入Stalled狀态,warp排程器排程其他eligible warp執行。
記憶體指令延遲的原因:
- 通路裝置記憶體本身存在大的延遲。
- SIMT的執行模型,意味着隻有當warp内所有32個線程都得到了資料後,才會從Stalled态轉為Eligible态。
最好的情況下,GPU發起一次DRAM->Cache的transaction就把Warp中所有線程所需的資料全部擷取到Cache,最壞的情況下,則需要32次transaction。
對齊與合并通路
通過上面的簡單分析不難想到,優化全局記憶體的通路性能,需要考慮下面兩個方面,
- 記憶體對齊通路(Aligned memory access): DRAM->Cache transaction,首位址是32或128位元組的倍數。
- 記憶體合并通路(Coalesced memory access): Warp内線程通路連續記憶體塊。
下圖為一個理想的對齊合并記憶體通路的例子,
Warp内每個線程需要4個位元組的資料,且總共32*4=128個位元組是連續的,起始位址為128。此時隻需一次128位元組的memory transactioin(DRAM->Cache)。
下圖為非對齊,非合并的情況,
這種情況下需要3個128位元組的memory transaction,一個從記憶體位址0->127,為标記為1的線程取值,, 一個從128->255,為标記2的線程取值,一個從256->383,為标記3的線程取值。
下面以Global Memory為例,展開讨論不同情況下記憶體通路的性能。
Global Memory Read
根據是否使用L1 cache,可分為兩種情況讨論,
- Cached load(使用L1 cache)
- Uncached load(不适用L1 cache)
Cached Load
使用L1 cache,memory transaction的以L1 cache line的大小128位元組為間隔訪存。
合并對齊通路(Aligned, coalesced):
上圖,warp内線程通路的位址在128~256之間,每個線程需要4個位元組,所有線程所需記憶體位址按照線程ID連續排列。隻需執行一個128位元組的transaction即可滿足所有線程的需求。此時總線使用率為100%,即在這次transaction中,記憶體帶寬得以充分使用,沒有多餘的資料。
另一種情況,
此時,warp的線程通路的資料在128~256之間,每個線程需要4個位元組,但是記憶體位址沒有按照線程ID排序。與之前的例子相同,一次128位元組的transaction即可滿足要求,且總線使用率為100%。
連續,不對齊,
上圖warp需要連續的128位元組,但是記憶體的首位址沒有128位元組對齊。此時需要兩次128位元組的transaction,總線使用率為50%(總共讀了256個位元組,實際使用128位元組)
如果warp内的線程通路同一4位元組資料,
上圖warp内所有線程通路了相同4個位元組,需要一次128位元組的transaction,總線使用率4/128=3.125%。
最壞情況下,
Warp所需的資料散落在global memory裡,那麼需要最多32次128位元組的transaction。
再來看不使用L1 cache的情況。
Uncached Load
如果不使用L1 cache,一次記憶體傳輸由1, 2或4個segments完成,每個segment為32位元組, 并且按照32位元組對齊。顯然這種情況下資料傳輸得到了更精細的劃分(最小32位元組),這會帶來更高效的非對齊,非合并的記憶體通路。
上圖是一個理想的情況下,對齊合并的記憶體通路,隻需一個4 segment的transaction,總線使用率100%。
非對齊情況,
上圖warp需要連續的128位元組資料,但是首位址并未和32位元組對齊,此時128位元組的資料最多分布在5個segment内,是以總線使用率至少為80%。顯然要比使用L1 cached的時候更好。
再來看warp通路同一個4位元組資料的情況,
此時的總線使用率為4/32 = 12.5%,也要比使用L1 cache時(3.125%)要好。
考慮最壞的情況,
32個線程所需的資料散落在至多32個segment裡。散落在32個32位元組的segment裡顯然要比散落在32個128位元組的情況要好。
Read-Only cache: Read-Only cache原本用于紋理記憶體的緩存。GPU 3.5以上的版本可以使用該緩存替代L1,作為Global記憶體的緩存。此cache采用32位元組對齊間隔,是以比原128位元組的緩沖區更适合非對齊非合并的情況。
Global Memory Write
記憶體寫操作情況要簡單的多,Fermi/Kelper的L1 cache并不支援寫操作。記憶體寫僅通過L2 cache寫入裝置記憶體。與Uncached Load類似,transaction分為1, 2, 4個segment,每個segment32位元組。
理性情況下,
warp寫入連續的128個位元組,僅需一個4-segments的transaction。
對齊,但是不合并,
對齊,但散落在192個位元組的空間内,則需要3個1-segment的transaction。
64位元組的連續存儲,需要一個2-segment的transaction。
AoS與SoA
[1]中讨論了兩種資料組織形式,Array of structures 和 Structure of array.
//Array of structures (AoS)
struct innerStruct {
float x;
float y;
};
struct innerStruct myAoS[N];
//Structure of array
struct innerArray {
float x[N];
float y[N];
};
struct innerArray moa;
上圖為AoS, SoA的記憶體結構。采用AoS的形式,Warp線程通路x的時候,會把y的值也載入cache。浪費了50%的帶寬。而SoA就不存在這個問題。是以推薦采用SoA的形式組織資料。
訪存性能瓶頸通常發生在片外DRAM的讀寫上,除了減少裝置記憶體的transaction以及提高總線使用率之外,在程式設計時還應盡量減少對Global記憶體的通路次數,這部分在下一篇的共享記憶體裡有所涉及。