OpenCL内存模型
众所周知,OpenCL使用设备(Device)端的内存进行计算。为了更高效地完成计算,OpenCL内存模型中划分了具有不同特性的内存,如下图所示。
其中:
- 主机内存: 用于完成CL代码端所需数据的读入和写出;
- 全局内存:所有的工作组可见,读写速度慢,可存储量大;
- 常量内存:所有工作组可见,在核函数内部只读,读取速度快,但是存储量有限制;
- 局部内存:仅在每个工作组内可见,存储量有限制;在部分厂商的设备中,此模块的访问速度较快,可用于缓存全局数据以减少数据读取时间,具体使用此内存缓存全局数据能否加速访存,建议查询设备商的开发者手册
- 私有内存:各工作项内部使用的变量,通常位于寄存器上,很快,但有限;不能使用太多,否则会导致部分数据被放置到全局内存
此外,现在的GPU大部分都有纹理内存,它的性质和全局内存类似,但主要用于存储图像数据。其最大的特点在于支持使用硬件插值,读入数据时使用纹理缓存,对图像数据的读取较全局内存快。
OpenCL中跟buffer内存相关的函数
获取内存对象的信息
//
// 获取内存对象属性信息
// param_name: 属性索引值可以是如下的值:
// 1)CL_MEM_TYPE: 获取内存对象类型
// 2)CL_MEM_FLAGS: 获取内存对象flags标记值
// 3)CL_MEM_SIZE : 获取内存对象数据大小
// 4)CL_MEM_HOST_PTR: 获取内存对象映射的CPU内存地址
// 5)CL_MEM_CONTEXT: 获取内存对象上下文
// ......
//
cl_int clGetMemObjectInfo(
cl_mem buffer, // 缓冲区对象
cl_mem_info param_name, // 属性索引
size_t param_value_size, // 属性值大小
void * param_value, // 输出属性值
size_t *param_value_size_ret); // 返回错误码
这里列出的属性只是常见的几个,在OpenCL的官方文档里面,你能查询到更多的内存属性索引值,这对于查找内存相关的错误非常有用。
内存的创建与释放
//
// 创建内存对象
// flags : 核函数操作内存的标志,可同时使用多个;
// size : 想要分配的内存大小,为 length * sizeof(ElemType);
// host_ptr: 对应的主机端内存的指针,搭配flag使用,可不填
//
cl_mem clCreateBuffer(
cl_context context, // CL上下文
cl_mem_flags flags, // 缓冲区创建标记
size_t size, // 缓冲区数据大小
void * host_ptr, // 缓冲区在CPU内存中的地址
cl_int *errcode_ref); // 返回错误码
//
// 释放内存对象
//
cl_int clReleaseMemObject(cl_mem buffer);
这里值得注意的是创建内存时标志位(flags)的选择,论坛里有一篇很不错的帖子:openCL缓存对象的传输与映射,它包含了两部分内容:
- 核函数对此内存的访问权限(并不限制主机端):
- 读写(CL_MEM_READ_WRITE,默认)
- 只写(CL_MEM_WRITE_ONLY)
- 只读(CL_MEM_READ_ONLY)
- 主机端内存与设备端内存的相互关系:
- 使用主机端已有内存初始化(CL_MEM_COPY_HOST_PTR),需要host_ptr不为空
- 将主机端内存拷贝至设备端并创建内存(CL_MEM_USE_HOST_PTR),需要host_ptr不为空,建议在具有单独显存的设备上使用
- 零拷贝内存,主机端和设备端内存直接映射(CL_MEM_ALLOC_HOST_PTR)
设备端与主机端数据的传输
//
// 通过命令队列,向缓冲区对象上传数据,这个相当于将内存中数据上传到GPU显存
// 注意:
// 1) event: 是用于异步上传数据,blocking_write必须为false,等数据上传完成后触发
// 2)上传的数据范围:将内存中 (ptr+offset) ~ (ptr+offset+cb) 这部分数据上传
//
cl_int clEnqueueWriteBuffer(
cl_command_queue command_queue, // 命令队列
cl_mem buffer, // 要上传的缓冲区对象
cl_bool blocking_write, // 是否同步上传,如果为true,当前函数阻塞到上传完
size_t offset, // 上传数据的开始偏移地址
size_t cb, // 上传数据字节数
void * ptr, // 要上传的CPU中内存数据地址
cl_uint num_events_in_wait_list, // 要等待的事件数量
const cl_event * event_wait_list, // 要等待的事件列表
cl_event *event) // 上传完成后触发的事件,当为异步上传时用到
//
// 通过命令队列,从缓冲区对象下载数据,这个相当于将 GPU显存中数据下载到CPU内存
//
cl_int clEnqueueReadBuffer(
cl_command_queue command_queue, // 命令队列
cl_mem buffer, // 要下载的缓冲区对象
cl_bool blocking_read, // 是否同步下载,如果为true,当前函数阻塞到下载完
size_t offset, // 下载数据的开始偏移地址
size_t cb, // 下载数据字节数
void * ptr, // 要下载的CPU中内存数据地址
cl_uint num_events_in_wait_list, // 要等待的事件数量
const cl_event * event_wait_list, // 要等待的事件列表
cl_event *event); // 下载完成后触发的事件,当为异步下载时用到
注意:从设备端内存下载数据时,需要先确认所有数据都已从设备端下载至主机端后,在从主机端读出数据,否则可能会导致错误。它可以通过数据同步实现,其中有两种方法:
- (1)调用所有
clEnqueueReadBuffer
语句后,在主机端处理下载的数据前,使用clFinish
函数强制等之前的所有语句都处理结束; - (2)将blocking_read选项设置为
True
,确保函数返回时数据下载已经完成。
当然,OpenCL中也有只传输部分数据的“子缓冲区”对象,用于交换部分数据,没有用过,就不班门弄斧了
调用实例
这里没有新的东西,因此,调用实例参考前面的文章即可。
调用纹理内存的image对象和内存内容拷贝
这部分内容比较重要,但是前人已经写得比较详细了,我就不赘述。入门信息可以参考,华叔的博客:OpenCL开发——(7)Image对象和采样器。本文仅额外补充以下两个部分的内容:
- OpenCL中Image对象使用时需要注意的细节
- OpenCL中的零拷贝及其注意事项
有需求的朋友欢迎跳转阅读。
参考
除文中标注内容外,还额外参考了华叔的博客:OpenCL开发——(5)内存对象 - 华叔-视觉魔术师的文章 - 知乎