3. 使用OpenCL事件进行性能计时

{'id' 学习卡片

任务说明

本案例目标是在ARM Mali GPU上使用OpenCL事件机制测量内核执行时间。你将编写一个完整的OpenCL C主机程序,该程序创建一个简单的向量加法内核,通过cl_event记录内核执行起止时间,并输出以毫秒为单位的执行耗时。该程序不依赖任何未展示的前序代码,可直接编译运行。

实现方案

程序流程如下:获取平台与设备(优先选择GPU设备)→ 创建上下文与命令队列 → 编译内嵌的内核源码 → 创建输入/输出缓冲区并填充数据 → 执行内核并绑定事件 → 调用clWaitForEvents等待完成 → 使用clGetEventProfilingInfo读取时间戳 → 计算并打印耗时。计时精度由设备侧profiling定时器决定,Mali GPU通常支持纳秒级分辨率。

1. 平台/设备查询 2. 上下文/命令队列 3. 内核编译 4. 缓冲区 5. clEnqueueNDRange 6. clWaitForEvents 7. 读取Profiling信息 8. 计算耗时 输出示例 Kernel execution time: 0.123 ms

完整代码


#include <CL/cl.h>
#include <stdio.h>
#include <stdlib.h>

#define VEC_SIZE 4096

const char *kernel_src =
"__kernel void vec_add(__global const float *a,"
"                      __global const float *b,"
"                      __global float *c) {"
"    int i = get_global_id(0);"
"    c[i] = a[i] + b[i];"
"}";

int main(void) {
    cl_platform_id platform;
    cl_device_id device;
    cl_context context;
    cl_command_queue queue;
    cl_program program;
    cl_kernel kernel;
    cl_mem buf_a, buf_b, buf_c;
    cl_event event;
    cl_int err;

    // 1. 获取平台与GPU设备
    err = clGetPlatformIDs(1, &platform, NULL);
    err |= clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);
    if (err != CL_SUCCESS) {
        fprintf(stderr, "Failed to get GPU device\n");
        return -1;
    }

    // 2. 创建上下文与命令队列(启用profiling)
    context = clCreateContext(NULL, 1, &device, NULL, NULL, &err);
    queue = clCreateCommandQueue(context, device,
                                 CL_QUEUE_PROFILING_ENABLE, &err);

    // 3. 编译内核
    program = clCreateProgramWithSource(context, 1, &kernel_src,
                                        NULL, &err);
    err = clBuildProgram(program, 1, &device, NULL, NULL, NULL);
    if (err != CL_SUCCESS) {
        fprintf(stderr, "Kernel build failed\n");
        return -1;
    }
    kernel = clCreateKernel(program, "vec_add", &err);

    // 4. 创建缓冲区并填充输入数据
    buf_a = clCreateBuffer(context, CL_MEM_READ_ONLY,
                           VEC_SIZE * sizeof(float), NULL, &err);
    buf_b = clCreateBuffer(context, CL_MEM_READ_ONLY,
                           VEC_SIZE * sizeof(float), NULL, &err);
    buf_c = clCreateBuffer(context, CL_MEM_WRITE_ONLY,
                           VEC_SIZE * sizeof(float), NULL, &err);

    float *a = malloc(VEC_SIZE * sizeof(float));
    float *b = malloc(VEC_SIZE * sizeof(float));
    for (int i = 0; i < VEC_SIZE; i++) {
        a[i] = (float)i;
        b[i] = (float)(i * 2);
    }
    clEnqueueWriteBuffer(queue, buf_a, CL_TRUE, 0,
                         VEC_SIZE * sizeof(float), a, 0, NULL, NULL);
    clEnqueueWriteBuffer(queue, buf_b, CL_TRUE, 0,
                         VEC_SIZE * sizeof(float), b, 0, NULL, NULL);

    // 5. 设置内核参数并执行
    clSetKernelArg(kernel, 0, sizeof(cl_mem), &buf_a);
    clSetKernelArg(kernel, 1, sizeof(cl_mem), &buf_b);
    clSetKernelArg(kernel, 2, sizeof(cl_mem), &buf_c);

    size_t global = VEC_SIZE;
    err = clEnqueueNDRangeKernel(queue, kernel, 1, NULL,
                                 &global, NULL, 0, NULL, &event);
    if (err != CL_SUCCESS) {
        fprintf(stderr, "Kernel enqueue failed\n");
        return -1;
    }

    // 6. 等待内核执行完成
    clWaitForEvents(1, &event);

    // 7. 读取profiling时间戳
    cl_ulong start, end;
    clGetEventProfilingInfo(event, CL_PROFILING_COMMAND_START,
                            sizeof(cl_ulong), &start, NULL);
    clGetEventProfilingInfo(event, CL_PROFILING_COMMAND_END,
                            sizeof(cl_ulong), &end, NULL);

    // 8. 计算并输出耗时(纳秒转毫秒)
    double ms = (double)(end - start) / 1e6;
    printf("Kernel execution time: %.3f ms\n", ms);

    // 清理资源
    clReleaseEvent(event);
    clReleaseMemObject(buf_a);
    clReleaseMemObject(buf_b);
    clReleaseMemObject(buf_c);
    clReleaseKernel(kernel);
    clReleaseProgram(program);
    clReleaseCommandQueue(queue);
    clReleaseContext(context);
    free(a);
    free(b);
    return 0;
}

编译与运行

在Mali GPU开发板上使用OpenCL ICD驱动编译运行:


gcc -o event_timer event_timer.c -lOpenCL
./event_timer

若开发环境使用交叉编译器,请链接对应的OpenCL库。运行前确认设备支持CL_QUEUE_PROFILING_ENABLE(Mali GPU均支持)。

验证方法

程序执行后观察终端输出。若输出形如Kernel execution time: 0.123 ms的正数耗时,且程序退出码为0,则计时功能正常。可通过修改VEC_SIZE(如增大至65536)观察耗时随之增加,以验证计时确实反映内核执行时间。若输出Kernel build failed或Failed to get GPU device,请检查OpenCL驱动安装与设备权限。