為什么要使用共享內存呢,因為共享內存的訪問速度快。這是首先要明確的,下面詳細研究。
cuda程序中的內存使用分為主機內存(host memory) 和 設備內存(device memory),我們在這里關注的是設備內存。設備內存都位於gpu之上,前面我們看到在計算開始之前,每次我們都要在device上申請內存空間,然后把host上的數據傳入device內存。cudaMalloc()申請的內存,還有在核函數中用正常方法申請的變量的內存。這些內存叫做全局內存,那么還有沒有別的內存種類呢?常用的還有共享內存,常量內存,紋理內存,他們都用一些不正常的方法申請。
他們的申請方法如下:
共享內存:__shared__ 變量類型 變量名;
常量內存:__constant__ 變量類型 變量名;
紋理內存:texture<變量類型> 變量名;
| 存儲類型 | 寄存器 | 共享內存 | 紋理內存 | 常量內存 | 全局內存 |
| 帶寬 | ~8TB/s | ~1.5TB/s | ~200MB/s | ~200MB/s | ~200MB/s |
| 延遲 | 1個周期 | 1~32周期 | 400~600周期 | 400~600周期 | 400~600周期 |
他們在不同的情況下有各自的作用,他們最大的區別就是帶寬不同,通俗說就是訪問速度不同。后面三個看起來沒什么不同,但是他們在物理結構方面有差別,適用於不同的情況。
共享內存實際上是可受用戶控制的一級緩存。申請共享內存后,其內容在每一個用到的block被復制一遍,使得在每個block內,每一個thread都可以訪問和操作這塊內存,而無法訪問其他block內的共享內存。這種機制就使得一個block之內的所有線程可以互相交流和合作。下面的例子中就顯示了線程之間的交流和合作。
這個例子計算的是兩個向量的點積。
1 /* 2 * Copyright 1993-2010 NVIDIA Corporation. All rights reserved. 3 * 4 * NVIDIA Corporation and its licensors retain all intellectual property and 5 * proprietary rights in and to this software and related documentation. 6 * Any use, reproduction, disclosure, or distribution of this software 7 * and related documentation without an express license agreement from 8 * NVIDIA Corporation is strictly prohibited. 9 * 10 * Please refer to the applicable NVIDIA end user license agreement (EULA) 11 * associated with this source code for terms and conditions that govern 12 * your use of this NVIDIA software. 13 * 14 */ 15 16 17 #include "../common/book.h" 18 19 #define imin(a,b) (a<b?a:b) 20 21 const int N = 33 * 1024; 22 const int threadsPerBlock = 256; 23 const int blocksPerGrid = 24 imin( 32, (N+threadsPerBlock-1) / threadsPerBlock ); 25 26 27 __global__ void dot( float *a, float *b, float *c ) { 28 __shared__ float cache[threadsPerBlock]; 29 int tid = threadIdx.x + blockIdx.x * blockDim.x; 30 int cacheIndex = threadIdx.x; 31 32 float temp = 0; 33 while (tid < N) { 34 temp += a[tid] * b[tid]; 35 tid += blockDim.x * gridDim.x; 36 } 37 38 // set the cache values 39 cache[cacheIndex] = temp; 40 41 // synchronize threads in this block 42 __syncthreads(); 43 44 // for reductions, threadsPerBlock must be a power of 2 45 // because of the following code 46 int i = blockDim.x/2; 47 while (i != 0) { 48 if (cacheIndex < i) 49 cache[cacheIndex] += cache[cacheIndex + i]; 50 __syncthreads(); 51 i /= 2; 52 } 53 54 if (cacheIndex == 0) 55 c[blockIdx.x] = cache[0]; 56 } 57 58 59 int main( void ) { 60 float *a, *b, c, *partial_c; 61 float *dev_a, *dev_b, *dev_partial_c; 62 63 // allocate memory on the cpu side 64 a = (float*)malloc( N*sizeof(float) ); 65 b = (float*)malloc( N*sizeof(float) ); 66 partial_c = (float*)malloc( blocksPerGrid*sizeof(float) ); 67 68 // allocate the memory on the GPU 69 HANDLE_ERROR( cudaMalloc( (void**)&dev_a, 70 N*sizeof(float) ) ); 71 HANDLE_ERROR( cudaMalloc( (void**)&dev_b, 72 N*sizeof(float) ) ); 73 HANDLE_ERROR( cudaMalloc( (void**)&dev_partial_c, 74 blocksPerGrid*sizeof(float) ) ); 75 76 // fill in the host memory with data 77 for (int i=0; i<N; i++) { 78 a[i] = i; 79 b[i] = i*2; 80 } 81 82 // copy the arrays 'a' and 'b' to the GPU 83 HANDLE_ERROR( cudaMemcpy( dev_a, a, N*sizeof(float), 84 cudaMemcpyHostToDevice ) ); 85 HANDLE_ERROR( cudaMemcpy( dev_b, b, N*sizeof(float), 86 cudaMemcpyHostToDevice ) ); 87 88 dot<<<blocksPerGrid,threadsPerBlock>>>( dev_a, dev_b, 89 dev_partial_c ); 90 91 // copy the array 'c' back from the GPU to the CPU 92 HANDLE_ERROR( cudaMemcpy( partial_c, dev_partial_c, 93 blocksPerGrid*sizeof(float), 94 cudaMemcpyDeviceToHost ) ); 95 96 // finish up on the CPU side 97 c = 0; 98 for (int i=0; i<blocksPerGrid; i++) { 99 c += partial_c[i]; 100 } 101 102 #define sum_squares(x) (x*(x+1)*(2*x+1)/6) 103 printf( "Does GPU value %.6g = %.6g?\n", c, 104 2 * sum_squares( (float)(N - 1) ) ); 105 106 // free memory on the gpu side 107 HANDLE_ERROR( cudaFree( dev_a ) ); 108 HANDLE_ERROR( cudaFree( dev_b ) ); 109 HANDLE_ERROR( cudaFree( dev_partial_c ) ); 110 111 // free memory on the cpu side 112 free( a ); 113 free( b ); 114 free( partial_c ); 115 }
我們首先關注核函數dot。__shared__ float cache[threadsPerBlock];就是這節重點,申請cache數組時,由於使用了共享內存,則每一個block里面都有一份cache,使得block內的thread都可以訪問和操作其各自的cache數組。
1 while (tid < N) { 2 temp += a[tid] * b[tid]; 3 tid += blockDim.x * gridDim.x; 4 }
這一段我們相當熟悉,每個線程計算若干對a,b的乘積,然后相加。然后這樣cache[cacheIndex] = temp;將結果存入cache中。這時,每一個線程的結果都被存在了cache數組中,我們知道接下來要對數組求和,然而這里有潛在的危險,那就是我們不知道所有線程是否已經將數據寫入了cache,也就是說,是否每一個線程都已經執行完了第39行。這里就需要等待,等待所有線程執行到同一位置,這就是 __syncthreads();的作用。這個函數稱為同步函數,即在所有線程全部執行到__syncthreads()為止,誰也不許動,其后任何代碼都無法執行。
因此,我們可以很清楚的明白所有線程全部執行完了第39行,然后同步解除,大家再一起往前走。做加法。
1 int i = blockDim.x/2; 2 while (i != 0) { 3 if (cacheIndex < i) 4 cache[cacheIndex] += cache[cacheIndex + i]; 5 __syncthreads(); 6 i /= 2; 7 } 8 9 if (cacheIndex == 0) 10 c[blockIdx.x] = cache[0];
這段就不難理解了,逐對相加,最后cache【0】位置的數就是結果。將其值存入c數組,准備導出。
剩下的main函數部分是如下幾步操作(和前面學習的差不多):
1.為輸入輸出數組分配內存
2.將a,b數組付初值,然后復制給device中,cudaMemcpy()
3.調用核函數執行並行計算。
4.device值返回后數組c求和。
很明顯,由於我們使用了共享內存存儲cache數組,使得在操作cache數組時的速度有了大幅提高(相比於全局內存)。共享內存的意義也就在此。
現在,請觀察下面的兩組代碼:
while (i != 0) { if (cacheIndex < i) cache[cacheIndex] += cache[cacheIndex + i]; __syncthreads(); i /= 2; }
while (i != 0) { if (cacheIndex < i) { cache[cacheIndex] += cache[cacheIndex + i]; __syncthreads(); } i /= 2; }
下面的代碼中由於if的存在,只有部分線程包含同步操作。代碼似乎得到了優化。但是真的如此嗎
當然不是的,上面的紅字“所有線程全部執行到__syncthreads()為止”,所有很重要,<<<>>>中launch了多少個threadperblock,那么就必須要等待所有的線程,一個都不能少。由於if的存在,上例中部分線程永遠都不可能執行到cache[cacheIndex] += cache[cacheIndex + i];這一步,因此就要永遠等待下去,因而程序無法執行。
總結:在能用共享內存的時候盡量用,進而提高block內的執行效率,但是在同步問題上一定要慎重。。。
