OpenCL内存性能优化 (3)
7.4 零拷贝
Adreno OpenCL提供了一些机制来避免在主机端可能发生的昂贵的内存复制操作。根据内存对象的创建方式,有几种不同的选项可以避免过度复制。
7.4.1 使用map代替copy
假设OpenCL应用程序对数据流有完全的控制,即目标和源内存对象的创建都由OpenCL应用程序管理。这是最简单的情况,可以通过以下步骤避免内存复制:
-
当创建一个buffer/image对象时,使用标志CL_MEM_ALLOC_HOST_PTR,并按照如下步骤操作:
- 首先设置clCreateBuffer并赋值给cl_mem_flags:
cl_mem Buffer = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_ALLOC_HOST_PTR, sizeof(cl_ushort) * size, NULL, &status);
- 然后使用map函数返回指向主机的指针:
cl_uchar *hostPtr = (cl_uchar *)clEnqueueMapBuffer( commandQueue, Buffer, CL_TRUE, CL_MAP_WRITE, 0, sizeof(cl_uchar) * size, 0, NULL, NULL, &status);
- Host使用指针hostPtr更新缓冲区:例如,主机可以填充摄像头数据或从磁盘读取数据到缓冲区
- 解除对象映射:
status = clEnqueueUnmapMemObject( commandQueue, Buffer, (void *) hostPtr, 0, NULL, NULL)
- 该对象可以被OpenCL内核使用。
CL_MEM_ALLOC_HOST_PTR是避免在这种情况下复制数据的唯一方法。对于其他标志,如CL_MEM_USE_HOST_PTR或CL_MEM_COPY_HOST_PTR,驱动程序必须做额外的内存复制以便GPU访问。
7.4.2 避免对非OpenCL分配的对象进行内存复制
7.4.2.1 ION内存扩展
如果一个内存对象最初是在OpenCL API范围之外创建的,并且使用ION/Gralloc分配,那么cl_qcom_ion_host_ptr扩展可以用来创建一个缓冲区/镜像对象,它将其ION内存映射到GPU可访问内存,而不引起额外的复制。
注意:通过QTI扩展使用ION内存来避免内存复制,这是一个详细的示例代码,可以在请求时提供。
7.4.2.2 QTI Android原生缓冲区(ANB)扩展
在许多摄像机和视频处理用例中,ANB(由gralloc分配)必须被共享。共享是可能的,因为缓冲区是基于ION的。然而,要使用ION路径,开发人员需要从这些缓冲区提取内部句柄,这需要访问QTI的内部头文件。cl_qcom_android_native_buffer_host_ptr扩展提供了一种更直接的方式与OpenCL共享ANB,而不需要访问QTI头文件。这使得ISVs和其他第三方开发人员能够为ANB实现零拷贝技术。
注意:可以根据请求提供cl_qcom_android_native_buffer_host_ptr扩展的示例。
7.4.2.3 使用标准EGL扩展
cl_khr_egl_image扩展从一个EGL映像创建一个OpenCL映像。随之而来的主要好处是:
- 标准化;使用这种技术编写的代码很可能在支持它的其他gpu上工作。
- EGL/CL扩展(cl_khr_egl_event和EGL_KHR_cl_event)设计用于此扩展,使更有效的同步成为可能。
- YUV处理使用egl_img_image_plane_attrbs扩展会简单一点。
7.5 提高缓存利用率
为了更好的使用缓存,应该遵循以下规则:
-
检查缓存抖动和缓存使用效率。Snapdragon Profiler可以提供缓存访问信息,例如加载/存储的字节数和缓存命中率/失误率。
- 如果要加载到UCHE的字节数比内核预期的要高很多,可能存在缓存抖动。
- L1/L2命中率/失误率等指标可以判断缓存的使用情况。
-
通过以下方法避免抖动:
- 调整工作组大小,如减少工作组大小
- 改变访问模式,例如改变内核的维度。
- 如果在使用循环时出现缓存抖动,在循环中添加原子或barrier可以减少抖动的可能性。
7.6 CPU缓存操作
对于可缓存的内存对象,OpenCL驱动程序必须在适当的时间刷新和/或使CPU缓存失效。这确保了CPU和GPU在尝试访问数据时都能看到数据的最新副本。例如,当映射内核输出缓冲区供主机CPU读取时,CPU缓存必须失效。OpenCL驱动程序有一个复杂的CPU缓存管理策略,该策略通过跟踪每个内存对象上的数据可见性,并尽可能地推迟操作,来尽量减少缓存操作的数量。例如,在内核启动之前,可能会对输入缓冲区进行CPU缓存刷新。
CPU缓存操作有一个极其容易被测量的开销值,即CL_PROFILING_COMMAND_QUEUED和CL_PROFILING_COMMAND_SUBMIT之间的增量,如图4-1所示。在某些情况下,clEnqueueMapBuffer/Image和clEnqueueUnmapBuffer/Image的执行时间会增加。CPU缓存操作的成本通常随内存对象大小而线性增加。
为了最小化CPU缓存操作的成本,应用程序的管道应该是结构化的,这样处理就不会在CPU和GPU之间来回移动。此外,应用程序应该以这样的方式分配内存对象,即受CPU和GPU来回访问的数据在不同的内存对象中,只有一次访问转换的数据。
内存对象应该使用适合其预期用途的CPU缓存策略来创建。当为缓存对象或图像对象分配内存时,驱动程序将选择CPU缓存策略。CPU cache策略默认为回写。但是,如果在flags中指定了CL_MEM_HOST_WRITE_ONLY或CL_MEM_READ_ONLY,驱动程序将假定应用程序不打算使用主机CPU读取数据。在这种情况下,CPU缓存策略设置为write-combine。
对于外部分配的内存对象(如使用ION和ANB机制),应用程序可以更直接地控制CPU缓存策略。当将这些对象导入OpenCL时,应用程序必须正确设置CPU缓存策略标志。
7.7 SVM的使用
Adreno A5x gpu支持粗粒度支持向量机(SVM),这是OpenCL 2.0完整配置文件中的一个关键特性。使用SVM,主机和设备内存地址是相同的。OpenCL 2.0中的SVM特性允许在主机和设备之间轻松共享内存,并且现在可以访问OpenCL设备上的主机指针。
对于粗粒度的SVM,主机或设备对内存的访问限制在同步点(map/unmap)。这可以极大地简化需要跨主机和设备使用基于指针的数据结构的应用程序。
7.8 减少功耗耗的最佳操作
功率和电量是移动应用的一个主要因素。具有最佳性能的应用程序可能没有最佳的功率/能量性能,反之亦然。因此,了解功率/电量和性能要求是很重要的。以下是OpenCL在降低电能和能源消耗方面的一些技巧:
- 尝试所有方法来避免内存复制,例如,使用ION内存实现零拷贝,使用CL_MEM_ALLOC_HOST_PTR创建缓冲区时使用clCreateBuffer。还要避免使用执行数据复制的OpenCL API。
- 最小化主机和设备之间的内存事务。这可以通过在固定内存或本地内存中存储内存来实现,使用更短的数据类型,降低数据精度,消除私有内存的使用等。
- 优化内核并提高其性能。内核运行得越快,通常消耗的能量就越少。
- 最小化软件开销。例如,使用事件驱动的管道来减少主机和设备的通信开销。避免创建过多的对象,避免在内核执行之间创建或释放对象。