OpenCL 主机端的OpenCL同步

主机端的同步主要是对命令队列中的OpenCL命令进行同步。我们在第3章中已经谈到了一些clEnqueue系的API,该系列API用于将特定的命令放入命令队列供OpenCL系统进行排队执行。默认情况下,命令队列是顺序执行命令的。若当前OpenCL环境支持无序执行命令,使用CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE属性则可以在创建命令队列时指定命令,可以不按照次序执行。

在默认情况下,虽然命令的执行已经是按照次序了,不过OpenCL的实现中往往是在发射了第一条命令之后不等它执行完,而继续发射第二条命令进行执行。所以,有一些clEnqueue系命令含有一个布尔类型参数用来指定此命令是否阻塞。如果是CL_TRUE,那么这个命令操作时不会立刻返回当前的clEnqueue的调用,而是等该操作完全执行完毕之后才会返回。这样就保证了这些命令对于其后续的命令而言是按前后次序顺序完成执行的。
OpenCL提供了clFlush函数API用于发射调用此函数之前的所有在指定命令队列中的命令。还提供了clFinish函数API用于等待调用此函数之前的所有在指定命令队列中的命令执行完成。
下面我们通过使用一个完整的代码示例来体会一下对clEnqueueWriteBuffer使用阻塞方式与非阻塞方式的效果。并且,本章后续的示例代码都将基于下面这个完整的代码加以修改。而输出结果则基于OS X 10.10.3,Intel Core i7 4650U,Intel HD Graphics5000的计算环境得出。编译工具是Apple LLVM 6.1,当然任何支持C99的编译器都能通过编译。这段代码也完全可以在Linux环境下通过编译运行。

#ifdef _APPLE_
#include 〈OpenCL/opencl.h>
#else
#include 〈CL/cl.h>
#endif
#include 〈stdio.h>
#include 〈string.h>
#include 〈stdlib.h>
#include 〈sys/time.h>
int main(void)
{
    cl_int ret;
    cl_platform_id platform_id = NULL;
    cl_device_id device_id = NULL;
    cl_context context = NULL;
    cl_command_queue command_queue = NULL;
    cl_mem src1MemObj = NULL;
    cl_mem src2MemObj = NULL;
    cl_mem dstMemObj = NULL;
    char *kernelSource = NULL;
    cl_program program = NULL;
    cl_kernel kernel = NULL;
    int *pHostBuffer = NULL;
    int *pDeviceBuffer = NULL;
    //获得OpenCL平台
    clGetPlatformIDs(1, &platform_id, NULL);
    if(platform_id == NULL)
    {
        puts("Get OpenCL platform failed!");
        goto FINISH;
    }
    //获得OpenCL计算设备,这里使用GPU类型的计算设备
    clGetDeviceIDs(platform_id, CL_DEVICE_TYPE_GPU, 1, &device_id,
                      NULL);
    if(device_id == NULL)
{
        puts("No GPU available as a compute device!");
        goto FINISH;
    }
    //根据设备ID来创建上下文
    context = clCreateContext(NULL, 1, &device_id, NULL, NULL,&ret);
    if(context == NULL)
    {
        puts("Context not established!");
        goto FINISH;
    }
    //根据上下文与设备ID来创建命令队列
    command_queue = clCreateCommandQueue(context, device_id, 0,
                                                &ret);
    if(command_queue == NULL)
    {
        puts("Command queue cannot be created!");
        goto FINISH;
    }
    /*为了能比较好地观察结果,我们分配64MB的存储空间,
将主机端的数据传递到设备端*/
const size_t contentLength = sizeof(int) * 16 * 1024 * 1024;
    src1MemObj = clCreateBuffer(context, CL_MEM_READ_ONLY,
                                      contentLength, NULL, &ret);
    if(src1MemObj == NULL)
    {
        puts("Source1 memory object failed to create!");
        goto FINISH;
    }
    src2MemObj = clCreateBuffer(context, CL_MEM_READ_ONLY,
                                      contentLength, NULL, &ret);
    if(src2MemObj == NULL)
    {
        puts("Source2 memory object failed to create!");
        goto FINISH;
    }
    //在主机端分配并初始化64MB的缓存数据
    pHostBuffer = malloc(contentLength);
    for(int i = 0; i 〈 contentLength / sizeof(int); i++)
        pHostBuffer[i] = i;
    //这里定义一对时间戳来记录clEnqueueWriteBuffer所花费的时间
    struct timeval tsBegin, tsEnd;
    long t1Duration, t2Duration;
    gettimeofday(&tsBegin, NULL);
    /*我们先使用阻塞方式传递数据来观察结果。
两个数据源都用同一个主机端的存储地址来传送数据*/
ret = clEnqueueWriteBuffer(command_queue, src1MemObj, CL_TRUE,0,
                                contentLength, pHostBuffer, 0,
                                NULL,NULL);
    if(ret != CL_SUCCESS)
    {
        puts("Data1 transfer failed");
        goto FINISH;
    }
    gettimeofday(&tsEnd, NULL);
    t1Duration = 1000000L * (tsEnd.tv_sec - tsBegin.tv_sec ) +
                  (tsEnd.tv_usec - tsBegin.tv_usec);
    //再记录第二个数据源的传输时间
    gettimeofday(&tsBegin, NULL);
    ret = clEnqueueWriteBuffer(command_queue, src2MemObj,
                                    CL_TRUE, 0,
                                    contentLength, pHostBuffer,
                                    0, NULL, NULL);
    if(ret != CL_SUCCESS)
    {
        puts("Data2 transfer failed");
        goto FINISH;
    }
    gettimeofday(&tsEnd, NULL);
    t2Duration = 1000000L * (tsEnd.tv_sec - tsBegin.tv_sec ) +
                  (tsEnd.tv_usec - tsBegin.tv_usec);
    printf("t1 duration: %ld, t2 duration: %ld", t1Duration,
        t2Duration);
FINISH:
    if(pHostBuffer != NULL)
free(pHostBuffer);
    if(pDeviceBuffer != NULL)
        free(pDeviceBuffer);
    if(kernelSource != NULL)
        free(kernelSource);
    if(src1MemObj != NULL)
        clReleaseMemObject(src1MemObj);
    if(src2MemObj != NULL)
        clReleaseMemObject(src2MemObj);
    if(dstMemObj != NULL)
        clReleaseMemObject(dstMemObj);
    if(kernel != NULL)
        clReleaseKernel(kernel);
    if(program != NULL)
        clReleaseProgram(program);
    if(command_queue != NULL)
        clReleaseCommandQueue(command_queue);
    if(context != NULL)
        clReleaseContext(context);
    puts("Program complete");
    return 0;
}

上述代码使用了阻塞方式来调用clEnqueueWriteBuffer函数API,前后两次调用所花费的时间分别为21 813微秒和26 155微秒。在调用后一个clEnqueueWriteBuffer函数之前,前一次调用已经确保将数据从主机端完整地送到了设备端的存储空间。
下面,我们将上述两个clEnqueueWriteBuffer函数调用中的第3个参数由CL_TRUE改为CL_FALSE,来观察结果。结果显示前后两次调用所花费的时间分别为140微秒和28微秒。与阻塞方式相比,非阻塞方式所花费的时间大幅减少。但是各位读者别忘了,采用非阻塞方式虽然OpenCL系统很快将执行权还给了当前线程,但是数据传输的作业未必已经完成,往往还处于进行中的状态。那么当我们采用非阻塞的模式来传输数据时如何确保该命令完全执行完成呢?直接调用clFinish函数进行同步是一个方法。还有一个方法就是我们下面介绍的,使用OpenCL的事件机制。

Camera课程

Python教程

Java教程

Web教程

数据库教程

图形图像教程

办公软件教程

Linux教程

计算机教程

大数据教程

开发工具教程