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的执行模型。
好了,我们第一版程序非常简单,参照《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;
}
编译,运行。我们发现程序能够正常的运行,不过只有一个结果,而且程序的运行速度还是有点儿慢。究竟有多么慢呢?我们还得想办法记录程序运行的时间。在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型,因此带宽的计算公式是:带宽=样本量/执行时间。
结果如下图所示:
我们看到了,在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程序的执行效率。