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<<>>

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

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

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

#include 
#include 
#include 
#include 
 
#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 >>( 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 
#include 
#include 
#include 
#include 
 
#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 >>( 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,CUDA应用开发)