第6章 OpenCL主机端内存模型 - 6.1 内存对象

优质
小牛编辑
123浏览
2023-12-01

OpenCL定义了三种内存对象——数组,图像和管道——这几种内存对象可以通过主机端的API进行创建。数组和图像内存对象上存储的数据,可以在主机端和设备端进行随机访问。管道对象上的数据对象只能在内核端先进先出(FIFO),并且主机端无法访问这些数据。

数组对象可以看做为CPU上的一维数组,并且其分配过程与C的malloc()函数类似。数组对象可以包含任何标量数据,向量数据或自定义结构体。数据在数组中是顺序存储的,这样OpenCL内核就能以随机访问的方式对数组进行访问(就如同C中的一维数组)。

图像对象就有些不同,其数据的布局或存放方式在硬件上进行过一些优化,这样指针就很难直接一个一个的访问对应的数据,并且硬件上的数据布局方式对于开发者来说是不可见的。这样,内核端只能使用内置函数对图像对象进行访问。因为GPU设计之初就是为了处理图形任务,所以GPU对图像数据访问效率已有较高优化。图像有三个优势:

  1. GPU上的层级缓存和数据流结构就是为了优化访问图像类型数组所准备
  2. GPU驱动会在硬件层面上优化图像数据的排布,从而提升访问图像数据的效率,尤其是二维图像模式
  3. 硬件支持图像是一个很复杂的数据访问过程,在这个过程中硬件会将一些存储的数据进行压缩

下面的几个子节,将分别对这三种内存对象进行更为详尽的描述。

6.1.1 数组对象

数组对象的分配有点类似使用malloc(),其分配方式非常简单。创建数组对象只需要提供上下文对象,数组大小,以及一些标识就可以。创建数组对象的API为clCreateBuffer()

  1. cl_mem
  2. clCreateBuffer(
  3. cl_context context,
  4. cl_mem_flags flags,
  5. size_t size,
  6. void *host_ptr,
  7. cl_int *err)

函数会返回一个数组对象,如果需要将错误码传出,则需要传入最后一个参数。flags参数可以将数组配置成只读或只写的数据,以及设置其他分配选项。例如,下面的代码,我们就创建了一个只读的数组对象,其存储的数据与主机端a数组数分布相同,二者也具有同样的大小。这里,我们将详细讨论一些分配选项在之后的章节(例如:CL_MEM_USE_HOST_PTR)。错误码将从err传出,对应的错误码在OpenCL标准文档中都有定义。通常OpenCL函数执行成功,都会以CL_SUCCESS作为错误码返回。

{%ace edit=false, lang=’c_cpp’%}
cl_int err;
int a[16];

cl_mem newBuffer = clCreateBuffer(
context,
CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR,
16 * sizeof(int),
a,
&err);

if (err != CL_SUCCESS){
// Handle error as necessary
}
{%endace%}

OpenCL也支持子数组对象(subbuffer),也就是可以将单独的数组对象再进行划分为更小的数组对象,这些数组对象可以相互覆盖,可以读或写和拷贝,以及和其父数组以相同的方式使用。注意有覆盖和包含关系的子数组对象,会让其父数组对象结构变的更加复杂,并且在实际使用过程中这种情况会造成一些未定义的行为。

6.1.2 图像对象

OpenCL中,图像与数组对象有三点不同:

  1. 图像数据排布的不透明性,使其不能直接在内核中使用指针进行数据读取
  2. 多维结构
  3. 图像对数据成员有一定的要求,并不能像数组那让接受任意数据类型

图像对象之所以在OpenCL中存在,因为GPU硬件设计已经对图像的存储方式进行过优化,这样会让设备在访问图像数据时的效率更高。访问图像的内置函数,并不能像数组对象那样提供各种方式的访存模式,不过其能作用就是能让一些滤波方式在硬件方面得到很好的支持,从而具有较高的效率。滤波操作中,会有基于一组以特定方式排布的像素进行变换,高效的内存访问则会对滤波操作进行加速。这些操作需要多次读取的长指令队列,不过有硬件的支持其执行效率很高。

图像数据可在内核端,通过特定的函数进行访问(第7章设备端内存模型会详细讨论)。主机端访问图像对象的方式与数组对象没有太大的区别,不过对图像对象操作的主机函数支持多维度的寻址。clEnqueueReadImage()更像clEnqueueReadBufferRect(),而非clEnqueuReadBuffer()

图像对象和数组对象最大的区别,就是图像对象支持的格式。图像格式包括通道序和通道类型。通道顺序定义了有多少通道需要使用——例如,CL_RGB,CL_R或CL_ARGB。通道类型就是要选择通道内数据存储的格式,从CL_FLOAT到充分利用内存的CL_UNORM_SHORT_565(其将一个16比特字打包后放入内存)。当内核代码要访问这些数据时,读取到的数据结果都上转换成标准的OpenCL C类型数据。图像格式支持的列表可以通过clGetSupportedImageFormats()获取。

图像对象可通过clCreateImage()进行创建,其声明如下:

  1. cl_mem
  2. clCreateImage(
  3. cl_context context,
  4. cl_mem_flags flags,
  5. const cl_image_format *image_format,
  6. const cl_image_desc *image_desc,
  7. void *host_ptr,
  8. cl_int *errcode_ret)

context,flags和host_ptr这些与创建数组对象所需要的参数一致。图像类型(image_format)和图像描述符(image_desc)参数定义了图像的维度,数据格式和数据分布。这种结构已经在第4章进行详细的描述(一个初始化图像对象的完整例子)。

6.1.3 管道对象

OpenCL 2.0支持一种新的内存对象——管道对象。管道对象的数据结构是FIFO结构,其用来将一个内核对象的数据传递给另一个内核对象。因为之前OpenCL标准中的内存模型十分松散,所以根本无法实现管道,因为没有办法在上一个内核结束前确定内存的状态。通道的意义就是为了在两个内核中共享一部分数据,并且保证这些共享数据的状态。这种标识对于处理器来说是一种趋势,对于支持管道对象的任意设备,至少有能力实现内核间共享数据的原子操作,并且必须要有一套内存一致模型来支持内存获取和释放语义。

可以设想一下,如果设备具有这样的能力,编程者都可以用一个数组对象来实现属于自己的“管道对象”。OpenCL 2.0的内存模型中这个方案是可行的,其背后是很多设计师和工程师的努力。管道具有并发“生产者-消费者”机制,这种方式比起之前的标准,让很多问题变的简单了许多(例如,当每个工作项生成了大量的输出数据,就可以对这些数据进行打包处理)。在同一设备上执行“生产者-消费者”任务时,就可以使用管道对象,这样也允许硬件供应商能将这块内存映射到一个延迟较低的内存区域中。管道对象是不允许主机端对其进行读写的,所以访问管道对象属于设备端内存模型。

管道数据通常称为(packets),其包含了OpenCL C或用户自定义的类型。创建管道对象的API为clCreatePipe(),其声明如下:

  1. cl_mem
  2. clCreatePipe(
  3. cl_context context,
  4. cl_mem_flags flags,
  5. cl_uint pipe_packet_size,
  6. cl_uint pipe_max_packets,
  7. const cl_pipe_properties *properties,
  8. cl_int *errcode_ret)

当创建一个管道,需要提供包大小(pipe_packed_size)和最大包数(pipe_max_packets)。如同其他创建内存对象的API,这个API也需要设置一些与内存相关标识。对于管道对象来说,只有设置CL_MEM_READ_WRITE标识是合法的,其也是管道对象默认的标识参数。以后,管道对象不可在主机端访问,即便是编程者没有意识到,也需要使用CL_MEM_HOST_NO_ACCESS来显式表明管道对象在主机端不可访问。