一个高效运行的 app 应该满足以下几个要求:

  1. 低 CPU 开销
  2. 优化 GPU 性能
  3. 最大化 CPU 和 GPU 的并行处理
  4. 有效的资源管理

资源管理

在 Metal 框架里,许多对象的生命周期可能贯穿整个 app 的始终,因此这些对象被称为持久对象。创建这些对象开销很大,所以应该尽早创建并重用。持久对象主要包括:

  • MTLDevice
  • MTLCommandQueue
  • MTLLibrary
  • MTLRenderPipelineState / MTLComputePipelineState
  • MTLBuffer / MTLTexture

如果需要配置多个渲染或计算管道,那么应该尽可能重用 MTLFuntion。此外,尽管 MTLDepthStencilStateMTLSamplerState 的创建开销不是很大,但这两个对象仍然应该仅创建一次并重用。

通常一个 app 里会用到多个 MTLBufferMTLTexture,而这两个对象的创建开销也是很大的,所以,一种可行的解决方案就是使用缓冲池。当使用缓冲池管理 MTLBuffer 的时候需要考虑到此时 MTLBuffer 已经成为一种竞态资源,你应当合理使用锁或者信号量机制来保证资源的获取与回收。关于这个话题可以参考 Raywenderlich 的一篇博客

MTLBufferMTLTexture 都遵循 MTLResource 协议。对于 MTLResource 来说,你应该合理配置资源的一些可选项来最大化利用 CPU/GPU 的快速内存访问和驱动程序的性能优化。主要有两个设置:

  • MTLStorageMode
  • MTLResourceOption

MTLStorageMode 用于设置资源的存储位置和访问权限,对于 iOS 来说,默认使用的是 MTLStorageMode.shared 模式。在这种模式下,MTLTexture 存储在 CPU 和 GPU 共享使用的内存中。然而这种共享是有限制的:为了减少 CPU 和 GPU 的缓存刷新次数,系统仅确保在 command buffer 的调用边界内维持 CPU 和 GPU 的这种关联性。

MTLStorageMode 影响 MTLResource 的存储,而 MTLResourceOption 则影响 MTLResource 的创建。

iOS 系统使用统一内存模型,也就是说,对于 iOS 设备,CPU 和 GPU 共享系统内存。

以上可以用一幅图来直观的感受一下:

需要注意的是:

在 macOS 中,MTLStorageMode.shared 模式仅支持 MTLBuffer,不支持 MTLTexture。这是由于 MTLBuffer 通常存储线性数据,GPU 访问比较简单,而 MTLTexture 则更复杂,增加了 GPU 读取的复杂度。

将数据绑定到着色器函数上时应该根据数据的大小选择合适的方法。以顶点着色器(vertex shader)为例:

  • 数据小于 4KB 时,使用 setVertexBytes(_ bytes:, length:, index:) 方法
  • 如果一块 buffer 的数据全部用于某个着色器函数的某个特定索引参数时,使用 setVertexBuffer(_ buffer:, offset:, index:)
  • 如果一块 buffer 的数据用在多个 draw call 上,使用 setVertexBufferOffset(_ offset:, index:)

显示管理

如果要将 Metal 渲染的内容绘制到屏幕上,通常遵循着这样一条显示链:

CAMetalLayer -> CAMetalDrawable -> MTLTexture

也就是说,Metal 最终会将渲染的内容输出到一个 MTLTexture 上用于显示。然而我们不用手动操控 MTLTexture 的内容,只需要 MTLCommandBuffer 调用 commit 方法前调用 present(_ drawable:) 方法即可。尽管如此,只有当一个 MTLCommandBuffer 被调度并执行完一系列绘制指令后,渲染的内容才会真正的 present 到 drawable 上,由于 CAMetalDrawable 的创建开销较大,如果过早尝试获取一个 CAMetalDrawable 对象可能会导致线程阻塞,因此,你应该尽可能短的持有 CAMetalDrawable 对象,并使用 MTLCommandBufferpresent(_ drawable:) 方法而不是直接使用 CAMetalDrawablepresent 方法来显示渲染内容。(实际上,MTLCommandBufferpresent(_ drawable:) 方法只是一个便利方法,它会通过执行 addScheduledHandler(_ block:) 方法在指令执行完成时调用 CAMetalDrawablepresent 方法来显示渲染内容。)遵循以下两个步骤可以帮助我们实现「尽可能短地持有 CAMetalDrawable 对象」这个目标:

  1. 在 CPU 更新完缓冲数据和离屏渲染之后,MTLCommandBuffer 执行屏幕渲染指令之前
  2. 绘制一帧画面的 CPU 指令执行完成之后立即释放 CAMetalDrawable 对象。最好使用一个 autoreleasepool 来包装这个渲染步骤,这可以有效避免多次渲染引起的死锁

一幅图表示一个 CAMetalDrawable 的生命周期:

iOS 9 发布后,Metal 也增加了一个新的框架:MetalKit。它提供的 MTKView 可以更加方便的用于渲染内容的显示。所以应该尽可能使用 MTKView 而不是直接使用 CAMetalLayer。同时,它也自动支持 nativeScale 特性,这可以避免额外的渲染和重采样操作,提高效率。如果你使用 UIViewCAMetalLayer 自己实现类似的功能,那么你需要设置 contentScaleFactor 属性来匹配 nativeScale,并且在视图尺寸变化时做出相应变化。

一般来说,app 的视图刷新率都是以 60 fps 为目标。然而如果你的 app CPU 负载过高,不能达到 60 fps 时,你应该降低视图的刷新率到一个恒定的帧率来避免画面抖动。比如说,对游戏来说,30 fps 是一个可接受的刷新率。推荐使用 MTKView,设置它的 preferredFramesPerSecond 属性来调整视图刷新率。

假如说你设置了你的 MTKView 刷新率为 30 fps,由于屏幕的刷新率(跟视图刷新率不一样)为 60 fps,当你使用 present(_ drawable:) 来显示内容时,有可能这个方法调用的时候视图内容还没有渲染完成,因为这个方法的调用间隔以屏幕刷新率为基准。此时,你应该使用 present(_ drawable:, atTime:)present(_ drawable:, afterMinimumDuration)

命令生成

LoadAction 表示渲染指令执行之前的行为,StoreAction 表示渲染指令完成后的行为。合理的选择不同的行为指令可以有效地避免不必要的开销。

LoadAction 的选择可以用下表表示:

保存前一帧渲染内容 渲染目标写入范围 LoadAction
N/A 全部 .dontCare
No 部分 .clear
Yes 部分 .load

.clear 选项会对渲染目标用一个指定颜色填充,.load 会加载之前渲染的内容,因此这两种选项都会有额外的开销。

对于 StoreAction,这部分不是特别理解,以后补充。

为了提高性能,应该尽量减少不必要的渲染指令编码器(render command encoder),如果可能的话,可以通过合并多个 RCE 的方式来达成这种目的。但是两个 RCE 是否能够兼容取决于他们的渲染目标、load 和 store 行为指令、其他依赖。比如说有两个渲染指令编码器 RCE1RCE 2:

  • RCE1 和 RCE2 是为渲染同一帧而创建
  • RCE1 和 RCE2 是由同一个 command buffer 创建
  • 如果 RCE1 先于 RCE2 创建,则 RCE2 共享 RCE1 的渲染目标,且 RCE2 不从 RCE1 的渲染目标里采样
  • RCE1 的渲染目标的 store 行为指令为 .store.dontCare,RCE2 的渲染目标的 load 行为指令为 .load.dontCare
  • RCE1 和 RCE2 之间没有创建其他 RCE

满足上面的要求的话,RCE1RCE2 就可以合并创建一个 RCEM,如图所示:

此外,如果 RCE1 能跟在它之前创建的 RCE0 合并,RCE2 能跟在它之后创建的 RCE3 合并,那么这四个 RCE 可以合并为一个。

在 Metal 里,一个 command buffer 是提交给 GPU 执行指令的最小单元,它由 CPU 创建并提交给 GPU 执行,尽管可以实现一个缓冲池来保持 CPU 处理工作一直先于 GPU 并让 GPU 一直处于工作状态,但如果 CPU 负载过大,仍然有可能导致 GPU 得不到足够的指令执行而空转,因此应该尽量减少创建和提交 command buffer

编译

主要就是尽量提前编译着色器代码,比如构建 app 时、使用 Metal 工具将着色器代码提前编译成 framework、合并多个着色器代码。


以上是我学习 Metal Best Practices Guide 而做的一个简陋的笔记,如有理解错误的地方还望指出。