CUDA程序优化小记(一)

CUDA程序优化小记(一)

 

         CUDA全称Computer Unified Device Architecture(计算机同一设备架构),它的引入为计算机计算速度质的提升提供了可能,从此微型计算机也能有与大型机相当计算的能力。可是不恰当地使用CUDA技术,不仅不会让应用程序获得提升,反而会比普通CPU的计算还要慢。最近我通过学习《GPGPU编程技术》这本书,深刻地体会到了这一点,并且用CUDARuntime应用改写书上的例子程序;来体会CUDA技术给我们计算能力带来的提升。

原创文章,反对未声明的引用。原博客地址:http://blog.csdn.net/gamesdev/article/details/17488237

         我这个程序实现的是一个缩减内核。缩减的意思是从多个数据中提炼出较少的数据。具体来说,我将要实现的是平方和。即a12+ a22+ a32+a42+ a52这样的。首先了解一下CUDA内核的调用方式,即这样:

functionCall<<<dim3 网格大小,dim3 块大小,size_t 共享内存数量,cudaStream_t cuda的流>>>

         CUDA的执行模型是这样的:一次执行任务由一个或若干个网格(grid)组成,每一个格中有若干个块(block),每一个块中有若干个线程(thread),由这些组成了CUDA的执行模型。

CUDA程序优化小记(一)_第1张图片

         好了,我们第一版程序非常简单,参照《GPGPU编程技术》中的算法,再加上CUDARuntime的编程写法,一个简单的程序就写好了。

#include <cuda_runtime.h>
#include <cctype>
#include <cassert>
#include <iostream>
 
#define DATA_SIZE 1048576
#ifndef nullptr
#define nullptr 0
#endif
 
using namespace std;
 
void GenerateData( int* pData,size_t dataSize )// 产生数据
{
    assert( pData != nullptr );
    for ( size_t i = 0; i <dataSize; i++ )
    {
       srand( i + 3 );
       pData[i] = rand( ) % 100;
    }
}
 
__global__ static voidKernel_SquareSum( int* pIn, size_t* pDataSize, int* pOut )
{
    for ( size_t i = 0; i <*pDataSize; ++i )
    {
       *pOut += pIn[i] * pIn[i];
    }
}
 
bool CUDA_SquareSum( int* pOut, int* pIn, size_t dataSize )
{
    assert( pIn != nullptr );
    assert( pOut != nullptr );
 
    int* pDevIn = nullptr;
    int* pDevOut = nullptr;
    size_t* pDevDataSize = nullptr;
 
    // 1、设置设备
    cudaError_t cudaStatus = cudaSetDevice( 0 );
    if ( cudaStatus != cudaSuccess )
    {
       fprintf( stderr, "调用cudaSetDevice()函数失败!" );
       return false;
    }
 
    switch ( true)
    {
    default:
       // 2、分配显存空间
       cudaStatus = cudaMalloc( (void**)&pDevIn,dataSize * sizeof( int) );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMalloc()函数初始化显卡中数组时失败!" );
           break;
       }
 
       cudaStatus = cudaMalloc( (void**)&pDevOut,sizeof( int ));
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMalloc()函数初始化显卡中返回值时失败!" );
           break;
       }
 
       cudaStatus = cudaMalloc( (void**)&pDevDataSize,sizeof( size_t ) );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMalloc()函数初始化显卡中数据大小时失败!" );
           break;
       }
 
       // 3、将宿主程序数据复制到显存中
       cudaStatus = cudaMemcpy( pDevIn, pIn, dataSize * sizeof( int ),cudaMemcpyHostToDevice );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMemcpy()函数初始化宿主程序数据数组到显卡时失败!" );
           break;
       }
 
       cudaStatus = cudaMemcpy( pDevDataSize, &dataSize, sizeof( size_t ), cudaMemcpyHostToDevice );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMemcpy()函数初始化宿主程序数据大小到显卡时失败!" );
           break;
       }
 
       // 4、执行程序,宿主程序等待显卡执行完毕
       Kernel_SquareSum<<<1, 1>>>( pDevIn,pDevDataSize, pDevOut );
 
       // 5、查询内核初始化的时候是否出错
        cudaStatus =cudaGetLastError( );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "显卡执行程序时失败!" );
           break;
       }
 
       // 6、与内核同步等待执行完毕
       cudaStatus = cudaDeviceSynchronize( );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "在与内核同步的过程中发生问题!" );
           break;
       }
 
       // 7、获取数据
       cudaStatus = cudaMemcpy( pOut, pDevOut, sizeof( int ),cudaMemcpyDeviceToHost );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "在将结果数据从显卡复制到宿主程序中失败!" );
           break;
       }
 
       cudaFree( pDevIn );
       cudaFree( pDevOut );
       cudaFree( pDevDataSize );
       return true;
    }
 
    cudaFree( pDevIn );
    cudaFree( pDevOut );
    cudaFree( pDevDataSize );
    return false;
}
 
int g_Data[DATA_SIZE];
int main( int argc, char** argv )
{
    int result;
 
    GenerateData( g_Data, DATA_SIZE );
    CUDA_SquareSum( &result, g_Data, DATA_SIZE );
 
    cout << "用CUDA计算平方和的结果是:" << result << '\n';
 
    return 0;
}

CUDA程序优化小记(一)_第2张图片

编译,运行。我们发现程序能够正常的运行,不过只有一个结果,而且程序的运行速度还是有点儿慢。究竟有多么慢呢?我们还得想办法记录程序运行的时间。在C/C++中,有一个结构clock_t,它其实就是unsigned int,用来表示时间的,需要记录时间时,调用clock()函数(在time.h中)就可以获得当前的CPU时间了。这就是第二版程序制作的初衷。

#include <cuda_runtime.h>
#include <cctype>
#include <cassert>
#include <cstdio>
#include <ctime>
 
#define DATA_SIZE 1048576
#ifndef nullptr
#define nullptr 0
#endif
 
using namespace std;
 
void GenerateData( int* pData,size_t dataSize )// 产生数据
{
    assert( pData != nullptr );
    for ( size_t i = 0; i <dataSize; i++ )
    {
       srand( i + 3 );
       pData[i] = rand( ) % 100;
    }
}
 
////////////////////////在设备上运行的内核函数/////////////////////////////
__global__ static voidKernel_SquareSum( int* pIn, size_t* pDataSize,
                                   int*pOut, clock_t* pElapsed )
{
    // 开始计时
    clock_t startTime = clock( );
 
    for ( size_t i = 0; i <*pDataSize; ++i )
    {
       *pOut += pIn[i] * pIn[i];
    }
 
    *pElapsed = clock( ) - startTime;
}
 
bool CUDA_SquareSum( int* pOut,clock_t* pElapsed,
                  int* pIn, size_tdataSize )
{
    assert( pIn != nullptr );
    assert( pOut != nullptr );
 
    int* pDevIn = nullptr;
    int* pDevOut = nullptr;
    size_t* pDevDataSize = nullptr;
    clock_t* pDevElasped = nullptr;
 
    // 1、设置设备
    cudaError_t cudaStatus = cudaSetDevice( 0 );// 只要机器安装了英伟达显卡,那么会调用成功
    if ( cudaStatus != cudaSuccess )
    {
       fprintf( stderr, "调用cudaSetDevice()函数失败!" );
       return false;
    }
 
    switch ( true)
    {
    default:
       // 2、分配显存空间
       cudaStatus = cudaMalloc( (void**)&pDevIn,dataSize * sizeof( int) );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMalloc()函数初始化显卡中数组时失败!" );
           break;
       }
 
       cudaStatus = cudaMalloc( (void**)&pDevOut,sizeof( int ));
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMalloc()函数初始化显卡中返回值时失败!" );
           break;
       }
 
       cudaStatus = cudaMalloc( (void**)&pDevDataSize,sizeof( size_t ) );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMalloc()函数初始化显卡中数据大小时失败!" );
           break;
       }
 
       cudaStatus = cudaMalloc( (void**)&pDevElasped,sizeof( clock_t ) );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMalloc()函数初始化显卡中耗费用时变量失败!" );
           break;
       }
 
       // 3、将宿主程序数据复制到显存中
       cudaStatus = cudaMemcpy( pDevIn, pIn, dataSize * sizeof( int ),cudaMemcpyHostToDevice );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMemcpy()函数初始化宿主程序数据数组到显卡时失败!" );
           break;
       }
 
       cudaStatus = cudaMemcpy( pDevDataSize, &dataSize, sizeof( size_t ), cudaMemcpyHostToDevice );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "调用cudaMemcpy()函数初始化宿主程序数据大小到显卡时失败!" );
           break;
       }
 
       // 4、执行程序,宿主程序等待显卡执行完毕
       Kernel_SquareSum<<<1, 1>>>( pDevIn,pDevDataSize, pDevOut, pDevElasped );
 
       // 5、查询内核初始化的时候是否出错
       cudaStatus = cudaGetLastError( );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "显卡执行程序时失败!" );
           break;
        }
 
       // 6、与内核同步等待执行完毕
       cudaStatus = cudaDeviceSynchronize( );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "在与内核同步的过程中发生问题!" );
           break;
       }
 
       // 7、获取数据
       cudaStatus = cudaMemcpy( pOut, pDevOut, sizeof( int ),cudaMemcpyDeviceToHost );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "在将结果数据从显卡复制到宿主程序中失败!" );
           break;
       }
 
       cudaStatus = cudaMemcpy( pElapsed, pDevElasped, sizeof( clock_t ), cudaMemcpyDeviceToHost );
       if ( cudaStatus != cudaSuccess)
       {
           fprintf( stderr, "在将耗费用时数据从显卡复制到宿主程序中失败!" );
           break;
       }
 
       cudaFree( pDevIn );
       cudaFree( pDevOut );
       cudaFree( pDevDataSize );
       cudaFree( pDevElasped );
       return true;
    }
 
    cudaFree( pDevIn );
    cudaFree( pDevOut );
    cudaFree( pDevDataSize );
    cudaFree( pDevElasped );
    return false;
}
 
int g_Data[DATA_SIZE] = { 0 };// 之所以定义为全局变量是因为windows程序对栈的限制
int main( int argc, char** argv )// 函数的主入口
{
    int result;
    clock_t elapsed;
 
    GenerateData( g_Data, DATA_SIZE );// 通过随机数产生数据
    CUDA_SquareSum( &result, &elapsed, g_Data, DATA_SIZE );// 执行平方和
 
    // 判断是否溢出
    char* pOverFlow = nullptr;
    if ( result < 0 ) pOverFlow = "(溢出)";
    else pOverFlow = "";
 
    // 显示基准测试
    printf( "用CUDA计算平方和的结果是:%d%s\n耗费用时:%d\n",
       result, pOverFlow, elapsed );
 
    cudaDeviceProp prop;
    if ( cudaGetDeviceProperties(&prop, 0 ) == cudaSuccess )
    {
       clock_t actualTime = elapsed / clock_t( prop.clockRate );
       printf( "实际执行时间为:%dms\n", actualTime);
       printf( "带宽为:%.2fMB/s\n",
           float( DATA_SIZE * sizeof( int )>> 20 ) * 1000.0f / float( actualTime ));
       printf( "GPU设备型号:%s\n", prop.name );
    }
 
    return 0;
}

第二版程序相比第一版程序增加了计时的功能,英伟达的编译器nvcc也是能够识别并且编译stdc的函数的,只是上面提到的clock()函数的作用是获取CPU时间换成获取GPU时间了,因此我们在内核函数(由__global__定义,在本文中指的是Kernel_SquareSum)中进行计时,并且通过CUDA在宿主(host)端的API函数cudaMemcpy来获取之。

获得了GPU时间,还必须转化为我们能觉察到的日常执行时间,计算方法是:执行时间=GPU时间/主频。获得GPU主频的方法是通过cudaDeviceProp结构体以及宿主端的API函数cudaGetDeviceProperties()来获取,其中的clockRate就是主频了。

带宽的获取来自实际执行时间。因为我们的样本量保存在DATA_SIZE中,并且我们使用的是int型,因此带宽的计算公式是:带宽=样本量/执行时间。

结果如下图所示:

CUDA程序优化小记(一)_第3张图片

         我们看到了,在GeForce GT 750M中执行时间为466ms,所用带宽为8.58M/s。如果是在稍老一些的英伟达显卡上,执行时间和要比之长、带宽要比之短。《GPGPU编程》一书中作者所用显卡是GeForce 9600MGT计算的时间是690ms,带宽为5.8MB/s;我的开发机中使用的显卡是GeForce 9500GT,执行时间是971ms,带宽为4.12MB/s。但不管是哪个显卡,程序的执行效率都非常低,带宽也很低,这是因为我们只是拿CUDA中的一个网格,一个块和一个线程执行的,效率比CPU要低多了,下一篇文章将会慢慢讲述CUDA程序是如何优化的。

显卡

执行时间

带宽

GeForce 9500 GT

971ms

4.12MB/s

GeForce 9600M GT

690ms

5.8MB/s

GeForce GT750M

466ms

8.58MB/s

 

         另外说明一下,由于程序中采用了srand()和rand()来产生随机数,因此每次执行的平方和结果都不一样,有时候会出现溢出的现象,不过这不是我们所关心的,我们关心的是CUDA程序的执行效率。

你可能感兴趣的:(CUDA)