cuda學習3-共享內存和同步


  為什么要使用共享內存呢,因為共享內存的訪問速度快。這是首先要明確的,下面詳細研究。

  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內的執行效率,但是在同步問題上一定要慎重。。。

 

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM