[OpenCL] 主机编程:数据传输和分区(11)

3.4 获取有关缓冲区对象的信息

  clGetImageInfo 仅提供有关图像对象的信息,而您可以使用 clGetMemObjectInfo 获取有关图像对象和缓冲区对象的信息。 其签名如下:

clGetMemObjectInfo (cl_mem object, cl_mem_info param_name,
	size_t param_value_size, void *param_value,
	size_t *param_value_size_ret)

  这些论点是直截了当的。 前三个提供输入:内存对象、标识您请求的数据类型的名称以及您请求的数据量。 最后两个参数是输出参数,其中函数返回您请求的数据和返回数据的大小。

表 3.4 列出了可以使用 clGetMemObjectInfo 访问的不同类型的信息。
在这里插入图片描述
  当您想要检查内存对象数据的大小和位置时,这些参数变得特别有用。 下面的代码展示了它是如何工作的。 它创建一个包含 100 个浮点值的缓冲区和一个包含这些浮点数的 20 个元素子集的子缓冲区。 然后它调用 clGetMemObjectInfo 来检查两个缓冲区对象。

注意以下代码使用子缓冲区,因此只能在支持 OpenCL 1.1 标准的系统上编译。

清单 3.1 缓冲区和子缓冲区:buffer_check.c

...
float main_data[100];
cl_mem main_buffer, sub_buffer;
void * main_buffer_mem = NULL, * sub_buffer_mem = NULL;
size_t main_buffer_size, sub_buffer_size;
cl_buffer_region region;
...
// 创建包含 100 个值的缓冲区
main_buffer = clCreateBuffer(context,
  CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
  sizeof(main_data), main_data, & err);
if (err < 0) {
  perror("Couldn't create a buffer");
  exit(1);
}
region.origin = 30 * sizeof(float);
region.size = 20 * sizeof(float);
// 创建具有 20 个值的子缓冲区
sub_buffer = clCreateSubBuffer(main_buffer,
  CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
  CL_BUFFER_CREATE_TYPE_REGION, &
  region, & err);
if (err < 0) {
  perror("Couldn't create a sub-buffer");
  exit(1);
}
// 获取尺寸信息
clGetMemObjectInfo(main_buffer, CL_MEM_SIZE,
  sizeof(main_buffer_size), & main_buffer_size, NULL);
clGetMemObjectInfo(sub_buffer, CL_MEM_SIZE,
  sizeof(sub_buffer_size), & sub_buffer_size, NULL);
printf("Main buffer size: %lu\n", main_buffer_size);
printf("Sub-buffer size: %lu\n", sub_buffer_size);
// 获取主机指针
clGetMemObjectInfo(main_buffer, CL_MEM_HOST_PTR,
  sizeof(main_buffer_mem), & main_buffer_mem, NULL);
clGetMemObjectInfo(sub_buffer, CL_MEM_HOST_PTR,
  sizeof(sub_buffer_mem), & sub_buffer_mem, NULL);
printf("Main buffer memory address: %p\n", main_buffer_mem);
printf("Sub-buffer memory address: %p\n", sub_buffer_mem);
printf("Main array address: %p\n", main_data);
// 释放缓冲区对象
clReleaseMemObject(main_buffer);
clReleaseMemObject(sub_buffer);
...

在我的系统上,打印结果如下:

Main buffer size: 400
Sub-buffer size: 80
Main buffer data address: 0x972000
Sub-buffer data address: 0x972078
Main array address: 0x7ff60805920

  子缓冲区不分配自己的内存区域来保存数据。 相反,它访问主缓冲区使用的同一内存区域。 因为 createBuffer 是使用 CL_MEM_COPY_HOST_PTR 标志调用的,所以两个缓冲区都不会访问原始浮点数组中的数据。

  此时,您应该对如何创建和检查内存对象(无论它们是图像对象、缓冲区对象还是子缓冲区对象)有一个扎实的了解。 您可以通过将这些对象作为kernel函数的参数将它们发送到设备,但是还有其他方法可以传输这些数据。 下一节介绍将内存对象数据从主机传送到设备、从设备到主机以及在设备之间传送的命令。

3.5 内存对象传输命令

  让我们回顾一下命令队列的主题。 当主机想要访问设备时,它会创建一个命令队列。 主机通过向队列分派命令与设备进行通信。 我们将这种将命令放入命令队列的过程称为入队。

  到目前为止,我们只关注告诉设备执行 kernel 的命令。 然而,kernel 执行只是一种类型的命令。 其他命令告诉设备如何以及在何处传输数据,本节将详细研究这些命令。

  OpenCL 提供了许多将数据传输命令排入队列的函数,它们的名称都采用 clEnqueueXX 的形式。 为了方便起见,我们将它们分为三类:启动读/写数据传输的函数,映射和取消映射内存的函数,以及在内存对象之间复制数据的函数。

注意 这些函数不会创建新的内存对象。 它们访问已作为内核参数传输到设备的内存对象中的数据

3.5.1 读/写数据传输

  至此,您知道如何使用 clSetKernelArg 将内存对象发送到设备。 但是,假设您创建了一个只写缓冲区对象来保存设备的输出。 kernel 完成处理后,如何将缓冲区数据返回给主机?

  要将缓冲区对象从设备读取到主机,最简单的函数是 clEnqueueReadBuffer。 这是读写内存对象的六个函数之一。 表 3.5 列出了它们,包括它们的论点和目的。 它们都返回一个整数错误代码。
在这里插入图片描述
  这些函数中的每一个还包含一个称为阻塞的布尔参数。 如果设置为 CL_TRUE,则函数在读/写操作完成之前不会返回。 如果阻塞设置为 CL_FALSE,该函数会将读/写命令排入队列,但不会等待数据传输完成。

在这里插入图片描述
  许多剩余的参数指定应该访问内存对象的哪一部分。 缓冲区读/写函数中的偏移量参数标识要读取或写入的缓冲区数据的开始。 data_size 参数标识应传输从偏移量开始的数据量。 图 3.3 显示了两个主要的读/写函数是如何工作的。

  clEnqueueReadImage 和 clEnqueueWriteImage 函数都接受两个乍一看可能没有意义的参数:origin[3] 和 region[3]。 这些数组指定要传入或传出图像对象的图像数据的矩形区域。 origin 标识要访问的第一个像素的位置,它的三个 size_t 元素分别标识像素的列、行和切片。 region 参数还包含三个 size_t 元素,它们标识要读取或写入的图像数据的尺寸(宽度、高度和深度)。 如果图像对象是二维的,则原点的最后一个元素必须为 0,区域的最后一个元素必须为 1。图 3.4 显示了图像读/写函数的操作方式。
在这里插入图片描述
  表 3.5 中的最后两个函数将数据传入和传出缓冲区对象,但它们访问矩形区域中的数据,类似于用于传输图像数据的区域。 与图像读/写函数一样,区域标识要传输的矩形的尺寸。 buffer_origin[3] 设置缓冲区对象数据的开始,host_origin[3] 设置主机内存中数据的开始。 您必须为主机和缓冲区对象指定行间距和切片间距,但这些参数可以设置为 0。

  当您想要传输与图像无关的多维数据时,clEnqueueReadBufferRect 和 clEnqueueWriteBufferRect 函数很有用。 例如,假设您在缓冲区对象中存储了一个矩阵,并且您想将一个子矩阵读入主机内存。 在这种情况下,clEnqueueReadBufferRect 是要使用的函数,图 3.5 显示了它是如何工作的。

在这里插入图片描述
  在这里,region 设置子矩阵的大小:[4, 4, 1]。 host_origin 等于 [1, 1, 0] 并且 buffer_origin 等于 [5, 3, 0]。 下面的清单显示了这种矩形数据传输是如何在代码中完成的。

注意 clEnqueueReadBufferRect 和 clEnqueueWriteBufferRect 仅在支持 OpenCL 1.1 标准的平台上可用。 在撰写本文时,Mac OS 仅支持 OpenCL 1.0,因此此代码无法在 Mac OS 系统上正常运行。

清单 3.2 读取矩形缓冲区数据:buffer_test.c

...
float full_matrix[80], zero_matrix[80];
const size_t buffer_origin[3] = {
  5 * sizeof(float),
  3,
  0
};
const size_t host_origin[3] = {
  1 * sizeof(float),
  1,
  0
};
const size_t region[3] = {
  4 * sizeof(float),
  4,
  1
};
cl_mem matrix_buffer;
...
// 创建和初始化缓冲区
matrix_buffer = clCreateBuffer(context,
  CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR,
  sizeof(full_matrix), full_matrix, & err);
if (err < 0) {
  perror("Couldn't create a buffer object");
  exit(1);
}
// 将缓冲区设置为内核参数
err = clSetKernelArg(kernel, 0,
  sizeof(cl_mem), & matrix_buffer);
if (err < 0) {
  perror("Couldn't set the buffer as the kernel argument");
  exit(1);
}
// 入队内核命令
err = clEnqueueTask(queue, kernel, 0, NULL, NULL);
if (err < 0) {
  perror("Couldn't enqueue the kernel");
  exit(1);
}
// 将数据写入缓冲区
err = clEnqueueWriteBuffer(queue, matrix_buffer,
  CL_TRUE, 0, sizeof(full_matrix), full_matrix,
  0, NULL, NULL);
if (err < 0) {
  perror("Couldn't write to the buffer object");
  exit(1);
}
// 从缓冲区读取矩形
err = clEnqueueReadBufferRect(queue, matrix_buffer,
  CL_TRUE, buffer_origin, host_origin, region,
  10 * sizeof(float), 0, 10 * sizeof(float), 0,
  zero_matrix, 0, NULL, NULL);
if (err < 0) {
  perror("Couldn't read the rectangle from the buffer object");
  exit(1);
}
...

  此应用程序从零缓冲区创建 kernel 参数,写入缓冲区,然后在指定偏移处从缓冲区读取 4x4 矩形。 这会将三个命令分派到命令队列。 第一个告诉设备执行 kernel ,第二个将主机数据传输到缓冲区对象,第三个从缓冲区对象读取一个矩形内存区域到主机内存中。

3.5.2 映射内存对象

  当常规 C/C++ 应用程序需要访问文件时,通常会将文件的内容放在进程内存中并使用内存操作读取或修改它。 这称为内存映射或仅映射文件。 这通常比常规文件 I/O 提供更高的性能。 对我来说,它也更简单,因为我使用与内存相关的函数比与文件相关的函数更频繁。

  OpenCL 提供了类似的机制来访问内存对象。 您可以将设备上的内存对象映射到主机上的内存区域,而不是使用前面介绍的读/写操作。 建立此映射后,您可以使用指针或其他内存操作读取或修改主机上的内存对象。

  表 3.6 列出了将命令排入队列以映射和取消映射内存对象的函数。 请注意,您不必映射整个内存对象。 对于缓冲区对象,您可以访问任何一维区域。 对于图像对象,您可以访问一个矩形区域。

在这里插入图片描述
  这些函数中的大多数参数类似于用于读取和写入的参数,但一个显着的区别是前两个函数返回一个 void 指针。 这个指针有两个用途:它标识主机上映射内存的开始,它标识映射以便 clEnqueueUnmapMemObject 知道要取消映射哪个区域。

  map/unmap 函数和读/写函数之间的第二个区别是 clEnqueueMapBuffer 和 clEnqueueMapImage 中使用的 map_flags 参数。 这配置了主机上映射内存的可访问性。 如果 map_flags 设置为 CL_MAP_READ,则映射的内存将是只读的。 如果该标志设置为 CL_MAP_WRITE,则映射的内存将是只写的。 如果使用 CL_MAP_READ|CL_MAP_WRITE,则内存将是可读写的。

  在 OpenCL 中处理内存映射数据通常是一个三步过程。 首先,使用 clEnqueueMapBuffer 或 clEnqueueMapImage 将内存映射操作入队。 然后使用 memcpy 之类的函数将数据传入和传出映射内存。 最后,通过调用 clEnqueueUnmapMemObject 取消映射区域。

  根据我的经验,与常规读/写操作相比,内存映射显着提高了性能。 第 7 章解释了时序和分析,将向您展示如何自己测试它。

3.5.3 在内存对象之间复制数据

  到目前为止,我们看到的每个数据传输操作都集中在主机内存和内存对象之间移动数据。 但是 OpenCL 提供了在内存对象之间传输数据的附加功能。 使用这些函数,您可以在设备上的两个内存对象之间或不同设备上的内存对象之间复制数据。 表 3.7 列出了每个数据复制函数及其参数。
在这里插入图片描述
  前两个函数将在相似内存对象类型之间复制数据的命令排入队列:缓冲区对象到缓冲区对象、图像对象到图像对象。 下一个函数将在不同类型的内存对象之间复制数据的命令排入队列:缓冲区对象到图像对象和图像对象到缓冲区对象。 如果你关注了读/写函数的讨论,这些函数不会有任何困难。

  让我们看一个演示如何映射和复制内存对象的示例。 图 3.6 显示了该计划。 目标是创建两个缓冲区对象,并使用 clEnqueueCopyBuffer 将 Buffer 1 的内容复制到 Buffer 2。 然后 clEnqueueMapBuffer 将 Buffer 2 的内容映射到主机内存, memcpy 将映射的内存传输到一个数组。

在这里插入图片描述

  以下列表显示了如何在代码中实现。 map_copy应用程序浏览四个命令。 首先将内核传输及其参数传输到设备。 第二份将一个缓冲对象复制到下一个缓冲区对象。 第三个命令配置内存映射,第四个取消映射内存。

清单 3.3 复制和映射缓冲区对象:map_copy.c

...
float data_one[100], data_two[100], result_array[100];
cl_mem buffer_one, buffer_two;
void * mapped_memory;
...
// 创建缓冲区对象
buffer_one = clCreateBuffer(context,
    CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR,
    sizeof(data_one), data_one, & err);
if (err < 0) {
    perror("Couldn't create a buffer object");
    exit(1);
}
buffer_two = clCreateBuffer(context,
    CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR,
    sizeof(data_two), data_two, & err);
// 设置内核参数
err = clSetKernelArg(kernel, 0, sizeof(cl_mem), &
    buffer_one);
err |= clSetKernelArg(kernel, 1, sizeof(cl_mem), &
    buffer_two);
if (err < 0) {
    perror("Couldn't set the buffer as the kernel argument");
    exit(1);
}
queue = clCreateCommandQueue(context, device, 0, & err);
if (err < 0) {
    perror("Couldn't create a command queue");
    exit(1);
};
// 入队内核命令
err = clEnqueueTask(queue, kernel, 0, NULL, NULL);
if (err < 0) {
    perror("Couldn't enqueue the kernel");
    exit(1);
}
// 入队命令以复制缓冲区
err = clEnqueueCopyBuffer(queue, buffer_one,
    buffer_two, 0, 0, sizeof(data_one),
    0, NULL, NULL);
if (err < 0) {
    perror("Couldn't perform the buffer copy");
    exit(1);
}
// 将缓冲区对象映射到主机内存
mapped_memory = clEnqueueMapBuffer(queue,
    buffer_two, CL_TRUE, CL_MAP_READ, 0,
    sizeof(data_two), 0, NULL, NULL, & err);
if (err < 0) {
    perror("Couldn't map the buffer to host memory");
    exit(1);
}
// 复制主机内存
memcpy(result_array, mapped_memory, sizeof(data_two));
// 取消映射缓冲区对象
err = clEnqueueUnmapMemObject(queue, buffer_two,
    mapped_memory, 0, NULL, NULL);
if (err < 0) {
    perror("Couldn't unmap the buffer");
    exit(1);
}
...

  此代码不应呈现任何惊喜。 只要命令队列配置为按顺序处理命令(默认配置),它将将Buffer_One的内容传输到Buffer_TWO并将Buffer_TWO映射到托管内存。

  此时,您应该彻底了解内存对象和可用于传输数据的不同方法。 在下一节中,我们将继续探索数据,但这一次,我们将检查如何在单个设备中分发数据和计算任务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值