OpenCL C 内存对象
内存对象是 OpenCL 运行时系统分配和管理的对象,用于在主机(Host) 和设备(Device) 之间传递数据。所有需要在内核中使用的数据(除了简单的标量)都必须封装在内存对象中。
1. 内存对象的类型
OpenCL 主要定义了两种类型的内存对象:
内存对象类型 | 描述 | 主要用途 | 内核中参数类型 |
---|---|---|---|
Buffer (缓冲区) | 一个通用的一维数据块,类似于 C 中的 malloc 分配的内存。 | 存储数组、结构体、数值等任何线性数据。 | __global <type>* , __constant <type>* , __local <type>* |
Image (图像) | 一个专门为图形处理优化的不透明对象,通常是 2D 或 3D。 | 存储纹理、帧缓冲区等图像数据。支持硬件滤波和特定的寻址模式。 | image2d_t , image3d_t , sampler_t |
核心区别:
Buffer 是简单的字节数组,你可以直接通过指针索引访问任何元素。
Image 的内部布局是不透明的(例如,为了优化可能采用瓦片式存储),你不能直接通过指针访问。必须使用专门的内置函数(
read_imagef
,write_imagef
)来读写,并由硬件自动处理格式转换、滤波和边界处理。
2. 内存对象的创建
内存对象由主机端的 API 调用创建。
a) 创建 Buffer 对象
c
// clCreateBuffer 函数原型
cl_mem clCreateBuffer(cl_context context, // OpenCL 上下文cl_mem_flags flags, // 内存标志,指定读写权限和分配方式size_t size, // 缓冲区的大小(字节)void *host_ptr, // 主机端指针,可用于初始化cl_int *errcode_ret // 返回错误码
);
重要的 cl_mem_flags
:
CL_MEM_READ_ONLY
: Buffer 在内核中只读。CL_MEM_WRITE_ONLY
: Buffer 在内核中只写。CL_MEM_READ_WRITE
: Buffer 在内核中可读写。CL_MEM_USE_HOST_PTR
: 使用host_ptr
指向的内存作为存储。OpenCL 可能会直接使用这块内存,也可能将其内容拷贝到设备优化过的内存中。CL_MEM_COPY_HOST_PTR
: 分配新的设备内存,并将host_ptr
指向的数据拷贝过去。CL_MEM_READ_ONLY
常与此合用。CL_MEM_ALLOC_HOST_PTR
: 申请一块可以被映射(map) 到主机地址空间的内存(“零拷贝”内存的一种)。
标志组合示例:
c
// 典型的输入缓冲区(只读,复制主机数据)
cl_mem_flags input_flags = CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR;// 典型的输出缓冲区(只写)
cl_mem_flags output_flags = CL_MEM_WRITE_ONLY;// 可读写的中间缓冲区
cl_mem_flags rw_flags = CL_MEM_READ_WRITE;// 使用主机内存的缓冲区(零拷贝)
cl_mem_flags use_host_flags = CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR;
创建示例:
c
float host_data[1000] = {...};
cl_int err;// 示例1:创建一个设备上只读的buffer,并用host_data初始化
cl_mem input_buffer = clCreateBuffer(context,CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, // 常用组合sizeof(host_data),host_data,&err
);// 示例2:创建一个设备上只写的buffer,不初始化
cl_mem output_buffer = clCreateBuffer(context,CL_MEM_WRITE_ONLY,1000 * sizeof(float),NULL,&err
);
b) 创建 Image 对象
创建 Image 比 Buffer 更复杂,需要指定格式(Channel Order, Channel Type)和描述(宽度、高度、深度)。
c
// clCreateImage2D (已废弃) 或通用的 clCreateImage
cl_mem clCreateImage(cl_context context,cl_mem_flags flags,const cl_image_format *image_format, // 图像格式描述符const cl_image_desc *image_desc, // 图像维度描述符void *host_ptr,cl_int *errcode_ret
);
cl_image_format
结构体:
c
typedef struct _cl_image_format {cl_channel_order image_channel_order; // 通道顺序 (e.g., CL_RGBA, CL_R)cl_channel_type image_channel_data_type; // 通道数据类型 (e.g., CL_FLOAT, CL_UNORM_INT8)
} cl_image_format;
cl_image_desc
结构体: 指定图像类型(2D, 3D)、维度、步幅等。
创建示例:
c
cl_image_format format;
format.image_channel_order = CL_RGBA; // RGBA 四个通道
format.image_channel_data_type = CL_FLOAT; // 每个通道是32位浮点数cl_image_desc desc;
desc.image_type = CL_MEM_OBJECT_IMAGE2D;
desc.image_width = 512;
desc.image_height = 512;
desc.image_depth = 0;
// ... 设置其他字段,通常可以默认置0 ...cl_mem input_image = clCreateImage(context,CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,&format,&desc,host_image_data, // 假设host_image_data指向512x512 RGBA float数据&err
);
// 创建 2D 图像
cl_mem create_image_2d(cl_context context, cl_mem_flags flags, const cl_image_format* format,size_t width, size_t height) {cl_int err;cl_mem image = clCreateImage2D(context, flags, format, width, height, 0, NULL, &err);if (err != CL_SUCCESS) {printf("创建 2D 图像失败,错误: %d\n", err);return NULL;}return image;
}// 创建 3D 图像
cl_mem create_image_3d(cl_context context, cl_mem_flags flags,const cl_image_format* format,size_t width, size_t height, size_t depth) {cl_int err;cl_mem image = clCreateImage3D(context, flags, format, width, height, depth, 0, 0, NULL, &err);if (err != CL_SUCCESS) {printf("创建 3D 图像失败,错误: %d\n", err);return NULL;}return image;
}
3. 主机与设备间的数据传输
创建对象后,需要将数据在主机和设备间移动。主要使用 clEnqueueWriteBuffer
/ clEnqueueReadBuffer
等命令。
a) 写数据到设备 (Host -> Device)
c
// 将主机数据拷贝到之前创建的 input_buffer
err = clEnqueueWriteBuffer(command_queue, // 命令队列input_buffer, // 目标buffer对象CL_TRUE, // 阻塞写入 (CL_TRUE: 等待拷贝完成才返回; CL_FALSE: 异步)0, // buffer中的偏移量(字节)sizeof(host_data), // 拷贝数据的大小host_data, // 主机源数据指针0, NULL, NULL // 事件等待列表和事件对象,用于同步
);
b) 从设备读取数据 (Device -> Host)
c
// 从 output_buffer 读取数据到主机内存
err = clEnqueueReadBuffer(command_queue,output_buffer,CL_TRUE, // 阻塞读取0,sizeof(host_output_data),host_output_data, // 存放结果的主机内存指针0, NULL, NULL
);
对于 Image,也有对应的 clEnqueueWriteImage
和 clEnqueueReadImage
函数。
显式数据传输
// 主机到设备传输
cl_int write_to_device(cl_command_queue queue, cl_mem buffer, const void* host_ptr, size_t size) {cl_event event;cl_int err = clEnqueueWriteBuffer(queue, buffer, CL_TRUE, 0, size, host_ptr, 0, NULL, &event);if (err == CL_SUCCESS) {clReleaseEvent(event);}return err;
}// 设备到主机传输
cl_int read_from_device(cl_command_queue queue, cl_mem buffer,void* host_ptr, size_t size) {cl_event event;cl_int err = clEnqueueReadBuffer(queue, buffer, CL_TRUE, 0,size, host_ptr, 0, NULL, &event);if (err == CL_SUCCESS) {clReleaseEvent(event);}return err;
}// 非阻塞传输
cl_int write_async(cl_command_queue queue, cl_mem buffer,const void* host_ptr, size_t size, cl_event* event) {return clEnqueueWriteBuffer(queue, buffer, CL_FALSE, 0,size, host_ptr, 0, NULL, event);
}
图像数据传输
// 写入图像数据
cl_int write_image_2d(cl_command_queue queue, cl_mem image,size_t width, size_t height, const void* data) {const size_t origin[3] = {0, 0, 0};const size_t region[3] = {width, height, 1};return clEnqueueWriteImage(queue, image, CL_TRUE, origin, region,0, 0, data, 0, NULL, NULL);
}// 读取图像数据
cl_int read_image_2d(cl_command_queue queue, cl_mem image,size_t width, size_t height, void* data) {const size_t origin[3] = {0, 0, 0};const size_t region[3] = {width, height, 1};return clEnqueueReadImage(queue, image, CL_TRUE, origin, region,0, 0, data, 0, NULL, NULL);
}
4. 在内核中使用内存对象
创建并传输数据后,就可以在内核中使用它们了。
a) 使用 Buffer
内核中将 Buffer 作为指针参数使用,并必须带有地址空间限定符。
c
// Kernel
__kernel void my_buffer_kernel(__global const float* input, // 对应 cl_mem input_buffer, 用 CL_MEM_READ_ONLY 创建__global float* output, // 对应 cl_mem output_buffer, 用 CL_MEM_WRITE_ONLY 创建__local float* scratch_mem) // 局部内存,大小在运行时由主机决定
{size_t gid = get_global_id(0);output[gid] = input[gid] * 2.0f;// ... 可以使用 scratch_mem ...
}
主机设置内核参数时,直接将 cl_mem
对象设进去:
c
clSetKernelArg(kernel, 0, sizeof(cl_mem), &input_buffer);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &output_buffer);
// 对于 __local 参数,只需指定大小,指针为NULL
size_t local_mem_size = 256 * sizeof(float);
clSetKernelArg(kernel, 2, local_mem_size, NULL);
b) 使用 Image
内核中使用专门的 Image 类型和内置函数。
c
// Kernel
__kernel void my_image_kernel(read_only image2d_t srcImage, // 只读图像对象write_only image2d_t dstImage, // 只写图像对象sampler_t sampler) // 采样器,定义滤波和寻址模式
{int2 coord = (int2)(get_global_id(0), get_global_id(1));// 从srcImage读取一个像素(使用采样器处理坐标越界、进行滤波等)float4 pixel = read_imagef(srcImage, sampler, coord);// 进行处理(例如,变灰)float gray = 0.2126f * pixel.x + 0.7152f * pixel.y + 0.0722f * pixel.z;// 将结果写入dstImagewrite_imagef(dstImage, coord, (float4)(gray, gray, gray, pixel.w));
}
主机设置参数类似:
c
clSetKernelArg(kernel, 0, sizeof(cl_mem), &input_image);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &output_image);
// 采样器也是内核的一个参数,需要单独创建(clCreateSampler)并设置
clSetKernelArg(kernel, 2, sizeof(cl_sampler), &sampler);
5. 内存映射
内存映射允许主机直接访问设备内存,避免显式拷贝。
// 映射设备内存
void* map_memory(cl_command_queue queue, cl_mem buffer, cl_map_flags flags, size_t size) {cl_event event;void* mapped_ptr;cl_int err = clEnqueueMapBuffer(queue, buffer, CL_TRUE, flags,0, size, 0, NULL, &event, &mapped_ptr);if (err != CL_SUCCESS) {return NULL;}clReleaseEvent(event);return mapped_ptr;
}// 取消映射
cl_int unmap_memory(cl_command_queue queue, cl_mem buffer, void* mapped_ptr) {cl_event event;cl_int err = clEnqueueUnmapMemObject(queue, buffer, mapped_ptr, 0, NULL, &event);if (err == CL_SUCCESS) {clReleaseEvent(event);}return err;
}// 使用内存映射的示例
cl_int use_mapped_memory(cl_command_queue queue, cl_mem buffer, size_t size) {// 映射内存进行写入float* mapped_data = (float*)map_memory(queue, buffer, CL_MAP_WRITE, size);if (!mapped_data) return CL_OUT_OF_RESOURCES;// 直接操作映射的内存for (size_t i = 0; i < size / sizeof(float); i++) {mapped_data[i] = (float)i;}// 取消映射return unmap_memory(queue, buffer, mapped_data);
}
6. 子缓冲区 (Sub-Buffers)
子缓冲区允许将一个大缓冲区划分为多个小缓冲区。
// 创建子缓冲区
cl_mem create_sub_buffer(cl_mem parent_buffer, cl_mem_flags flags,size_t origin, size_t size) {cl_buffer_region region;region.origin = origin;region.size = size;cl_int err;cl_mem sub_buffer = clCreateSubBuffer(parent_buffer, flags,CL_BUFFER_CREATE_TYPE_REGION,®ion, &err);if (err != CL_SUCCESS) {printf("创建子缓冲区失败,错误: %d\n", err);return NULL;}return sub_buffer;
}// 创建多个子缓冲区
cl_mem* create_sub_buffers(cl_mem parent_buffer, cl_mem_flags flags,size_t total_size, size_t chunk_size, int* num_chunks) {*num_chunks = total_size / chunk_size;cl_mem* sub_buffers = (cl_mem*)malloc(*num_chunks * sizeof(cl_mem));for (int i = 0; i < *num_chunks; i++) {cl_buffer_region region;region.origin = i * chunk_size;region.size = chunk_size;cl_int err;sub_buffers[i] = clCreateSubBuffer(parent_buffer, flags,CL_BUFFER_CREATE_TYPE_REGION,®ion, &err);if (err != CL_SUCCESS) {printf("创建子缓冲区 %d 失败\n", i);// 清理已创建的子缓冲区for (int j = 0; j < i; j++) {clReleaseMemObject(sub_buffers[j]);}free(sub_buffers);return NULL;}}return sub_buffers;
}
7. 内存对象信息查询
// 获取内存对象信息
void print_mem_object_info(cl_mem mem_obj) {cl_mem_object_type type;cl_mem_flags flags;size_t size;void* host_ptr;cl_context context;clGetMemObjectInfo(mem_obj, CL_MEM_TYPE, sizeof(type), &type, NULL);clGetMemObjectInfo(mem_obj, CL_MEM_FLAGS, sizeof(flags), &flags, NULL);clGetMemObjectInfo(mem_obj, CL_MEM_SIZE, sizeof(size), &size, NULL);clGetMemObjectInfo(mem_obj, CL_MEM_HOST_PTR, sizeof(host_ptr), &host_ptr, NULL);clGetMemObjectInfo(mem_obj, CL_MEM_CONTEXT, sizeof(context), &context, NULL);printf("内存对象信息:\n");printf(" 类型: %s\n", (type == CL_MEM_OBJECT_BUFFER) ? "缓冲区" :(type == CL_MEM_OBJECT_IMAGE2D) ? "2D图像" :(type == CL_MEM_OBJECT_IMAGE3D) ? "3D图像" : "未知");printf(" 大小: %zu bytes\n", size);printf(" 标志: 0x%X\n", flags);printf(" 主机指针: %p\n", host_ptr);
}// 获取图像格式信息
void print_image_format_info(cl_mem image) {cl_image_format format;clGetImageInfo(image, CL_IMAGE_FORMAT, sizeof(format), &format, NULL);printf("图像格式:\n");printf(" 通道顺序: %u\n", format.image_channel_order);printf(" 通道类型: %u\n", format.image_channel_data_type);
}
8. 内存对象生命周期管理
内存对象是引用计数的。创建时引用计数为1。
clRetainMemObject
: 增加引用计数。clReleaseMemObject
: 减少引用计数。当计数为0时,对象被释放,内存被回收。
c
clRetainMemObject(input_buffer); // 很少需要手动调用,除非多线程共享
// ... 使用 input_buffer ...
clReleaseMemObject(input_buffer);
clReleaseMemObject(output_buffer);
// 释放所有关联的内存对象是主机程序的责任
9. 完整示例:向量操作
#include <CL/cl.h>
#include <stdio.h>
#include <stdlib.h>#define DATA_SIZE 1024const char* get_cl_error_string(cl_int error) {// 简化的错误字符串映射switch (error) {case CL_SUCCESS: return "成功";case CL_INVALID_VALUE: return "无效值";case CL_OUT_OF_HOST_MEMORY: return "主机内存不足";case CL_OUT_OF_RESOURCES: return "资源不足";default: return "未知错误";}
}int main() {cl_int err;cl_platform_id platform;cl_device_id device;cl_context context;cl_command_queue queue;cl_program program;cl_kernel kernel;// 1. 初始化 OpenCLclGetPlatformIDs(1, &platform, NULL);clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);context = clCreateContext(NULL, 1, &device, NULL, NULL, &err);queue = clCreateCommandQueue(context, device, 0, &err);// 2. 创建内存对象float* host_data = (float*)malloc(DATA_SIZE * sizeof(float));for (int i = 0; i < DATA_SIZE; i++) {host_data[i] = (float)i;}// 输入缓冲区(只读,复制主机数据)cl_mem input_buffer = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,DATA_SIZE * sizeof(float), host_data, &err);// 输出缓冲区(只写)cl_mem output_buffer = clCreateBuffer(context,CL_MEM_WRITE_ONLY,DATA_SIZE * sizeof(float),NULL, &err);// 3. 创建和编译内核const char* kernel_source = "__kernel void vector_double(__global const float* input, \n"" __global float* output) { \n"" int i = get_global_id(0); \n"" output[i] = input[i] * 2.0f; \n""} \n";program = clCreateProgramWithSource(context, 1, &kernel_source, NULL, &err);clBuildProgram(program, 1, &device, NULL, NULL, NULL);kernel = clCreateKernel(program, "vector_double", &err);// 4. 设置内核参数clSetKernelArg(kernel, 0, sizeof(cl_mem), &input_buffer);clSetKernelArg(kernel, 1, sizeof(cl_mem), &output_buffer);// 5. 执行内核size_t global_size = DATA_SIZE;size_t local_size = 64;clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global_size, &local_size, 0, NULL, NULL);// 6. 读取结果float* results = (float*)malloc(DATA_SIZE * sizeof(float));clEnqueueReadBuffer(queue, output_buffer, CL_TRUE, 0,DATA_SIZE * sizeof(float), results, 0, NULL, NULL);// 7. 验证结果int correct = 1;for (int i = 0; i < DATA_SIZE; i++) {if (results[i] != host_data[i] * 2.0f) {correct = 0;break;}}printf("计算结果: %s\n", correct ? "正确" : "错误");// 8. 显示内存对象信息printf("\n输入缓冲区信息:\n");print_mem_object_info(input_buffer);printf("\n输出缓冲区信息:\n");print_mem_object_info(output_buffer);// 9. 清理资源free(host_data);free(results);clReleaseMemObject(input_buffer);clReleaseMemObject(output_buffer);clReleaseKernel(kernel);clReleaseProgram(program);clReleaseCommandQueue(queue);clReleaseContext(context);return 0;
}
总结
函数 | 用途 | 说明 |
---|---|---|
clCreateBuffer | 创建缓冲区 | 创建一维数据数组 |
clCreateImage2D | 创建2D图像 | 创建二维图像对象 |
clCreateSubBuffer | 创建子缓冲区 | 创建缓冲区的子区域 |
clEnqueueRead/WriteBuffer | 数据传输 | 主机设备间数据拷贝 |
clEnqueueMapBuffer | 内存映射 | 主机直接访问设备内存 |
clEnqueueCopyBuffer | 内存复制 | 设备内数据拷贝 |
OpenCL 内存对象是连接主机和设备数据的桥梁。其使用流程可以概括为:
创建:在主机上使用
clCreateBuffer
/clCreateImage
创建内存对象。传输:使用
clEnqueueWrite*
将输入数据从主机传输到设备。设置参数:使用
clSetKernelArg
将内存对象绑定到内核参数。执行:将内核入队执行,设备处理数据。
回读:使用
clEnqueueRead*
将结果数据从设备传输回主机。释放:使用
clReleaseMemObject
释放不再需要的内存对象。