Download PDF [686.3 kB]
Download Sample OCL ZIP [10.89 mB]
目录
介绍
新接触 OpenCL 的编程人员可能会发现,最完整的文档 Khronos OpenCL 规范并不适合作为 OpenCL 编程的开始。 该规格介绍了许多选项和备选方案,最初可能会让编程人员晕头转向。 其他面向 OpenCL 编写的代码示例可能主要使用设备内核代码,或使用用 OpenCL “包装器”库编写的主机代码,这会隐藏如何直接使用标准 OpenCL 主机 API 的具体细节。
本文中介绍的 SampleOCL旨在呈现清晰、可读的复杂(non-trivial)OpenCL 程序的基本元素。 该示例代码是面向主机(CPU)而非内核代码或性能 OpenCL™ 代码。 它展示了如何使用 OpenCL v1.2 规范构建一个简单的 OpenCL 应用的基本原理。[1] 同样,本文主要介绍了主机代码的结构以及该代码使用的 OpenCL API。
示例介绍
该代码示例使用的 OpenCL 内核与 ToneMapping 示例相同(参见下文参考文献),后者曾在面向 OpenCL 应用的英特尔® SDK 中发布[2]。 该简单内核旨在帮助由于太暗或太亮而分辨不清的图片清晰可见。 它从输入缓冲区中读取像素,对其进行修改,然后将其编写至输出缓冲区的同一位置。 关于该内核如何运行的更多信息,请参见文档高动态范围色调映射后期处理效果(High Dynamic Range Tone Mapping Post Processing Effect )[3]。
OpenCL 实施
SampleOCL 示例应用并不是要“包装” OpenCL,也就是说,它并非要使用“更高级的” API 来替换 OpenCL API。 总体而言,我发现,这些包装器不比直接使用 OpenCL API 更简单或更整洁,而且,虽然最初创建包装器的编程人员认为其使用起来更简单,但是包装器会对维护代码的 OpenCL 编程人员带来很大负担。 OpenCL API 是一个标准。 如要在专门的“改进” API 中对其进行包装,则需要丢弃许多使用该标准时的值。
依次说法,SampleOCL 实施的确使用了一些 C++ 类及相关方法将 OpenCL API 分成了几组。 该应用主要分为两大类,以区分通用应用元素和与 OpenCL 相关的元素。 前者是 C_SampleOCLApp,后者是 C_OCL。
限制
该示例代码仅关注 OpenCL 应用的基本原理,如版本 1.2 中的说明。 它并没有介绍与其他修订版之间的不同,但是其大部分信息应与最新修订版相关。
该示例的主机端应用代码并非要展示最优性能。 简便起见,我们将多个明显的优化略去。
OpenCL 应用基本原理
接下来,我们将对基本 OpenCL 应用程序序列进行完整的介绍。 我们在此强调“基本”,因为有许多选项并未涉及。 更多信息请参见 OpenCL 规范[1]。
OpenCL 应用需要能够在多处理设备上大规模并行,如采用 SIMD 指令和图形处理单元(GPU)的多核 CPU — 无论是独立还是集成至 CPU。 因此,OpenCL 应用首先需要能够确定哪些设备可用,并选择一台或多台设备进行使用。 一个平台可能支持多种设备,如包括集成 GPU 的 CPU,而且应用能够使用多个平台。
OpenCL 应用的每个可用平台都包括相关的名称、厂商等。该信息可通过使用 OpenCL API clGetPlatformIDs(),然后再使用 clGetPlatformInfo() 来获取,而且可用于选择目标平台。
选定平台后,必须创建一个环境(context)来实施应用所需的 OpenCL 设备、内存和其他资源。 拥有所选平台 ID 和目标设备类型(CPU、GPU 等)的规范后,应用便能够调用 clCreateContextFromType(),然后使用 clGetContextInfo() 来获取设备 ID。 或者,它可以直接使用 clGetDeviceIDs() 在给定平台 ID 上请求设备 ID,然后使用带有这些设备 ID 的 clCreateContext() 创建环境。 本示例使用了第二种方法来创建带有一个 GPU 设备的环境。
拥有目标设备 ID 和环境后,我们便可使用 clCreateCommandQueue() 为每台要使用的设备创建命令队列。 命令队列用于主机应用至 GPU 或其他设备的“排队”操作,例如,申请执行某个 OpenCL 内核。 本示例代码为 GPU 设备创建了一个命令队列。
初始化操作完成后,通常我们接下来将会使用 clCreateProgramWithSource() 创建一个或多个 OpenCL 程序对象。 程序创建后,它还需要使用 clBuildProgram() 进行构建(基本的编写和链接)。 该 API 支持为编译器设置选项,如设置 #defines 以修改程序源代码。
最后,借助创建和构建的程序,我们可创建链接至该程序中的函数的内核对象,从而为每个内核函数名称调用 clCreateKernel()。
运行 OpenCL 内核前,需要先设置要处理的数据,通常通过使用 clCreateBuffer() API 函数创建线性内存缓冲区来完成。 (本示例中未使用映像作 为 OpenCL 内存对象类型。) clCreateBuffer 函数可为既定尺寸的缓冲区分配内存,并可随意从主机内存复制数据,而且,它能够设置缓冲区,以直接使用主机代码已经分配的空间。 (后者能够避免从主机内存复制至 OpenCL 缓冲区,这是常见的性能优化。)
一般而言,内存将至少使用一个输入和一个输出缓冲区以及其他参数。 每次只能设置一个参数,以便内核在执行时访问。访问时只需调用每个参数的 clSetKernelArg() 函数即可。 该函数可使用内核函数参数列表中的一个特殊参数 — 数字索引进行调用。 第一个参数使用 index 0 传递,第二个参数 index 1 等。
借助参数集,调用包含内核对象和命令队列的函数 clEnqueueNDRangeKernel(),申请运行该内核。 内核排队后,主机代码可以做其他的事情,或者可以通过调用 clFinish() 函数等待内核(及之前加入队列的所有任务)完成。 本示例可以调用 clFinish(),因为它包括能够记录一个循环中总内核执行(包括所有排队开销)的时间,该循环需要等待所有执行都完成后, 才能记录最终时间或得出平均时间。
以上是构建 OpenCL 应用的部分。 此外,还有一些清除操作,如调用 clReleaseKernel、clReleaseMemObject、clReleaseProgram 等。虽然 OpenCL 在程序退出时应自动释放所有资源,但是本示例中仍包括这些操作。 较为复杂的程序可能希望即时释放资源以避免内存泄露。
最后应注意的一点是:虽然本示例没有使用“事件”,但是它们对于(比如)希望覆盖 CPU 和 GPU 处理的复杂应用非常有用。 但是,应注意到,任何将指针传递至事件的 clEnqueueXXXXX() 函数(其中 “XXXXX” 用众多可行函数中一个的名称进行替换)都将分配一个事件,然后调用代码负责在某一时刻将包含指针的 clReleaseEvent() 调用至事件。 如果该操作未完成,随着事件累积,程序将会出现内存泄露。
常见的错误是使用 clCreateUserEvent() 函数分配事件以传递至 clEnqueueXXXX 函数,这种操作认为完成后 OpenCL 将会标记该事件。 OpenCL 不会使用该事件,clEnqueueXXXX 将会返回新事件,覆盖指针传递的事件变量的内容。 这种方式很容易导致内存漏洞。 除了本示例的范围以外,用户事件还有其他目的。 关于 OpenCL 事件的更多详细信息,请参见 OpenCL 规范。[1]
项目结构
_tmain ( argc, argv ) - Main.cpp文件中的主要接入点函数。
创建 C_SampleOCLApp类的实例。
调用 C_SampleOCLApp::Run() 以启动应用。
这是它的全部功能! 参见以下 C_MainApp 和 C_SampleApp 类了解更多信息。
C_MainApp类 - C_MainApp.h文件中的通用 OpenCL 应用超类(super-class)
构建时,创建 OpenCL 类 C_OCL的实例。
定义通用应用 “run” 函数:
Run() | Run() 是读取代码以理解 OpenCL 应用如何初始化、运行和清理的良好起点。 Run() 可调用具有代表性的简单应用序列中的虚拟函数(见下文)。 |
声明要由 C_SampleOCLApp定义的虚拟函数(见下文):
AppParseArgs () | 解析命令行选项 |
AppUsage () | 打印使用说明 |
AppSetup () | 应用设置,包括 OpenCL 设置 |
AppRun () | 特定应用操作 |
AppCleanup () | 应用清除 |
C_SampleOCLApp类 - 来自 C_MainApp,可专门针对本示例定义函数。
为 SampleApp.cpp 和 SampleApp.h文件中的 C_MainApp虚拟函数实施应用特定代码。 (参见 C_MainApp类(见上文),了解实施的虚拟函数。)
在 ToneMap_OCL.cpp 文件中定义 "ToneMap" OpenCL 内核设置和运行函数:
RunOclToneMap () | 为 ToneMap 执行一次性设置,然后调用 ToneMap()。 |
ToneMap () | 设置 ToneMapping 内核参数并运行该内核。 |
C_OCL类 - 大部分的主机端 OpenCL API 可设置并清理代码。
构建时,初始化 OpenCL。 销毁时,在 OpenCL 之后清除。
在 C_OCL.cpp 和 C_OCL.h文件中定义 OpenCL 服务函数:
Start () | 为适当平台的英特尔® 锐炬™ 显卡设置 OpenCL 设备。 |
ReadAllPlatforms () | 获取所有可用 OpenCL 平台,保存其名称。 |
MatchPlatformName () | 辅助函数,可通过名称来选择平台。 |
GetDeviceType () | 辅助函数,可确定设备类型是 GPU 还是 CPU。 |
CheckExtension () | 检查某个 OpenCL 扩展名是否能够在目前的设备上使用。 |
ReadExtensions () | 获取列出当前设备的所有 OpenCL 扩展名的字符串。 |
SetCurrentDeviceType () | 设置目标设备类型并创建 OpenCL 环境和命令队列。 |
CreateProgramFromFile () | 加载包含 OpenCL 内核的文件,创建 OpenCL 程序并对其进行构建。 |
ReadSourceFile () | 将 OpenCL 内核源文件读入字符串,准备将其构建为程序。 |
CreateKernelFromProgram ( ) | 从以前构建的程序中创建 OpenCL 内核。 |
GetDeviceInfo () | 获取设备特定信息的两个辅助函数:其一可分配内存以接收和返回结果;其二可通过指针将结果返回至调用程序提供的内存。 |
ClearAllPlatforms () | 释放与以前选中的平台相关的所有内容。 |
ClearAllPrograms () | 释放所有现有 OpenCL 程序。 |
ClearAllKernels () | 释放所有现有 OpenCL 内核。 |
所使用的 OpenCL API
clBuildProgram | clCreateBuffer |
clCreateCommandQueue | clCreateContext |
clCreateKernel | clCreateProgramWithSource |
clEnqueueMapBuffer | clEnqueueNDRangeKernel |
clEnqueueUnmapMemObject | clFinish |
clGetDeviceIDs | clGetDeviceInfo |
clGetPlatformIDs | clGetPlatformInfo |
clReleaseCommandQueue | clReleaseContext |
clReleaseDevice | clReleaseDevice |
clReleaseKernel | clReleaseMemObject |
clReleaseProgram | clSetKernelArg |
控制示例
本示例从 Microsoft Windows* 命令行控制台运行。 它支持以下命令行和可选参数:
ToneMapping.exe [ ? | --h ] [-c|-g] [-list] [-p "platformName] [-i "full image filename"]
? OR --h | 打印该帮助消息 |
-c | 在 CPU 上运行 OpenCL |
-g | 在 GPU 上运行 OpenCL — 默认 |
-list | 显示平台名称字符串列表 |
-p "platformName" | 提供平台名称(如果有空间将会用引号标出)以供检查和使用。 |
-i "full image filename" | 提供图像文件名称(如果有空间将会用引号标出)以供处理。 |
参考文献
- OpenCL Specifications from Khronos.org:
- Intel® SDK for OpenCL™ Applications:http://software.intel.com/en-us/vcsource/tools/opencl-sdk
- High Dynamic Range Tone Mapping Post Processing Effect:
http://software.intel.com/en-us/vcsource/samples/hdr-tone-mapping
英特尔、Intel 标识、 Iris 和锐炬是英特尔在美国和其他国家的商标。
* 其他的名称和品牌可能是其他所有者的资产。
OpenCL 和 OpenCL 标识是苹果公司的商标,需获得 Khronos 的许可方能使用。
英特尔公司 © 2014 年版权所有。 所有权保留。