技术开发 频道

CUDA入门:CUDA程序优化步骤

  Thread 的同步

  前面提过,一个 block 内的 thread 可以有共享的内存,也可以进行同步。我们可以利用这一点,让每个 block 内的所有 thread 把自己计算的结果加总起来。把 kernel 改成如下:

__global__ static void sumOfSquares(int *num, int* result,
    clock_t
* time)
{
    
extern __shared__ int shared[];
    
const int tid = threadIdx.x;
    
const int bid = blockIdx.x;
    
int i;
    
if(tid == 0) time[bid] = clock();
    shared[tid]
= 0;
    
for(i = bid * THREAD_NUM + tid; i < DATA_SIZE;
        i
+= BLOCK_NUM * THREAD_NUM) {
       shared[tid]
+= num[i] * num[i];
    }

    __syncthreads();
    
if(tid == 0) {
        
for(i = 1; i < THREAD_NUM; i++) {
            shared[
0] += shared[i];
        }
        result[bid]
= shared[0];
    }

    
if(tid == 0) time[bid + BLOCK_NUM] = clock();
}
 

  利用 __shared__ 声明的变量表示这是 shared memory,是一个 block 中每个 thread 都共享的内存。它会使用在 GPU 上的内存,所以存取的速度相当快,不需要担心 latency 的问题。

  __syncthreads() 是一个 CUDA 的内部函数,表示 block 中所有的 thread 都要同步到这个点,才能继续执行。在我们的例子中,由于之后要把所有 thread 计算的结果进行加总,所以我们需要确定每个 thread 都已经把结果写到 shared[tid] 里面了。

  接下来,把 main 函式的一部份改成:

    int* gpudata, *result;
    clock_t
* time;
    cudaMalloc((
void**) &gpudata, sizeof(int) * DATA_SIZE);
    cudaMalloc((
void**) &result, sizeof(int) * BLOCK_NUM);
    cudaMalloc((
void**) &time, sizeof(clock_t) * BLOCK_NUM * 2);
    cudaMemcpy(gpudata, data,
sizeof(int) * DATA_SIZE,
        cudaMemcpyHostToDevice);

    sumOfSquares
<<<BLOCK_NUM, THREAD_NUM,
        THREAD_NUM
* sizeof(int)>>>(gpudata, result, time);

    
int sum[BLOCK_NUM];
    clock_t time_used[BLOCK_NUM
* 2];
    cudaMemcpy(
&sum, result, sizeof(int) * BLOCK_NUM,
        cudaMemcpyDeviceToHost);
    cudaMemcpy(
&time_used, time, sizeof(clock_t) * BLOCK_NUM * 2,
        cudaMemcpyDeviceToHost);
    cudaFree(gpudata);
    cudaFree(result);
    cudaFree(time);

    
int final_sum = 0;
    
for(int i = 0; i < BLOCK_NUM; i++) {
        final_sum
+= sum[i];
    }

  可以注意到,现在 CPU 只需要加总 BLOCK_NUM 也就是 32 个数字就可以了。

  不过,这个程序由于在 GPU 上多做了一些动作,所以它的效率会比较差一些。在 GeForce 8800GT 上,它需要约 164K cycles。

  当然,效率会变差的一个原因是,在这一版的程序中,最后加总的工作,只由每个 block 的 thread 0 来进行,但这并不是最有效率的方法。理论上,把 256 个数字加总的动作,是可以并行化的。最常见的方法,是透过树状的加法:

 

  把 kernel 改成如下:

__global__ static void sumOfSquares(int *num, int* result,
    clock_t
* time)
{
    
extern __shared__ int shared[];
    
const int tid = threadIdx.x;
    
const int bid = blockIdx.x;
    
int i;
    
int offset = 1, mask = 1;
    
if(tid == 0) time[bid] = clock();
    shared[tid]
= 0;
    
for(i = bid * THREAD_NUM + tid; i < DATA_SIZE;
        i
+= BLOCK_NUM * THREAD_NUM) {
       shared[tid]
+= num[i] * num[i];
    }

    __syncthreads();
    
while(offset < THREAD_NUM) {
        
if((tid & mask) == 0) {
            shared[tid]
+= shared[tid + offset];
        }
        offset
+= offset;
        mask
= offset + mask;
        __syncthreads();
    }

    
if(tid == 0) {
        result[bid]
= shared[0];
        time[bid
+ BLOCK_NUM] = clock();
    }
}
 

  后面的 while 循环就是进行树状加法。main 函式则不需要修改。

  这一版的程序,在 GeForce 8800GT 上执行需要的时间,大约是 140K cycles(相当于约 43GB/s),比完全不在 GPU 上进行加总的版本还快!这是因为,在完全不在 GPU 上进行加总的版本,写入到 global memory 的数据数量很大(8192 个数字),也对效率会有影响。所以,这一版程序不但在 CPU 上的运算需求降低,在 GPU 上也能跑的更快。

  进一步改善

  上一个版本的树状加法是一般的写法,但是它在 GPU 上执行的时候,会有 share memory 的 bank conflict 的问题(详情在后面介绍 GPU 架构时会提到)。采用下面的方法,可以避免这个问题:

    offset = THREAD_NUM / 2;
    
while(offset > 0) {
        
if(tid < offset) {
            shared[tid]
+= shared[tid + offset];
        }
        offset
>>= 1;
        __syncthreads();
    }

  这样同时也省去了 mask 变数。因此,这个版本的执行的效率就可以再提高一些。在 GeForce 8800GT 上,这个版本执行的时间是约 137K cycles。当然,这时差别已经很小了。如果还要再提高效率,可以把树状加法整个展开:

    if(tid < 128) { shared[tid] += shared[tid + 128]; }
    __syncthreads();
    
if(tid < 64) { shared[tid] += shared[tid + 64]; }
    __syncthreads();
    
if(tid < 32) { shared[tid] += shared[tid + 32]; }
    __syncthreads();
    
if(tid < 16) { shared[tid] += shared[tid + 16]; }
    __syncthreads();
    
if(tid < 8) { shared[tid] += shared[tid + 8]; }
    __syncthreads();
    
if(tid < 4) { shared[tid] += shared[tid + 4]; }
    __syncthreads();
    
if(tid < 2) { shared[tid] += shared[tid + 2]; }
    __syncthreads();
    
if(tid < 1) { shared[tid] += shared[tid + 1]; }
    __syncthreads();

  当然这只适用于 THREAD_NUM 是 256 的情形。这样可以再省下约 1000 cycles 左右(约 44GB/s)。

        更多内容请点击:

       CUDA专区:http://cuda.it168.com/

       CUDA论坛:http://cudabbs.it168.com/

0
相关文章