CUDA 程序的优化(2) 测量程序运行时间

测量程序运行时间

本节将介绍如何准确地测量CUDA程序的运行时间。CUDA的内核程序运行时间可以在设备端测量,也可以在主机端测量。而CUDA API的运行时间则只能从主机端测量。无论是主机端测时还是设备端测时,最好都测量内核函数多次运行的时间,然后再除以运行次数以获得更加准确的结果。使用CUDA runtime API时,会在第一次调用runtime API函数时启动CUDA环境。为了避免将这一部分时间计入,最好在正式测时开始前先进行一次包含数据输入输出的计算,这样也可以使GPU从平时的节能模式进入工作状态,使测试结果更加可靠。

4.2.1设备端测时

设备端测时使用GPU中的计时器的时戳计时。实现设备端测时有两种不同的方法,分别是调用clock()函数和使用CUDA API的事件管理功能。

使用clock()函数计时,在内核函数中要测量的一段代码的开始和结束的位置分别调用一次clock()函数,并将结果记录下来。由于调用_synothreads()函数后,一个block中的所有thread需要的时间是相同的,因此只需要记录每个block执行需要的时间就行了,而不需要记录每个thread的时间。clock()函数的返回值的单位是GPU的时钟周期,需要除以GPU的运行频率才能得到以秒为单位的时间。这里测得的时间是一个block在GPU中上下文保持的时间,而不是实际执行需要的时间;每个block实际执行的时间一般要短于测得的结果。下面是一个使用clock函数测时的例子。

设备端代码:

#ifndef _CLOCK_KERNEL_H_ 
#define _CLOCK_KERNEL_H_ 
 
//  这段代码测量进行归约运算时每个 block 使用的时钟周期数,并将结果存储在显存中 
__global__ static void timedReduction(const float * input, float * output, clock_t * timer) 
{ 
     // __shared__ float shared[2 * blockDim.x]; 
     extern __shared__ float shared[]; 
 
     const int tid = threadIdx.x; 
     const int bid = blockIdx.x; 
  
  //记录测时开始时的时戳 
     if (tid == 0) timer[bid] = clock(); 
 
     // Copy input. 
     shared[tid] = input[tid]; 
     shared[tid + blockDim.x] = input[tid + blockDim.x]; 
 
     // Perform reduction to find minimum. 
     for(int d = blockDim.x; d > 0; d /= 2) 
     { 
         __syncthreads(); 
 
         if (tid < d) 
         { 
             float f0 = shared[tid]; 
             float f1 = shared[tid + d]; 
             
             if (f1 < f0) { 
                 shared[tid] = f1; 
             } 
         } 
     } 
 
     // Write result. 
     if (tid == 0) output[bid] = shared[0]; 
 
     __syncthreads(); 
     //记录测时结束时的时戳 
     if (tid == 0) timer[bid+gridDim.x] = clock(); 
} 
 
#endif // _CLOCK_KERNEL_H_ 


下面是主机端代码,主机端代码根据设备端代码返回时戳的计算时间。

#include <stdio.h> 
#include <stdlib.h> 
 
#include <cutil_inline.h> 
 
#include "clock_kernel.cu" 
 
//  本程序用于演示如何精确地测量内核执行时间 
// Block 之间是并行、乱序执行的,本例测量每一个 block 的执行时间 
 
#define NUM_BLOCKS     64 
#define NUM_THREADS    256 
 
int main(int argc, char** argv) 
 
{ 
     //  使用参数中指定的设备,或者使用浮点处理能力最高的设备 
 
     if ( cutCheckCmdLineFlag(argc, (const char **)argv, "device")) 
      cutilDeviceInit(argc, argv); 
     else 
      cudaSetDevice( cutGetMaxGflopsDeviceId() ); 
 
     float * dinput = NULL; 
     float * doutput = NULL; 
     clock_t * dtimer = NULL; 
 
     clock_t timer[NUM_BLOCKS * 2]; 
     float input[NUM_THREADS * 2]; 
 
     for (int i = 0; i < NUM_THREADS * 2; i++) 
     { 
         input[i] = (float)i; 
     } 
 
     cutilSafeCall(cudaMalloc((void**)&dinput, sizeof(float) * NUM_THREADS * 2)); 
     cutilSafeCall(cudaMalloc((void**)&doutput, sizeof(float) * NUM_BLOCKS)); 
     cutilSafeCall(cudaMalloc((void**)&dtimer, sizeof(clock_t) * NUM_BLOCKS * 2)); 
     cutil SafeCall(cudaMemcpy(dinput, input, sizeof(float) * NUM_THREADS * 2, cudaMemcpyHostToDevice)); 
 
     timedReduction<<<NUM_BLOCKS,  NUM_THREADS,  sizeof(float)  *  2  *  NUM_THREADS>>>(dinput, 
doutput, dtimer); 
 
     //cutilSafeCall(cudaMemcpy(output, doutput, sizeof(float) * NUM_BLOCKS, cudaMemcpyDeviceToHost)); 
     cutil SafeCall(cudaMemcpy(timer, dtimer, sizeof(clock_t) * NUM_BLOCKS * 2, cudaMemcpyDeviceToHost)); 
 
     cutilSafeCall(cudaFree(dinput)); 
     cutilSafeCall(cudaFree(doutput)); 
     cutilSafeCall(cudaFree(dtimer)); 
 
     // This test always passes. 
     printf( "Test PASSED\n"); 
 
     //  计算第一个 block 开始时到最后一个 block 结束之间的时戳数 
     clock_t minStart = timer[0]; 
     clock_t maxEnd = timer[NUM_BLOCKS]; 
 
     for (int i = 1; i < NUM_BLOCKS; i++) 
     { 
         min Start = timer[i] < minStart ? timer[i] : minStart; 
         maxEnd = timer[NUM_BLOCKS+i] > maxEnd ? timer[NUM_BLOCKS+i] : maxEnd; 
     } 
 
     printf("time = %d\n", maxEnd - minStart); 
 
     cudaThreadExit(); 
 
     cutilExit(argc, argv); 
} 

注意:改变 block 和 thread 的数量,会影响 GPU 执行的效率。例如,在 G80(16 个 SM)

上执行这段代码时,结果如下:
Block 数量  1  8  16  32  64时钟周期数  3096  3232  3364  4615  9981

可以发现,当 block 数量少于 SM 数量时,由于一部分 SM 闲置,因此运行时间没有什么变化。当 block 数量达到 16 时,每个 SM 只分到一个 block,依然不能很好地隐藏访存延迟,因此 block 数量从 16 增加到 32 时执行时间没有翻倍。当 block 数量达到 64 时,执行时间才随着 block 数量的增加而线性增加。

使用 CUDA API 的事件管理功能计时则相对简单,下面是一段示意代码:

cudaEvent_t start, stop; 
float time;  
cudaEventCreate(&start);  
cudaEventCreate(&stop);  
cudaEventRecord( start, 0 ); 
kernel<<<grid,threads>>> ( d_odata, d_idata, size_x, size_y, NUM_REPS); cudaEventRecord( stop, 0 ); 
cudaEventSynchronize( stop ); 
cudaEventElapsedTime( &time, start, stop ); 
cudaEventDestroy( start ); 
cudaEventDestroy( stop ); 

注意 cudaEventElapsedTime()函数返回的时间已经以毫秒为单位,精度为 0.5 微秒。

4.2.2 主机端测时

与普通程序测时一样,CUDA的主机端测时也采用CPU的计时器测时。通常取得CPU中计时器的值的方法是调用汇编中的相应指令,或者操作系统提供的API。此外,一些函数库,如C标准库中的time库的clock_t()函数也可以用来测时。不过,clock_t()函数的精度很低,建议在两次调用clock_ t()时,让待测程序运行至少数十次,运行时间达到数秒,再取平均求得每次运行时间。

使用CPU测时,一定要牢记CUDA API的函数都是异步的。这就是说,在一个CUDA API函数在GPU上执行完成之前,CPU线程就己经得到了它的返回值。内核函数和带有asyn后缀的存储器拷贝函数都是异步的。

要从主机端准确的测量一个或者一系列CUDA调用需要的时间,就要先调用
cudaThreadSynchronize()函数,同步CPU线程与GPU之后,才能结束CPU测时。

cudaThreadSynchronize()函数的功能是阻塞CPU线程,直到cudaThreadSynchronize()函数之前所有的CUDA调用都己经完成。

与cudaThreadSynchronize()函数类似的函数有cudaStreamSynchronize()和
cudaEventSynchronize()。它们的作用是阻塞所有Stream/CUDA Events,直到这条函数前的所有CUDA调用都己完成。注意,同一串流中的各个流可能会交替执行,因此即使使用了cudaStreamSynchronize()函数,也很难测得准确的执行时间。

不过,一串流中的第一个流(ID为0的流)的行为总是同步的,因此使用这些函数对0号流进行测时,得到的结果是可靠的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值