从 0 到 1 掌握 OpenCL 异构计算(第 1 篇)
一、独立代码文件带逐行注释1. 主机端代码vector_add.cpp#include iostream #include fstream #include vector #include string #include CL/cl.h // OpenCL官方头文件定义所有API和数据结构 // 函数读取.cl内核文件内容到字符串 std::string read_kernel_file(const std::string filename) { // 以只读模式打开文件定位到文件末尾 std::ifstream file(filename, std::ios::in | std::ios::binary | std::ios::ate); if (!file.is_open()) { // 检查文件是否成功打开 throw std::runtime_error(无法打开内核文件 filename); } // 获取文件大小 size_t file_size file.tellg(); // 创建对应大小的字符串缓冲区 std::string kernel_source(file_size, \0); // 定位到文件开头 file.seekg(0); // 读取整个文件内容到缓冲区 file.read(kernel_source[0], file_size); // 关闭文件 file.close(); return kernel_source; } int main() { try { // -------------------------- 步骤1定义主机端数据 -------------------------- const int VECTOR_LENGTH 1024; // 向量长度可根据需求修改 std::vectorfloat host_a(VECTOR_LENGTH, 1.0f); // 输入向量A所有元素为1.0 std::vectorfloat host_b(VECTOR_LENGTH, 2.0f); // 输入向量B所有元素为2.0 std::vectorfloat host_c(VECTOR_LENGTH, 0.0f); // 输出向量C初始化为0.0 // -------------------------- 步骤2获取OpenCL平台和设备 -------------------------- cl_platform_id platform; // 获取第一个可用的OpenCL平台通常对应CPU/GPU厂商 cl_int err clGetPlatformIDs(1, platform, nullptr); if (err ! CL_SUCCESS) { throw std::runtime_error(获取OpenCL平台失败错误码 std::to_string(err)); } cl_device_id device; // 获取该平台下第一个GPU类型的设备 err clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, device, nullptr); if (err ! CL_SUCCESS) { // 如果没有GPU设备尝试使用CPU设备 std::cerr 未找到GPU设备尝试使用CPU设备 std::endl; err clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, 1, device, nullptr); if (err ! CL_SUCCESS) { throw std::runtime_error(获取OpenCL设备失败错误码 std::to_string(err)); } } // 打印设备信息可选用于验证 char device_name[256]; clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(device_name), device_name, nullptr); std::cout 使用设备 device_name std::endl; // -------------------------- 步骤3创建上下文和命令队列 -------------------------- // 创建上下文管理所有OpenCL资源设备、内存、程序、内核 cl_context context clCreateContext(nullptr, 1, device, nullptr, nullptr, err); if (err ! CL_SUCCESS) { throw std::runtime_error(创建上下文失败错误码 std::to_string(err)); } // 创建命令队列主机向设备发送命令的通道 cl_command_queue queue clCreateCommandQueue(context, device, 0, err); if (err ! CL_SUCCESS) { throw std::runtime_error(创建命令队列失败错误码 std::to_string(err)); } // -------------------------- 步骤4创建设备端内存缓冲区 -------------------------- // 创建只读缓冲区将主机端向量A的数据拷贝到设备端 cl_mem dev_a clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, VECTOR_LENGTH * sizeof(float), host_a.data(), err); if (err ! CL_SUCCESS) { throw std::runtime_error(创建设备缓冲区dev_a失败错误码 std::to_string(err)); } // 创建只读缓冲区将主机端向量B的数据拷贝到设备端 cl_mem dev_b clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, VECTOR_LENGTH * sizeof(float), host_b.data(), err); if (err ! CL_SUCCESS) { throw std::runtime_error(创建设备缓冲区dev_b失败错误码 std::to_string(err)); } // 创建只写缓冲区用于存储设备端计算结果 cl_mem dev_c clCreateBuffer(context, CL_MEM_WRITE_ONLY, VECTOR_LENGTH * sizeof(float), nullptr, err); if (err ! CL_SUCCESS) { throw std::runtime_error(创建设备缓冲区dev_c失败错误码 std::to_string(err)); } // -------------------------- 步骤5加载并编译内核程序 -------------------------- // 读取外部.cl内核文件内容 std::string kernel_source read_kernel_file(vector_add.cl); const char* kernel_source_ptr kernel_source.c_str(); // 创建程序对象从源代码字符串创建 cl_program program clCreateProgramWithSource(context, 1, kernel_source_ptr, nullptr, err); if (err ! CL_SUCCESS) { throw std::runtime_error(创建程序对象失败错误码 std::to_string(err)); } // 编译程序为指定设备生成可执行代码 err clBuildProgram(program, 1, device, nullptr, nullptr, nullptr); if (err ! CL_SUCCESS) { // 如果编译失败获取编译日志并打印 size_t log_size; clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, nullptr, log_size); std::vectorchar build_log(log_size); clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, build_log.data(), nullptr); throw std::runtime_error(内核编译失败\n std::string(build_log.begin(), build_log.end())); } // 创建内核对象从编译好的程序中提取指定名称的内核函数 cl_kernel kernel clCreateKernel(program, vector_add, err); if (err ! CL_SUCCESS) { throw std::runtime_error(创建内核对象失败错误码 std::to_string(err)); } // -------------------------- 步骤6设置内核参数 -------------------------- // 设置第0个参数设备端向量A err clSetKernelArg(kernel, 0, sizeof(cl_mem), dev_a); // 设置第1个参数设备端向量B err | clSetKernelArg(kernel, 1, sizeof(cl_mem), dev_b); // 设置第2个参数设备端结果向量C err | clSetKernelArg(kernel, 2, sizeof(cl_mem), dev_c); if (err ! CL_SUCCESS) { throw std::runtime_error(设置内核参数失败错误码 std::to_string(err)); } // -------------------------- 步骤7执行内核 -------------------------- size_t global_size VECTOR_LENGTH; // 全局线程数每个元素对应一个线程 // 将内核执行命令加入命令队列 err clEnqueueNDRangeKernel(queue, kernel, 1, nullptr, global_size, nullptr, 0, nullptr, nullptr); if (err ! CL_SUCCESS) { throw std::runtime_error(执行内核失败错误码 std::to_string(err)); } // -------------------------- 步骤8读取计算结果 -------------------------- // 阻塞式读取等待内核执行完成后将结果从设备端拷贝回主机端 err clEnqueueReadBuffer(queue, dev_c, CL_TRUE, 0, VECTOR_LENGTH * sizeof(float), host_c.data(), 0, nullptr, nullptr); if (err ! CL_SUCCESS) { throw std::runtime_error(读取结果失败错误码 std::to_string(err)); } // -------------------------- 步骤9验证结果 -------------------------- std::cout 计算成功前5个结果应为3.0 std::endl; for (int i 0; i 5; i) { std::cout host_c[ i ] host_c[i] std::endl; } // 验证所有结果是否正确 bool all_correct true; for (int i 0; i VECTOR_LENGTH; i) { if (host_c[i] ! host_a[i] host_b[i]) { all_correct false; std::cerr 错误host_c[ i ] host_c[i] 预期值 host_a[i] host_b[i] std::endl; break; } } if (all_correct) { std::cout 所有 VECTOR_LENGTH 个元素计算正确 std::endl; } // -------------------------- 步骤10释放所有OpenCL资源 -------------------------- clReleaseMemObject(dev_a); clReleaseMemObject(dev_b); clReleaseMemObject(dev_c); clReleaseKernel(kernel); clReleaseProgram(program); clReleaseCommandQueue(queue); clReleaseContext(context); } catch (const std::exception e) { std::cerr 程序异常 e.what() std::endl; return 1; } return 0; }代码功能说明完整实现了 OpenCL 向量加法的主机端逻辑包含异常处理、设备自动选择、内核文件读取、编译错误日志打印和结果验证功能符合工业级开发规范。验证来源Khronos OpenCL 2.0 官方规范、Intel OpenCL SDK 2024 开发指南2. 内核端代码vector_add.cl// 内核函数声明__kernel关键字表示这是一个可在设备上执行的函数 __kernel void vector_add( __global const float* a, // __global指向设备全局内存的指针只读 __global const float* b, // __global指向设备全局内存的指针只读 __global float* c // __global指向设备全局内存的指针可写 ) { // 获取当前线程在全局线程组中的索引0到VECTOR_LENGTH-1 int i get_global_id(0); // 边界检查防止向量长度不是局部线程数整数倍时出现越界访问 if (i 1024) { // 每个线程计算一个元素的加法 c[i] a[i] b[i]; } }代码功能说明运行在 GPU/CPU 设备上的并行计算内核每个线程独立计算向量中一个元素的加法。验证来源Khronos OpenCL C 语言规范、NVIDIA OpenCL 编程指南