目录
在OpenGL ES中编译着色器
着色器编译和程序链接都是昂贵的操作。高帧率渲染时,生成新程序会导致掉帧。因此,应避免在应用程序的交互部分进行着色器编译和程序链接。
预备知识
你必须理解以下概念:
- 着色器编译
- 程序链接
如何优化着色器编译
启动时或加载游戏关卡时,编译所有着色器并链接所有程序。
编译着色器时要避免的行为
Arm 建议您:
- 不要尝试在游戏的交互过程中编译着色器。
- 不要依赖 Android blob cache 来进行交互式编译和链接,因为第一次使用性能很差,因为着色器不在缓存中。这个cache通常太小,无法包含复杂应用程序的所有着色器程序,也无法缓存所有着色器。
未正确编译着色器的负面影响
在应用程序的交互部分尝试编译和链接着色器的代价是高 CPU 负载以及掉帧。
Vulkan 中的管道创建
Vulkan 管道具有与 OpenGL ES 着色器类似的编译要求。此外,Vulkan 不提供和Android blob cache类似的自动缓存。您负责提供已编译的持久化存储中的程序,供程序调用使用。
预备知识
你必须理解以下概念:
- vulkan 管道
如何优化管道创建
尝试使用以下优化技术:
- 在开始或加载游戏关卡时创建管道
- 使用管道缓存加速管道创建
- 将管道缓存序列化到磁盘,然后在下次使用应用程序时重新加载,为终端用户提供更快的加载时间。
注:Mali GPU 忽略以下标志:VK\_PIPELINE\_CREATE\_DERIVATIVE\_BIT、VK\_PIPELINE\_CREATE\_ALLOW\_DERIVATIVES\_BIT、VK\_PIPELINE\_CREATE\_DISABLE\_OPTIMIZATION\_BIT。
对应用程序的负面影响
请记住以下对您的应用程序的影响:
- 在应用程序的交互部分尝试创建管道会增加 CPU 负载并可能导致掉帧。
- 管道缓存不做序列化并重新加载的方法的话,会增加之后应用程序运行的加载时间。
例子
GitHub 上的 Vulkan samples仓库中提供了此最佳实践的示例代码。管道管理教程可以在 GitHub 上找到:Pipeline cache tutorial
Vulkan中的内存分配
对于频繁分配,不要使用 vkAllocateMemory() 分配器。 vkAllocateMemory() 需要昂贵的内核调用。
预备知识
你必须理解以下概念:
- 内存分配
如何优化内存分配
分配内存时,使用您自己的分配器来管理内存块分配。
分配内存时要避免的事情
不要将 vkAllocateMemory () 用作常用分配器。
次优内存分配的负面影响
使用 vkAllocateMemory() 作为常用分配器会增加应用程序 CPU 的负载。
调试内存分配问题
在运行时监控对 vkAllocateMemory() 的调用频率和分配大小。
OpenGL ES CPU 内存映射
通过使用 glMapBufferRange(),OpenGL ES可以直接访问映射到应用地址空间的缓冲。对映射内存的流式写入是高效的,但读取是昂贵的,因为它们在 CPU 上没有缓存。
预备知识
你必须理解以下概念:
- 缓冲(Buffer)
- 内存映射(Memory mapping)
如何优化CPU的内存映射
对连续地址进行只写更新,可以从CPU的存储合并特性(store merging)中受益。
使用 CPU 内存映射时要避免的
Arm 建议您不要从映射缓冲区中读取值。
未正确使用 CPU 内存映射的负面影响
从未缓存的缓冲区读取会增加读取它们的函数的CPU 负载。
Vulkan CPU内存映射
Vulkan 为应用程序提供对多种缓冲内存类型的支持。
预备知识
你必须理解以下概念:
- CPU和GPU内存映射
- 缓存与非缓存内存
内存类型
在 Midgard 架构 GPU 上,Mali GPU 驱动程序公开以下内存类型:
- DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT
- DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_CACHED_BIT
- DEVICE_LOCAL_BIT | LAZILY_ALLOCATED_BIT
在 Bifrost 及更高版本的架构 GPU 上,Mali GPU 驱动程序公开了以下内存类型:
- DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT
- DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_CACHED_BIT
- DEVICE_LOCAL_BIT | HOST_VISIBLE_BIT | HOST_COHERENT_BIT | HOST_CACHED_BIT
- DEVICE_LOCAL_BIT | LAZILY_ALLOCATED_BIT
四种内存类型及其用途
这些内存类型有用的目的是:
非缓存,一致(Not cached, coherent)
HOST_VISIBLE | HOST_COHERENT 内存类型:
- 保证支持。
- 在 CPU 上提供不缓存的存储。
- 避免不必要的数据污染 CPU 缓存。
- 使用 CPU 硬件写缓冲区有效地合并小的写入,然后将这些写入发送到外部存储设备。
- 它是 CPU 只写资源的最佳内存类型。
缓存,不一致(Cached, incoherent)
HOST_VISIBLE | HOST_CACHED 内存类型:
- 在 CPU 上提供缓存存储,但不保证 CPU 和 GPU 内存的一致性。内存的 CPU 视图与内存的 GPU 视图不一致,因此您必须调用:
- vkFlushMappedRanges() 当 CPU 向 GPU 写完数据。
- vkInvalidateMappedRanges() 在读回 GPU 写入的数据时。
- 但是,这两种调用的使用成本都很高,因此必须谨慎使用。
- 只用于CPU上的应用程序需要映射和读取的资源。
缓存,一致(Cached, coherent)
HOST_VISIBLE | HOST_COHERENT | HOST_CACHED 内存类型:
- 在CPU上提供缓存存储,他总之保持和GPU的内存视图一致,不需要手动同步。
- Mali Bifrost 和更高版本的 GPU 支持。前提是芯片组支持CPU和GPU之间的硬件一致性协议。
- 硬件一致性避免了手动同步操作的开销。缓存的一致的内存(cached corerent)优先于缓存的非一致内存类型(cached incoherent)
- 只用于CPU上的应用程序需要映射和读取的资源。
- 硬件一致性有一定的功耗,因此不要用于 CPU 只写的资源。对于只写资源,使用未缓存的一致内存类型绕过 CPU 缓存。
延迟分配(Lazily allocated)
LAZILY_ALLOCATED 内存类型:
- 它是一种特殊的内存类型,初始的时候只有 GPU 虚拟地址空间,而没有物理内存页。如果访问内存,则按需分配物理页。
- 它必须与使用 VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT 创建的瞬态图像(transient image)附件一起使用。瞬态图像是设计来仅在单个渲染通道中存在的帧缓冲区附件。这样做可以避免使用物理内存附件。您可以通过使用延迟分配和 VK_ATTACHMENT_STORE_OP_DONT_CARE 存储操作来做到这一点。常见的用例包括用于简单渲染的深度或模板缓冲区。另一个用例是G-buffer 附件,用于延迟照明,然后由稍后的子通道消耗,并且不会写回内存。
- 不用于会写回内存的资源。
延迟分配可以类比于是一种临时变量优化。可以参考这个回答。https://stackoverflow.com/questions/36612147/vulkan-lazily-allocated-memory。
For example, consider deferred rendering. You need g-buffers. But you're going to fill them up during the g-buffer pass, and you'll consume them during the lighting pass(es). After that point, you won't be using their contents again.
For many renderers, this doesn't really matter. But with a tile-based renderer, it can. Why? Because if a tile is big enough to store all of the g-buffer data all at once, then the implementation doesn't actually need to write the g-buffer data out to memory. It can just leave everything in tile memory, do the lighting pass(es) within the tile (you read them as input attachments), and then forget they exist.
如何优化 Vulkan CPU 内存映射
尝试使用以下优化技术:
- 使用 HOST_VISIBLE | HOST_COHERENT 用于不可变资源
- 使用 HOST_VISIBLE | HOST_COHERENT 内存,用于在 CPU 上只写的资源
- 使用 memcpy() 将更新写入 HOST_VISIBLE | HOST_COHERENT 内存或连续写入以从 CPU 写入组合单元获得最佳效率。
- 使用 HOST_VISIBLE | HOST_COHERENT | HOST_CACHED 内存,用于读取回 CPU 的资源。如果它不可用,使用 HOST_VISIBLE | HOST_CACHED。
- 将 LAZILY_ALLOCATED 内存用于仅在单个渲染过程中存在的瞬态帧缓冲区附件。
- 仅将 LAZILY_ALLOCATED 内存用于 TRANSIENT_ATTACHMENT 帧缓冲区附件。
- 映射和取消映射缓冲区有 CPU 成本。因此,建议持久映射需要经常访问的缓冲区。例如:统一缓冲区(uniform buffer)、数据缓冲区或动态顶点数据缓冲区。
Vulkan CPU 内存映射技术要避免
Arm 建议您:
- 不要从 CPU 上未缓存的内存中读回数据。
- 不要将必须在 CPU 上读取的子分配器元数据(vulkan suballocator metadata???)存储在未缓存的内存缓冲区中。
低效 Vulkan CPU 内存映射的负面影响
由于 CPU 处理成本增加,未缓存的回读可能比缓存的读取慢得多。
调试 Vulkan CPU 内存映射性能问题
您可以采用以下几种技术:
- 检查所有 CPU 读取缓冲区是否都在使用缓存内存。
- 设计好缓冲区的接口,需要时隐式刷新或者无效化缓冲。调试一致性故障既困难又耗时,如果没有做好这种基础架构实现。
vulkan的命令池(command pool)
命令池不会自动从已删除的命令缓冲区中回收内存,除非使用 RESET\_COMMAND\_BUFFER\_BIT 标志创建。没有此标志的池在应用程序重置池之前不会回收它们的内存。
预备知识
你必须理解以下概念:
- 命令池(command pool)
如何优化命令池
定期调用 vkResetCommandPool() 释放内存。
要避免的命令池技术
- 阅读 Optimizing command buffers for Vulkan了解为什么不推荐使用 RESET_COMMAND_BUFFER_BIT。但是,使用 RESET_COMMAND_BUFFER_BIT 比不释放内存要好。
- 创建命令池时,Mali GPU 会忽略标志 VK_COMMAND_POOL_CREATE_TRANSIENT_BIT。 Mali GPU 也会忽略 vkTrimCommandPool() 命令。
低效 Vulkan 命令池的负面影响
在触发手动命令池重置之前内存使用量增加。
优化vulkan的命令缓冲
命令缓冲的使用标志会影响性能。
预备知识
你必须理解以下概念:
- 命令缓冲
- 命令池
如何优化命令缓冲
尝试使用以下优化技术:
- 为了获得最佳性能,请设置 ONE_TIME_SUBMIT_BIT 标志。
- 构建每帧命令缓冲(per-frame command buffers )而不是同时使用命令缓冲(simultaneous command buffers)
- 如果是每次在应用程序逻辑中重放相同的命令序列,则使用 SIMULTANEOUS_USE_BIT。它比应用程序手动重播命令有效,但比一次性提交缓冲区效率低。
要避免的命令缓冲区技术
Arm 建议您:
- 不要设置 SIMULTANEOUS_USE_BIT,除非需要这样做。
- 不要使用设置了 RESET_COMMAND_BUFFER_BIT 的命令池。这样做会增加内存管理开销,因为它禁止驱动程序对池中的所有命令缓冲区使用单个大型分配器。
低效 Vulkan 命令缓冲区的负面影响
请记住以下几点:
- 如果使用了不适当的标志,或者命令缓冲区重置过于频繁,则存在增加 CPU 负载的风险。
- 避免在高频的调用路径上调用 vkResetCommandBuffer()。
调试 Vulkan 命令缓冲区性能问题
您可以尝试几种方法来加快调试过程:
- 使用除ONE_TIME_SUBMIT_BIT外的命令缓冲标志时,都做一下性能审查和评估。
- 评估 vkResetCommandBuffer() 的每次使用,并评估它是否可以用 vkResetCommandPool() 代替。
辅助命令缓冲区
当前的 Mali 硬件不支持在辅助命令缓冲区中调用命令。因此,使用辅助命令缓冲区时会产生额外的开销。
预备知识
你必须理解以下概念:
- 命令缓冲
- 命令池
使用辅助命令缓冲区
多线程构造命令缓冲区的时候,希望应用程序使用辅助命令缓冲区。但是,必须最小化辅助命令缓冲区调用的总数。与主命令缓冲区一样,我们建议避免使用 SIMULTANEOUS_USE_BIT 创建命令缓冲区,因为会增加开销。
如何优化辅助命令缓冲区
尝试使用以下优化技术:
- 使用辅助命令缓冲区以允许多线程渲染通道构造。
- 最小化每帧使用的辅助命令缓冲区调用的数量。
要避免的辅助命令缓冲区步骤
不要在辅助命令缓冲区上设置 SIMULTANEOUS_USE_BIT。
辅助命令缓冲区效率低下的负面影响
请记住,会导致 CPU 负载增加。
为 Vulkan 优化描述符集(descriptor sets )和布局(layouts)
Midgard 和 Bifrost Mali GPU 在 API 级别支持四个同时绑定的描述符集。但是,它们内部在每个绘制调用都需要一个物理描述符表(physical descriptor table)。
预备知识
你必须理解以下概念:
- 描述符集(Descriptor sets)
- 绑定空间(Binding spaces)
描述符集和布局
如果四个源描述符集中的任何一个已更改,则驱动程序会为绘制调用重建内部表。描述符更改后的第一个绘制调用比重复使用相同描述符集的绘制调用具有更高的 CPU 开销。较大的描述符集会导致更昂贵的重建。当前的驱动程序,在描述符集池的分配不是池化的。所以不要在性能关键代码路径上调用 vkAllocateDescriptorSets()。
在 Valhall 之前,描述符集池分配从未被池化。
对于 Valhall Mali GPU,表重建代价要小得多。但是,我们关于不在关键代码路径上分配的建议仍然适用。
vkDescriptorPool 创建标志
Mali 忽略 VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT 标志,但应用程序仍必须支持该规范。这意味着如果设置了 VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,那么您仍然必须检查池碎片。如果未设置该标志,则不得释放单个描述符集,而只能使用 vkAllocateDescriptorSets() 和 vkResetDescriptorPool()。
如何优化描述符集和布局
尝试使用以下优化技术:
- 尽可能打包描述符集绑定空间。
- 更新已经分配但不再引用的描述符集。 而不是重置描述符池和重新分配新的描述符集。
- 重用预先分配的描述符集,不要每次都用相同的信息更新它们。
- 使用 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC 或 VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC 来绑定具有不同偏移量的相同 UBO 或 SSBO。另一种方法是构建更多的描述符集。
要避免的描述符集和布局技术
Arm 建议您:
- 不要在描述符集中留下空白。
- 不要留下未使用的条目,因为复制和合并仍然有计算成本。
- 不要从性能关键代码路径上的描述符池中分配描述符集。
- 如果您从不打算更改绑定偏移量,请不要使用 DYNAMIC_OFFSET UBO/SSBO,因为处理动态偏移量会产生少量额外成本。
调试描述符集和布局性能问题
加快调试过程的方法:
- 监控管道布局中未使用的条目
- 内容相关的性能问题可以监控vkAllocateDescriptorSets()。
作者:lsh超棒棒棒
文章来源:知乎
推荐阅读
《Arm Mali GPU开发者最佳实战指南2.2》——优化应用逻辑
计算机体系结构学习资料汇总
深入GPU硬件架构及运行机制
更多Arm Mali GPU相关技术干货请关注Arm Mali GPU技术专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。