测量程序运行时间
本节将介绍如何准确地测量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 数量少于 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号流进行测时,得到的结果是可靠的。