CUDA程序优化小记(三)
CUDA全称ComputerUnified Device Architecture(计算机同一设备架构),它的引入为计算机计算速度质的提升提供了可能,从此微型计算机也能有与大型机相当计算的能力。可是不恰当地使用CUDA技术,不仅不会让应用程序获得提升,反而会比普通CPU的计算还要慢。最近我通过学习《GPGPU编程技术》这本书,深刻地体会到了这一点,并且用CUDARuntime应用改写书上的例子程序;来体会CUDA技术给我们计算能力带来的提升。
原创文章,反对未声明的引用。原博客地址:http://blog.csdn.net/gamesdev/article/details/17768591
上次我使用了GPU中的多线程制作了一版程序,它让我们显卡的带宽猛然提升了116倍(使用GeForce GT750M)。在GPU内核中使用多线程是GPGPU的基本功,事实上一个GPU通常能执行百万个线程,这就是使用GPU执行并行程序的威力。‘
程序还是有很大提升的空间的。在上一版程序中,存在的问题还是有的。因为GPU的设备存储器使用的是DRAM,这是一种和内存一样的存储类型,为了高效地写入和读取,应该对此进行顺序访问。其实这样做很像我们顺序操作内存中一块存储空间,如果不这么做会造成不必要的查找时间浪费。
在CUDA对线程进行调度时,通常将时间片按照线程的ID来平均分配给每一线程,尽管时间片的周期非常小,系统依然按照“线程0、线程1、线程2……”进行调度。而如果让每一个线程做到平均跨越式的访问,那么整体看来就像存储器(显存)被顺序访问一样。因此我们可以让每一个线程间隔THREAD_NUM个字节进行访问,那么第一轮所有的线程都会访问[0,THREAD_NUM-1]字节的数据,第二轮所有的线程都会访问[THREAD_NUM,2 * THREAD_NUM]字节的数据。这样做到了顺序访问。
下面是改进后程序的代码:
#include <cuda_runtime.h> #include <cctype> #include <cassert> #include <cstdio> #include <ctime> #define DATA_SIZE 1048576 #define THREAD_NUM 256 #ifndef nullptr #define nullptr 0 #endif using namespace std; ////////////////////////在设备上运行的内核函数///////////////////////////// __global__ static voidKernel_SquareSum( int* pIn, size_t* pDataSize, int*pOut, clock_t* pElapsed ) { const size_t computeSize =*pDataSize / THREAD_NUM; const size_t tID = size_t(threadIdx.x ); // 开始计时 clock_t startTime; if ( tID == 0 ) startTime =clock( );// 选择任意一个线程进行计时 for ( size_t i = tID *computeSize; i < ( tID + 1 ) * computeSize; ++i ) { pOut[threadIdx.x] += pIn[i] * pIn[i]; } if ( tID == 0 ) *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,THREAD_NUM * 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,THREAD_NUM>>>( 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, THREAD_NUM * 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; } 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; } } int main( int argc, char** argv )// 函数的主入口 { int* pData = nullptr; int* pResult = nullptr; clock_t* pElapsed = nullptr; // 使用CUDA内存分配器分配host端 cudaError_t cudaStatus = cudaMallocHost( &pData, DATA_SIZE * sizeof( int ) ); if ( cudaStatus != cudaSuccess ) { fprintf( stderr, "在主机中分配资源失败!" ); return 1; } cudaStatus = cudaMallocHost( &pResult, THREAD_NUM * sizeof( int ) ); if ( cudaStatus != cudaSuccess ) { fprintf( stderr, "在主机中分配资源失败!" ); return 1; } cudaStatus = cudaMallocHost( &pElapsed, sizeof( clock_t ) ); if ( cudaStatus != cudaSuccess ) { fprintf( stderr, "在主机中分配资源失败!" ); return 1; } GenerateData( pData, DATA_SIZE );// 通过随机数产生数据 CUDA_SquareSum( pResult, pElapsed, pData, DATA_SIZE );// 执行平方和 // 在CPU中将结果组合起来 int totalResult; for ( inti = 0; i < THREAD_NUM; ++i ) { totalResult += pResult[i]; } // 判断是否溢出 char* pOverFlow = nullptr; if ( totalResult < 0 )pOverFlow = "(溢出)"; else pOverFlow = ""; // 显示基准测试 printf( "用CUDA计算平方和的结果是:%d%s\n耗费用时:%d\n", totalResult, pOverFlow, *pElapsed ); cudaDeviceProp prop; if ( cudaGetDeviceProperties(&prop, 0 ) == cudaSuccess ) { clock_t actualTime = *pElapsed / 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 ); } cudaFreeHost( pData ); cudaFreeHost( pResult ); cudaFreeHost( pElapsed ); return 0; }
下面是程序的截图:
我们可以从中看到,使用显卡GeForce GT750M花费的时间是1ms,比上一版快4倍,带宽也有显著的提升。下面是三种显卡数据的对比:
显卡 |
执行时间 |
带宽 |
GeForce 9500 GT |
4ms |
1000.00MB/s |
GeForce 9600M GT |
3ms |
1.33GB/s |
GeForce GT750M |
1ms |
4000.0MB/s |