关于性能:C vs OpenCL,如何比较时间测量结果?

C vs OpenCL, how to compare results of time measurement?

因此,在另一篇文章中,我询问了有关C时间测量的问题。现在,我想知道如何比较C " function "与OpenCL " function "

的结果

这是主机OpenCL和C

的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#define PROGRAM_FILE"sum.cl"
#define KERNEL_FUNC"float_sum"
#define ARRAY_SIZE 1000000


#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#include <CL/cl.h>

int main()
{
    /* OpenCL Data structures */

    cl_platform_id platform;
    cl_device_id device;
    cl_context context;
    cl_program program;
    cl_kernel kernel;    
    cl_command_queue queue;
    cl_mem vec_buffer, result_buffer;

    cl_event prof_event;;

    /* ********************* */

    /* C Data Structures / Data types */
    FILE *program_handle; //Kernel file handle
    char *program_buffer; //Kernel buffer

    float *vec, *non_parallel;
    float result[ARRAY_SIZE];

    size_t program_size; //Kernel file size

    cl_ulong time_start, time_end, total_time;

    int i;
    /* ****************************** */

    /* Errors */
    cl_int err;
    /* ****** */

    non_parallel = (float*)malloc(ARRAY_SIZE * sizeof(float));
    vec          = (float*)malloc(ARRAY_SIZE * sizeof(float));

    //Initialize the vector of floats
    for(i = 0; i < ARRAY_SIZE; i++)
    vec[i] = i + 1;

    /************************* C Function **************************************/
    clock_t start, end;

    start = clock();

    for( i = 0; i < ARRAY_SIZE; i++)
    {
    non_parallel[i] = vec[i] * vec[i];
    }
    end = clock();
    printf("Number of seconds: %f\
"
, (clock()-start)/(double)CLOCKS_PER_SEC );

    free(non_parallel);
    /***************************************************************************/




    clGetPlatformIDs(1, &platform, NULL);//Just want NVIDIA platform
    clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);
    context = clCreateContext(NULL, 1, &device, NULL, NULL, &err);

    // Context error?
    if(err)
    {
    perror("Cannot create context");
    return 1;
    }

    //Read the kernel file
    program_handle = fopen(PROGRAM_FILE,"r");
    fseek(program_handle, 0, SEEK_END);
    program_size = ftell(program_handle);
    rewind(program_handle);

    program_buffer = (char*)malloc(program_size + 1);
    program_buffer[program_size] = '\\0';
    fread(program_buffer, sizeof(char), program_size, program_handle);
    fclose(program_handle);

    //Create the program
    program = clCreateProgramWithSource(context, 1, (const char**)&program_buffer,
                    &program_size, &err);

    if(err)
    {
    perror("Cannot create program");
    return 1;
    }

    free(program_buffer);

    clBuildProgram(program, 0, NULL, NULL, NULL, NULL);

    kernel = clCreateKernel(program, KERNEL_FUNC, &err);

    if(err)
    {
    perror("Cannot create kernel");
    return 1;
    }

    queue = clCreateCommandQueue(context, device, CL_QUEU_PROFILING_ENABLE, &err);

    if(err)
    {
    perror("Cannot create command queue");
    return 1;
    }

    vec_buffer = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
                sizeof(float) * ARRAY_SIZE, vec, &err);
    result_buffer = clCreateBuffer(context, CL_MEM_WRITE_ONLY, sizeof(float)*ARRAY_SIZE, NULL, &err);

    if(err)
    {
    perror("Cannot create the vector buffer");
    return 1;
    }

    clSetKernelArg(kernel, 0, sizeof(cl_mem), &vec_buffer);
    clSetKernelArg(kernel, 1, sizeof(cl_mem), &result_buffer);

    size_t global_size = ARRAY_SIZE;
    size_t local_size = 0;

    clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global_size, NULL, 0, NULL, &prof_event);

    clEnqueueReadBuffer(queue, result_buffer, CL_TRUE, 0, sizeof(float)*ARRAY_SIZE, &result, 0, NULL, NULL);
    clFinish(queue);



     clGetEventProfilingInfo(prof_event, CL_PROFILING_COMMAND_START,
           sizeof(time_start), &time_start, NULL);
     clGetEventProfilingInfo(prof_event, CL_PROFILING_COMMAND_END,
           sizeof(time_end), &time_end, NULL);
     total_time += time_end - time_start;

    printf("\
Average time in nanoseconds = %lu\
"
, total_time/ARRAY_SIZE);



    clReleaseMemObject(vec_buffer);
    clReleaseMemObject(result_buffer);
    clReleaseKernel(kernel);
    clReleaseCommandQueue(queue);
    clReleaseProgram(program);
    clReleaseContext(context);

    free(vec);

    return 0;
}

内核是:

1
2
3
4
__kernel void float_sum(__global float* vec,__global float* result){
    int gid = get_global_id(0);
    result[gid] = vec[gid] * vec[gid];
}

现在,结果是:

Number of seconds: 0.010000 <- This is the for the C code

Average time in nanoseconds = 140737284 <- OpenCL function

0,1407秒是内核执行OpenCL的时间,它比C函数还要多,是否正确?因为我认为OpenCL应该比C非并行算法更快...


在GPU上执行并行代码不一定比在CPU上执行并行代码要快。请注意,除了计算之外,您还必须在GPU内存之间来回传输数据。

在您的示例中,您要传输2 * N个项目并并行执行O(N)操作,这对GPU的使用效率非常低。因此,对于这种特定的计算,很可能CPU确实确实更快。


这是您的应用程序的一个大问题:

1
2
size_t global_size = ARRAY_SIZE;
size_t local_size = 0;

您正在创建单项工作组,这将使大多数GPU处于闲置状态。在许多情况下,使用单项工作组只会占用您GPU的1/15。

相反,请尝试以下操作:

1
2
size_t global_size = ARRAY_SIZE / 250; //important: local_size*global_size must equal ARRAY_SIZE
size_t local_size = 250; //a power of 2 works well. 250 is close enough, yes divisible by your 1M array size

现在,您正在创建大型组,以更好地饱和图形硬件的ALU。内核将按照您现在拥有的方式运行良好,但是也有一些方法可以从内核部分获得更多收益。

内核优化:将ARRAY_SIZE作为附加参数传递给内核,并使用具有最佳组大小的较少组。您还将消除local_size * global_size完全等于ARRAY_SIZE的需要。在此内核中永远不会使用工作项的全局ID,也不需要它,因为传入了总大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__kernel void float_sum(__global float* vec,__global float* result,__global int count){
  int lId = get_local_id(0);
  int lSize = get_local_size(0);
  int grId = get_group_id(0);
  int totalOps = count/get_num_groups(0);
  int startIndex = grId * totalOps;
  int maxIndex = startIndex+totalOps;
  if(grId == get_num_groups(0)-1){
    endIndex = count;
  }
  for(int i=startIndex+lId;i<endIndex;i+=lSize){
    result[i] = vec[i] * vec[i];
  }
}

现在,您可能会认为这样一个简单的内核有很多变量。请记住,每次执行内核都会对数据执行多项操作,而不仅仅是一项。使用以下值,在我的radeon 5870(20个计算单元)上,每个工作项最终在其for循环中计算781或782个值。每个组将计算50000条数据。我使用的变量的开销远远小于创建4000个工作组的开销-或100万个工作组。

1
2
size_t global_size = ARRAY_SIZE / numComputeUnits;
size_t local_size = 64; //also try other multiples of 16 or 64 for gpu; or multiples of your core-count for a cpu kernel

请参阅此处有关如何获取numComputeUnits的值的信息。


只为其他人寻求帮助:使用OpenCL概要分析内核运行时的简短介绍

启用概要分析模式:

1
cmdQueue = clCreateCommandQueue(context, *devices, CL_QUEUE_PROFILING_ENABLE, &err);

分析内核:

1
2
cl_event prof_event;
clEnqueueNDRangeKernel(cmdQueue, kernel, 1 , 0, globalWorkSize, NULL, 0, NULL, &prof_event);

读取以下位置的分析数据:

1
2
3
4
5
6
7
cl_ulong ev_start_time=(cl_ulong)0;    
cl_ulong ev_end_time=(cl_ulong)0;  

clFinish(cmdQueue);
err = clWaitForEvents(1, &prof_event);
err |= clGetEventProfilingInfo(prof_event, CL_PROFILING_COMMAND_START, sizeof(cl_ulong), &ev_start_time, NULL);
err |= clGetEventProfilingInfo(prof_event, CL_PROFILING_COMMAND_END, sizeof(cl_ulong), &ev_end_time, NULL);

计算内核执行时间:

1
float run_time_gpu = (float)(ev_end_time - ev_start_time)/1000; // in usec

您使用

的方法

1
total_time/ARRAY_SIZE

不是您想要的。它会为您提供每个工作项的运行时间。

操作/时间(以纳秒为单位)将为您提供GFLOPS(每秒千兆浮点操作数)。