OpenCL中提供了对存储器对象操作的API,可以实现主机向设备、设备向主机、设备间数据传输和存储器对象初始化等操作。现在我们就来详细讲述这些API函数的作用及其使用方法。
主机与设备间数据传输
在内核执行前,可以使用主机端的数据来初始化分配好的存储器对象中的数值;在内核计算完成后,可以用设备端存储器对象中的数据来更新主机端的数值。这两个操作涉及数据从主机向设备、设备向主机间数据拷贝。针对缓冲区对象和图像对象,OpenCL都提供了相应的API函数来实现这些功能。
缓冲区对象主机与设备间数据传输
使用如下函数实现在主机与设备间对缓冲区对象的数据拷贝:
cl_int clEnqueueReadBuffer (cl_command_queue command_queue,
cl_mem buffer,
cl_bool blocking_read,
size_t offset,
size_t size,
void *ptr,
cl_uint num_events_in_wait_list,
const cl_event *event_wait_list,
cl_event *event)
cl_int clEnqueueWriteBuffer (cl_command_queue command_queue,
cl_mem buffer,
cl_bool blocking_write,
size_t offset,
size_t size,
const void *ptr,
cl_uint num_events_in_wait_list,
const cl_event *event_wait_list,
cl_event *event)
cl_int clEnqueueReadBufferRect (cl_command_queue command_queue,
cl_mem buffer,
cl_bool blocking_read,
const size_t *buffer_origin,
const size_t *host_origin,
const size_t *region,
size_t buffer_row_pitch,
size_t buffer_slice_pitch,
size_t host_row_pitch,
size_t host_slice_pitch,
void *ptr,
cl_uint num_events_in_wait_list,
const cl_event *event_wait_list,
cl_event *event)
cl_int clEnqueueWriteBufferRect (cl_command_queue command_queue,
cl_mem buffer,
cl_bool blocking_write,
const size_t *buffer_origin,
const size_t *host_origin,
const size_t *region,
size_t buffer_row_pitch,
size_t buffer_slice_pitch,
size_t host_row_pitch,
size_t host_slice_pitch,
const void *ptr,
cl_uint num_events_in_wait_list,
const cl_event *event_wait_list,
cl_event *event)
- 参数command_queue为一个有效的主机命令队列,读/写命令将会入队到这个命令队列中。command_queue和buffer必须由同一个OpenCL上下文创建。
-
参数buffer为一个有效的缓冲区对象。
-
参数blocking_read和blocking_write表明读写操作是阻塞还是非阻塞的。如果blocking_read为CL_TRUE,即读命令是阻塞的,只有将buffer中的数据完全拷贝到ptr所指内存中后,函数才会返回;如果blocking_write为CL_FALSE,即读命令是非阻塞的,函数将此命令队列入队后就会返回。参数event会返回一个事件对象,用来查询读命令的执行状态。只有等到读命令执行完,才能继续使用ptr所指向的内容。如果blocking_write为CL_TRUE,OpenCL实现会阻塞拷贝ptr所指向的数据,并将这个写命令入队。当函数返回后,应用就能继续使用ptr所指向的内存了;如果blocking_write为CL_FASLE,OpenCL实现会用ptr实现非阻塞的写操作,函数会立即返回。在函数返回后,应用还不能立刻就使用ptr所指内存。参数event会返回一个事件对象,可以用来查询写命令的执行情况。在写命令完成后,应用就可以重新使用ptr所指向的内存了。
-
参数offset是读写区域在缓冲区对象中的偏移量,单位为字节。
-
参数size是要读写数据的大小,单位为字节。
-
参数ptr指向主机中的一块内存,用作读命令的数据源及写命令的目的地。
-
参数event_wait_list和num_events_in_wait_list中列出了执行此命令前要等待的事件。如果event_wait_list是NULL,则无须等待任何事件。并且num_events_in_wait_list必须为0。如果event_wait_list不是NULL,则其中所有事件都必须是有效的,并且num_event_in_wait_list必须大于0。event_wait_list中的事件充当同步点,并且必须与command_queue位于同一个上下文中。
event会返回一个事件对象,用来标识读/写命令,可以用来查询或等待此命令完成。如果event是NULL,就无法查询此命令的状态或等待其完成了。 -
参数buffer_origin定义了要读写的内存区域在缓冲区中的偏移量(x,y,z)。对于2维矩形区域,z的值即buffer_origin[2]应该为0。偏移量的计算为:buffer_origin[2]buffer_slice_pitch+buffer_origin[1]buffer_row_pitch+buffer_origin[0]。
-
参数host_origin定义了要读写的内存区域在ptr中的偏移量(x,y,z)。对于2维矩形区域,z的值即host_origin[2]应该为0。偏移量的计算为:host_origin[2]host_slice_pitch+host_origin[1]host_row_pitch+host_origin[0]。
-
参数region定义了要读写的2维或3维区域:(width,height,depth),单位分别是字节、行、切片(slice)。对于2维区域,depth的值即retgion[2]应该是1。
-
参数buffer_row_pitch是缓冲中每一行数据的字节数。如果buffer_row_pitch为0,则在计算偏移量时用region[0]替代。
-
参数buffer_slice_pitch是ptr中每一行数据的字节数。如果host_row_pitch为0,则在计算偏移量时用region[0]替代。
对于上述参数,并未按照函数参数先后顺序列出,根据参数名来对应。
函数clEnqueueReadBuffer实现了主机从缓冲区对象中读取数据。函数clEnqueue-WriteBuffer实现了主机向缓冲区对象写入数据。函数clEnqueueReadBufferRect实现了主机从缓冲区对象中读取一个2维或者3维矩形区域数据。函数clEnqueueWriteBufferRect实现了主机向缓冲区对象写入一个2维或者3维矩形区域数据。
如下例子展示了函数clEnqueueWriteBuffer和clEnqueueReadBuffer的用法:
……
int data_in[20] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19};
int data_out[40];
cl_mem buffer = clCreateBuffer(context, CL_MEM_READ_WRITE,
sizeof(int) * 40, NULL, &err);
clEnqueueWriteBuffer(cmdqueue, buffer, CL_TRUE, sizeof(int) * 13,
sizeof(int) * 20,
data_in, 0, NULL, NULL);
clEnqueueReadBuffer(cmdqueue, buffer, CL_TRUE, 0,
sizeof(int) * 40, data_out, 0, NULL, NULL);
……
对上述代码,可用图5-6演示。在clEnqueueWriteBuffer中设置了偏移量sizeof(int)*13,所以buffer中的数值在缓冲区对象中从第13个位置开始存放,缓冲区对象前13个数值结果未知。读者可以自行验证data_out的输出结果。
如下例子展示如何从一个缓冲区读取一个4×4的区域,并将它读入到主机内存:
……
int data_in[80];
int data_out[25];
for(int i = 0; i 〈 80; i++)
{
data_in[i] = i;
}
cl_mem buffer = clCreateBuffer(context, CL_MEM_READ_WRITE |
CL_MEM_COPY_HOST_PTR,
sizeof(int) * 80, data_in, &err);
const size_t buffer_origin[3] = {5 * sizeof(int), 3, 0};
const size_t host_origin[3] = {1 * sizeof(int), 1, 0};
const size_t origin[3] = {4 * sizeof(int), 4, 1};
clEnqueueReadBufferRect(cmdqueue, buffer, CL_TRUE, buffer_origin,
host_origin,
origin, 10 * sizeof(int), 0,
4 * sizeof(int), 0, data_out, 0,
NULL, NULL);
……
对于上述代码,可以用图5-7演示。clEnqueueReadBufferRect函数中的7个参数10\*sizeof (int)
使得buffer中的80个数值以10个为一行,一共有8行数据。第9个参数4\*sizeof(int)
使得out_out中的16个数值以4为一行,一共有4行。buffer_origin[3]={5\*sizeof(int),3,0}
表示矩形数据开始位置在缓冲区中第5列第3行开始的位置。host_origin[3]={0,0,0}
表示拷贝后的数据从5×5的区域中第1列第1行的位置开始存放。origin[3]={4\*sizeof(int),4,1}
表示每行拷贝4个数值,拷贝4行,一共16个数据。读者可以自行验证data_out的输出结果。
从上图可以看出,主机与设备间矩形数据拷贝对拷贝多维数据十分有用。对于我们常用的1维数据,可以在软件层面划分为2维数据,并根据需求拷贝,减少了不必要的数据拷贝。