kernel性能高低是不能單純的從warp的執行上來解釋的。好比以前博文涉及到的,將block的維度設置爲warp大小的一半會致使load efficiency下降,這個問題沒法用warp的調度或者並行性來解釋。根本緣由是獲取global memory的方式不好勁。程序員
衆所周知,memory的操做在講求效率的語言中佔有極重的地位。low-latency和high-bandwidth是高性能的理想狀況。可是購買擁有大容量,高性能的memory是不現實的,或者不經濟的。所以,咱們就要儘可能依靠軟件層面來獲取最優latency和bandwidth。CUDA將memory model unit分爲device和host兩個系統,充分暴露了其內存結構以供咱們操做,給予用戶充足的使用靈活性。編程
通常來講,程序獲取資源是有規律的,也就是計算機體系結構常常提到的局部原則。其又分爲時間局部性和空間局部性。 相信你們對計算機內存方面的知識都很熟悉了,這裏就很少說了,只簡單提下。數組
GPU和CPU的主存都是用DRAM實現,cache則是用lower-latency的SRAM來實現。GPU和CPU的存儲結構基本同樣。並且CUDA將memory結構更好的呈現給用戶,從而能更靈活的控制程序行爲。緩存
對於程序員來講,memory能夠分爲下面兩類:安全
在CPU的存儲結構中,L1和L2 cache都是non-programmable的。對於CUDA來講,programmable的類型很豐富:app
下圖展現了memory的結構,他們各自都有不用的空間、生命期和cache。ide
其中constant和texture是隻讀的。最下面這三個global、constant和texture擁有相同的生命週期。函數
寄存器是GPU最快的memory,kernel中沒有什麼特殊聲明的自動變量都是放在寄存器中的。當數組的索引是constant類型且在編譯期能被肯定的話,就是內置類型,數組也是放在寄存器中。性能
寄存器變量是每一個線程私有的,一旦thread執行結束,寄存器變量就會失效。寄存器是稀有資源。在Fermi上,每一個thread限制最多擁有63個register,Kepler則是255個。讓本身的kernel使用較少的register就可以容許更多的block駐留在SM中,也就增長了Occupancy,提高了性能。大數據
使用nvcc的-Xptxas -v,-abi=no(這裏Xptxas表示這個是要傳給ptx的參數,不是nvcc的,v是verbose,abi忘了,好像是application by interface)選項能夠查看每一個thread使用的寄存器數量,shared memory和constant memory的大小。若是kernel使用的register超過硬件限制,這部分會使用local memory來代替register,即所謂的register spilling,咱們應該儘可能避免這種狀況。編譯器有相應策略來最小化register的使用而且避免register spilling。咱們也能夠在代碼中顯式的加上額外的信息來幫助編譯器作優化:
__global__ void __launch_bounds__(maxThreadsPerBlock, minBlocksPerMultiprocessor) kernel(...) { // your kernel body }
maxThreadsPerBlock指明每一個block能夠包含的最大thread數目。minBlocksPerMultiprocessor是可選的參數,指明必要的最少的block數目。
咱們也可使用-maxrregcount=32來指定kernel使用的register最大數目。若是使用了__launch_bounds__,則這裏指定的32將失效。
有時候,若是register不夠用了,那麼就會使用local memory來代替這部分寄存器空間。除此外,下面幾種狀況,編譯器可能會把變量放置在local memory:
local memory這個名字是有歧義的:在local memory中的變量本質上跟global memory在同一塊存儲區。因此,local memory有很高的latency和較低的bandwidth。在CC2.0以上,GPU針對local memory會有L1(per-SM)和L2(per-device)兩級cache。
用__shared__修飾符修飾的變量存放在shared memory。由於shared memory是on-chip的,他相比localMemory和global memory來講,擁有高的多bandwidth和低不少的latency。他的使用和CPU的L1cache很是相似,可是他是programmable的。
按慣例,像這類性能這麼好的memory都是有限制的,shared memory是以block爲單位分配的。咱們必須很是當心的使用shared memory,不然會無心識的限制了active warp的數目。
不一樣於register,shared memory儘管在kernel裏聲明的,可是他的生命週期是伴隨整個block,而不是單個thread。當該block執行完畢,他所擁有的資源就會被釋放,從新分配給別的block。
shared memory是thread交流的基本方式。同一個block中的thread經過shared memory中的數據來相互合做。獲取shared memory的數據前必須先用__syncthreads()同步。L1 cache和shared memory使用相同的64KB on-chip memory,咱們也可使用下面的API來動態配置兩者:
cudaError_t cudaFuncSetCacheConfig(const void* func, enum cudaFuncCachecacheConfig);
func是分配策略,可使用下面幾種:
cudaFuncCachePreferNone: no preference (default)
cudaFuncCachePreferShared: prefer 48KB shared memory and 16KB L1 cache
cudaFuncCachePreferL1: prefer 48KB L1 cache and 16KB shared memory
cudaFuncCachePreferEqual: Prefer equal size of L1 cache and shared memory, both 32KB
Fermi僅支持前三種配置,Kepler支持所有,注意,在Maxwell以後,L1被捨棄了,因此這64KB就徹底屬於shared Memory了,也就沒有了上面這個分配一說。
Constant Memory駐留在device Memory,而且使用專用的constant cache(per-SM)。該Memory的聲明應該以__connstant__修飾。constant的範圍是全局的,針對全部kernel,對於全部CC其大小都是64KB。在同一個編譯單元,constant對全部kernel可見。
kernel只能從constant Memory讀取數據,所以其初始化必須在host端使用下面的function調用:
cudaError_t cudaMemcpyToSymbol(const void* symbol, const void* src,size_t count);
這個function拷貝src指向的count個byte到symbol的地址,symbol指向的是在device中的global或者constant Memory。
當一個warp中全部thread都從同一個Memory地址讀取數據時,constant Memory表現最好。例如,計算公式中的係數。若是全部的thread從不一樣的地址讀取數據,而且只讀一次,那麼constant Memory就不是很好的選擇,由於一次讀constant Memory操做會廣播給全部thread知道。
texture Memory駐留在device Memory中,而且使用一個只讀cache(per-SM)。texture Memory實際上也是global Memory在一塊,可是他有本身專有的只讀cache。這個cache在浮點運算頗有用(具體還沒弄懂)。texture Memory是針對2D空間局部性的優化策略,因此thread要獲取2D數據就可使用texture Memory來達到很高的性能,D3D編程中有兩種重要的基本存儲空間,其中一個就是texture。
global Memory是空間最大,latency最高,GPU最基礎的memory。「global」指明瞭其生命週期。任意SM均可以在整個程序的生命期中獲取其狀態。global中的變量既能夠是靜態也能夠是動態聲明。可使用__device__修飾符來限定其屬性。global memory的分配就是以前頻繁使用的cudaMalloc,釋放使用cudaFree。global memory駐留在devicememory,能夠經過32-byte、64-byte或者128-byte三種格式傳輸。這些memory transaction必須是對齊的,也就是說首地址必須是3二、64或者128的倍數。優化memory transaction對於性能提高相當重要。當warp執行memory load/store時,須要的transaction數量依賴於下面兩個因素:
通常來講,所需求的transaction越多,潛在的沒必要要數據傳輸就越多,從而致使throughput efficiency下降。
對於一個既定的warp memory請求,transaction的數量和throughput efficiency是由CC版本決定的。對於CC1.0和1.1來講,對於global memory的獲取是很是嚴格的。而1.1以上,因爲cache的存在,獲取要輕鬆的多。
跟CPU的cache同樣,GPU cache也是non-programmable的。在GPU上包含如下幾種cache,在前文都已經提到:
每一個SM都有一個L1 cache,全部SM共享一個L2 cache。兩者都是用來緩存local和global memory的,固然也包括register spilling的那部分。在Fermi GPus 和 Kepler K40或者以後的GPU,CUDA容許咱們配置讀操做的數據是否使用L1和L2或者只使用L2。
在CPU方面,memory的load/store均可以被cache。可是在GPU上,只有load操做會被cache,store則不會。
每一個SM都有一個只讀constant cache和texture cache來提高性能。
下表是以前介紹的幾種memory的聲明總結:
下面的代碼介紹了怎樣靜態的聲明global variable(以前的博文其實都是global variable)。大體過程就是,先聲明瞭一個float全局變量,在checkGlobal-Variable中,該值被打印出來,隨後,其值便被改變。在main中,這個值使用cudaMemcpyToSymbol來初始化。最終當全局變量被改變後,將值拷貝回host。
#include <cuda_runtime.h> #include <stdio.h> __device__ float devData; __global__ void checkGlobalVariable() { // display the original value printf("Device: the value of the global variable is %f\n",devData); // alter the value devData +=2.0f; } int main(void) { // initialize the global variable float value = 3.14f; cudaMemcpyToSymbol(devData, &value, sizeof(float)); printf("Host: copied %f to the global variable\n", value); // invoke the kernel checkGlobalVariable <<<1, 1>>>(); // copy the global variable back to the host cudaMemcpyFromSymbol(&value, devData, sizeof(float)); printf("Host: the value changed by the kernel to %f\n", value); cudaDeviceReset(); return EXIT_SUCCESS; }
編譯運行:
$ nvcc -arch=sm_20 globalVariable.cu -o globalVariable
$ ./globalVariable
輸出:
Host: copied 3.140000 to the global variable Device: the value of the global variable is 3.140000 Host: the value changed by the kernel to 5.140000
熟悉了CUDA的基本思想後,不難明白,儘管host和device的代碼是寫在同一個源文件,可是他們的執行卻在徹底不一樣的兩個世界,host不能直接訪問device變量,反之亦然。
咱們可能會反駁說,用下面的代碼就能得到device的全局變量:
cudaMemcpyToSymbol(devD6ata, &value, sizeof(float));
可是,咱們應該還注意到下面的幾點:
並且,cudaMemcpy不能用&devData這種方式來傳遞變量,正如上面所說,devData只是個符號,取址這種操做自己就是錯誤的:
cudaMemcpy(&devData, &value, sizeof(float),cudaMemcpyHostToDevice); // It’s wrong!!!
無論怎樣,CUDA仍是爲咱們提供了,利用devData這種符號來獲取變量地址的方式:
cudaError_t cudaGetSymbolAddress(void** devPtr, const void* symbol);
獲取地址以後,就可使用cudaMemcpy了:
float *dptr = NULL; cudaGetSymbolAddress((void**)&dptr, devData); cudaMemcpy(dptr, &value, sizeof(float), cudaMemcpyHostToDevice);
咱們只有一種方式可以直接獲取GPU memory,即便用pinned memory,下文將詳細介紹。
CUDA很是接近C的編程風格,以便可以快速上手掌握,在內存管理這點上,CUDA區別於C最明顯的操做就是在device和host之間不停的傳遞數據。很麻煩的一個過程,不過Unified Memory出現後,程序編寫就沒那麼複雜了,可是目前,Unified Memory的使用並未普及,咱們仍是要關注Memory的顯式的操做過程:
爲了達到最好的性能,CUDA提供了五花八門的接口供程序員顯式的在device和host之間傳遞數據。
前面的博文已經提到一部份內存分配函數了,在分配global Memory時,最經常使用的就是下面這個了:
cudaError_t cudaMalloc(void **devPtr, size_t count);
若是分配出錯則返回cudaErrorMemoryAllocation。分配成功後,就得對該地址初始化值,要麼從host調用cudaMemcpy賦值,要麼調用下面的API初始化:
cudaError_t cudaMemset(void *devPtr, int value, size_t count);
釋放資源就是:
cudaError_t cudaFree(void *devPtr);
device資源分配是個很是昂貴的操做,因此,device Memory應該儘量的重用,而不是從新分配。
一旦global Memory分配好後,若是不用cudaMemset就得用下面這個:
cudaError_t cudaMemcpy(void *dst, const void *src, size_t count,enum cudaMemcpyKind kind);
這個你們應該也很熟悉了,kind就是下面這幾種:
cudaMemcpyHostToHost
cudaMemcpyHostToDevice
cudaMemcpyDeviceToHost
cudaMemcpyDeviceToDevice
下圖是CPU和GPU之間傳輸關係圖,能夠看出來,CPU和GPU之間傳輸速度相對不好(NVLink技術能提升5~10倍),GPU和on-board Memory傳輸速度要快得多,因此對於編程來講,要時刻考慮減小CPU和GPU之間的數據傳輸。
Host Memory的分配默認狀況下是pageable的,也就是說,咱們要承受因pagefault致使的操做,,這個操做要將host virtual Memory的數據轉移到由OS決定的不物理位置。GPU沒法安全的獲取host的pageable Memory,由於GPU沒有辦法控制host OS物理上轉移數據的時機。所以,當將pageable host Memory數據送到device時,CUDA驅動會首先分配一個臨時的page-locked或者pinned host Memory,並將host的數據放到這個臨時空間裏。而後GPU從這個所謂的pinned Memory中獲取數據,以下左圖所示:
左圖是默認的過程,咱們也能夠顯式的直接使用pinned Memory,以下:
cudaError_t cudaMallocHost(void **devPtr, size_t count);
因爲pinned Memory可以被device直接訪問(不是指不經過PCIE了,而是相對左圖咱們少了pageable Memory到pinned Memory這一步),因此他比pageable Memory具備至關高的讀寫帶寬,固然像這種東西依然不能過分使用,由於這會下降pageable Memory的數量,影響整個虛擬存儲性能,咱們不能因小失大。
cudaError_t status = cudaMallocHost((void**)&h_aPinned, bytes); if (status != cudaSuccess) { fprintf(stderr, "Error returned from pinned host memory allocation\n"); exit(1); }
Pinned Memory的釋放也比較特殊:
cudaError_t cudaFreeHost(void *ptr);
Pinned Memory比pageable Memory的分配操做更加昂貴,可是他對大數據的傳輸有很好的表現。還有就是,pinned Memory效果的高低也是跟CC有關的。
將許多小的傳輸合併到一次大的數據傳輸,並使用pinned Memory將下降很大的傳輸消耗。這裏說起下,數據傳輸的消耗有時候是能夠被kernel的執行覆蓋的。
通常來講,host和device是不能直接訪問對方的數據的,前文也有提到,可是Zero-Copy Memory是個特例。
該Memory是位於host的,可是GPU thread能夠直接訪問,其優勢有:
當使用zero-copy來共享host和device數據時,咱們必須同步Memory的獲取,不然,device和host同時訪問該Memory會致使未定義行爲。
Zero-copy自己實質就是pinned memory而且被映射到了device的地址空間。下面是他的分配API:
cudaError_t cudaHostAlloc(void **pHost, size_t count, unsigned int flags);
其資源釋放固然也是cudaFreeHost,至於flag則是下面幾個選項:
當使用cudaHostAllocDefault時,cudaHostAlloc和cudaMallocHost等價。cudaHostAllocPortable則說明,分配的pinned memory對全部CUDA context都有效,而不是單單執行分配此操做的那個context或者說線程。cudaHostAllocWriteCombined是在特殊系統配置狀況下使用的,這塊pinned memory在PCIE上的傳輸更快,可是對於host本身來講,卻沒什麼效率。因此該選項通常用來讓host去寫,而後device讀。最經常使用的是cudaHostAllocMapped,就是返回一個標準的zero-copy。能夠用下面的API來獲取device端的地址:
cudaError_t cudaHostGetDevicePointer(void **pDevice, void *pHost, unsigned int flags);
flags是保留參數,留待未來使用,目前必須設置爲零。
使用zero-copy memory來做爲device memory的讀寫很頻繁的那部分的補充是很不明智的,pinned這一類適合大數據傳輸,不適合頻繁的操做,究其根本緣由仍是GPU和CPU之間低的可憐的傳輸速度,甚至,頻繁讀寫狀況下,zero-copy表現比global memory也要差很多。
下面一段代買是比較頻繁讀寫狀況下,zero-copy的表現:
int main(int argc, char **argv) { // part 0: set up device and array // set up device int dev = 0; cudaSetDevice(dev); // get device properties cudaDeviceProp deviceProp; cudaGetDeviceProperties(&deviceProp, dev); // check if support mapped memory if (!deviceProp.canMapHostMemory) { printf("Device %d does not support mapping CPU host memory!\n", dev); cudaDeviceReset(); exit(EXIT_SUCCESS); } printf("Using Device %d: %s ", dev, deviceProp.name); // set up date size of vectors int ipower = 10; if (argc>1) ipower = atoi(argv[1]); int nElem = 1<<ipower; size_t nBytes = nElem * sizeof(float); if (ipower < 18) { printf("Vector size %d power %d nbytes %3.0f KB\n", nElem,\ ipower,(float)nBytes/(1024.0f)); } else { printf("Vector size %d power %d nbytes %3.0f MB\n", nElem,\ ipower,(float)nBytes/(1024.0f*1024.0f)); } // part 1: using device memory // malloc host memory float *h_A, *h_B, *hostRef, *gpuRef; h_A = (float *)malloc(nBytes); h_B = (float *)malloc(nBytes); hostRef = (float *)malloc(nBytes); gpuRef = (float *)malloc(nBytes); // initialize data at host side initialData(h_A, nElem); initialData(h_B, nElem); memset(hostRef, 0, nBytes); memset(gpuRef, 0, nBytes); // add vector at host side for result checks sumArraysOnHost(h_A, h_B, hostRef, nElem); // malloc device global memory float *d_A, *d_B, *d_C; cudaMalloc((float**)&d_A, nBytes); cudaMalloc((float**)&d_B, nBytes); cudaMalloc((float**)&d_C, nBytes); // transfer data from host to device cudaMemcpy(d_A, h_A, nBytes, cudaMemcpyHostToDevice); cudaMemcpy(d_B, h_B, nBytes, cudaMemcpyHostToDevice); // set up execution configuration int iLen = 512; dim3 block (iLen); dim3 grid ((nElem+block.x-1)/block.x); // invoke kernel at host side sumArrays <<<grid, block>>>(d_A, d_B, d_C, nElem); // copy kernel result back to host side cudaMemcpy(gpuRef, d_C, nBytes, cudaMemcpyDeviceToHost); // check device results checkResult(hostRef, gpuRef, nElem); // free device global memory cudaFree(d_A); cudaFree(d_B); free(h_A); free(h_B); // part 2: using zerocopy memory for array A and B // allocate zerocpy memory unsigned int flags = cudaHostAllocMapped; cudaHostAlloc((void **)&h_A, nBytes, flags); cudaHostAlloc((void **)&h_B, nBytes, flags); // initialize data at host side initialData(h_A, nElem); initialData(h_B, nElem); memset(hostRef, 0, nBytes); memset(gpuRef, 0, nBytes); // pass the pointer to device cudaHostGetDevicePointer((void **)&d_A, (void *)h_A, 0); cudaHostGetDevicePointer((void **)&d_B, (void *)h_B, 0); // add at host side for result checks sumArraysOnHost(h_A, h_B, hostRef, nElem); // execute kernel with zero copy memory sumArraysZeroCopy <<<grid, block>>>(d_A, d_B, d_C, nElem); // copy kernel result back to host side cudaMemcpy(gpuRef, d_C, nBytes, cudaMemcpyDeviceToHost); // check device results checkResult(hostRef, gpuRef, nElem); // free memory cudaFree(d_C); cudaFreeHost(h_A); cudaFreeHost(h_B); free(hostRef); free(gpuRef); // reset device cudaDeviceReset(); return EXIT_SUCCESS; }
編譯運行:
$ nvcc -O3 -arch=sm_20 sumArrayZerocpy.cu -o sumZerocpy $ nvprof ./sumZerocpy Using Device 0: Tesla M2090 Vector size 1024 power 10 nbytes 4 KB Time(%) Time Calls Avg Min Max Name 27.18% 3.7760us 1 3.7760us 3.7760us 3.7760us sumArraysZeroCopy 11.80% 1.6390us 1 1.6390us 1.6390us 1.6390us sumArrays 25.56% 3.5520us 3 1.1840us 1.0240us 1.5040us [CUDA memcpy HtoD] 35.47% 4.9280us 2 2.4640us 2.4640us 2.4640us [CUDA memcpy DtoH]
下表是嘗試不一樣數組長度後的結果:
./sumZerocopy <size-log-2>
所以,對於共享host和device之間的一小塊內存空間,zero-copy是很好的選擇,由於他簡化的編程並且提供了合理的性能。
在CC2.0以上的設備支持一種新特性:Unified Virtual Addressing(UVA)。這個特性在CUDA4.0中首次介紹,並被64位Linux系統支持。以下圖所示,在使用UVA的狀況下,CPU和GPU使用同一塊連續的地址空間:
在UVA以前,咱們須要分別管理指向host memory和device memory的指針。使用UVA以後,實際指向內存空間的指針對咱們來講是透明的,咱們看到的是同一塊連續地址空間。
這樣,使用cudaHostAlloc分配的pinned memory得到的地址對於device和host來講是通用的。咱們能夠直接在kernel裏使用這個地址。回看前文,咱們對於zero-copy的處理過程是:
使用UVA以後,就不必來獲取device的映射地址了,直接使用一個地址就能夠,以下代碼所示:
// allocate zero-copy memory at the host side cudaHostAlloc((void **)&h_A, nBytes, cudaHostAllocMapped); cudaHostAlloc((void **)&h_B, nBytes, cudaHostAllocMapped); // initialize data at the host side initialData(h_A, nElem); initialData(h_B, nElem); // invoke the kernel with zero-copy memory sumArraysZeroCopy<<<grid, block>>>(h_A, h_B, d_C, nElem);
能夠看到,cudaHostAlloc返回的指針直接就使用在了kernel裏面,編譯指令;
$ nvcc -O3 -arch=sm_20 sumArrayZerocpyUVA.cu -o sumArrayZerocpyUVA
修改後的代碼執行效率和以前的效率是相差無幾的,你們能夠本身動手試試。
理解個大概,之後熟悉了回來補。。。