Metal 3 GPU 二进制文件的优化
Target and optimize GPU binaries with Metal 3
2022年6月6日
一句话判断
Metal 3 终于让你控制 shader 编译的时机和产物——Pipeline State Binary 和 Dynamic Library 把”运行时编译 shader”的延迟消灭在了上架之前。
这场 Session 讲了什么
这场 Session 讲了 Metal 3 中关于 GPU 二进制文件(shader 编译产物)的新能力。之前 Metal 的 shader 编译流程有一个痛点:第一次创建 MTLRenderPipelineState 时需要编译 shader,这个过程可能耗时几十到几百毫秒,造成画面卡顿。Metal 3 给了两个解决方案。
Pipeline State Binary。 这是今年最重要的能力之一。你可以把编译好的 pipeline state 序列化到磁盘上,下次 App 启动时直接从文件加载,跳过编译步骤。更关键的是,你可以在 Xcode 构建阶段就生成 pipeline state binary——这意味着用户拿到 App 的时候,shader 已经是编译好的状态了。首次启动不再有编译卡顿。
Dynamic Library。 Metal 3 引入了 Metal Dynamic Library(.metallib 文件),可以让你把 shader 代码组织成可复用的模块。之前每个 pipeline state 都要包含完整的 shader 代码,即使多个 pipeline 共享同一段代码。现在你可以把公共代码提取到 dynamic library 里,多个 pipeline 引用同一个 library。这不仅减少了二进制大小,还加快了编译速度。
Binary Archive。 MTLBinaryArchive 是 pipeline state binary 的容器。你可以把多个 pipeline state 的编译产物存到一个 archive 里。在开发阶段,你可以用 Xcode 的 Metal Pipeline State Tool 来预编译所有 pipeline state 并生成 archive。Archive 可以嵌入 App bundle 里,App 启动时直接加载。
Pipeline State 的延迟创建。 Metal 3 允许你异步创建 pipeline state——device.makeRenderPipelineState(descriptor:completionHandler:) 现在会在后台线程编译 shader,不会阻塞渲染线程。配合 binary archive,大部分 shader 根本不需要编译,直接从 archive 加载。
交叉编译和兼容性。 Pipeline state binary 是 GPU 架构相关的。如果你在 M1 上生成的 binary,在 M2 上可能无法使用(因为 GPU 微架构不同)。Metal 3 的 binary archive 支持同时存储多个 GPU 架构的编译产物。App 启动时根据当前设备的 GPU 架构选择合适的 binary。
值得深挖的点
构建时 vs 运行时生成 binary archive。 构建时生成(Xcode Build Phase)是最理想的——用户永远不需要等待编译。但这需要你准确地列出所有可能的 pipeline state descriptor。如果你的 App 有动态生成 pipeline state 的逻辑(比如根据用户设置组合不同的 shader),构建时就无法覆盖所有情况。一个折中方案是:构建时覆盖 80% 的常见 pipeline,运行时对剩余的做异步编译并追加到 archive 里。
Dynamic Library 的模块化设计。 Metal Dynamic Library 让 shader 代码的组织方式从”每个 pipeline 自包含”变成了”模块化复用”。你可以把常用的工具函数(色调映射、噪声生成、BRDF 计算)提取到一个 shared library 里,多个 pipeline 引用它。但要注意:dynamic library 一旦加载到 pipeline 里,它的符号就和 pipeline 绑定了。如果你更新了 library 的实现但没有更新引用它的 pipeline,pipeline 仍然使用旧版本的代码。
代码片段
创建和加载 Binary Archive:
// 从 App bundle 加载预编译的 archive
let archiveURL = Bundle.main.url(forResource: "MyPipelineArchive",
withExtension: "binarystorage")!
let binaryArchive = try device.makeBinaryArchive(url: archiveURL)
// 用 archive 创建 pipeline state(不需要编译!)
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
// ... 配置其他属性
// 指定 binary archive 作为编译产物来源
pipelineDescriptor.binaryArchives = [binaryArchive]
// 创建 pipeline state(从 archive 加载,不触发编译)
let pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
创建 Metal Dynamic Library:
// 从 metallib 文件创建 dynamic library
let libraryURL = Bundle.main.url(forResource: "CommonShaders",
withExtension: "metallib")!
let dynamicLibrary = try device.makeDynamicLibrary(url: libraryURL)
// 在 pipeline 中引用 dynamic library 的函数
let descriptor = MTLRenderPipelineDescriptor()
// 函数名格式:library_name::function_name
let functionDescriptor = MTLFunctionDescriptor()
functionDescriptor.name = "toneMap"
functionDescriptor.dynamicLibrary = dynamicLibrary
let fragmentFunction = try device.makeFunction(descriptor: functionDescriptor)
descriptor.fragmentFunction = fragmentFunction
在 Xcode 构建阶段生成 binary archive:
# 在 Build Phase 里添加 Run Script
# 使用 Metal Pipeline State Tool 预编译
xcrun metalpipelinearchive \
-descriptor-list pipeline_descriptors.json \
-output "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/PipelineArchive.binarystorage"
# pipeline_descriptors.json 示例
# [
# {
# "vertex_function": "vertex_main",
# "fragment_function": "fragment_main",
# "color_attachments": [{"pixel_format": "bgra8_unorm"}]
# }
# ]
最佳实践
在 Xcode 构建阶段生成 binary archive,覆盖最常见的 pipeline state 组合。在 App 启动时尝试加载 archive——如果 archive 不存在(比如系统升级导致 archive 不兼容),退回到异步编译模式。
使用 dynamic library 来减少 shader 代码重复。把公共的数学函数、光照计算、色调映射提取到 shared library。每个 pipeline 只需要包含自己特有的逻辑。这不仅减少 App 二进制大小,也加快了 pipeline state 的创建速度。
不要假设 binary archive 一定能加载成功。不同 GPU 架构、不同操作系统版本可能需要不同的编译产物。你的代码需要有 fallback 路径——archive 加载失败时走异步编译。用 MTLBinaryArchive.addRenderPipelineFunctionsWithDescriptor: 在运行时把新编译的 pipeline 追加到 archive 里,下次启动就能直接用了。
还有什么值得关注
- Binary archive 文件大小取决于 pipeline state 的复杂度和 GPU 架构数量。如果你的 App 支持多个 GPU 架构(M1、M2、M3),archive 可能会比较大。
- Xcode 14 的 Instruments 有新的 Metal Pipeline State 工具,可以可视化你的 pipeline state 创建耗时和缓存命中率。
- Dynamic library 的函数在 Metal Shading Language 里用
[[export]]属性标记,只有标记了export的函数才能被其他 library 和 pipeline 引用。 - 如果你的 shader 代码用了
#include,Metal 3 现在支持在 metallib 里嵌入 include 依赖,不需要在运行时提供 include 路径。