大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 Metal 优化 GPU 渲染器
了解如何使用最新的 Metal 功能和最佳实践来优化 GPU 渲染器。我们将向你展示如何使用函数特化和并行着色器编译来保持响应式创作工作流程和最快渲染速度,并帮助你调整计算着色器以获得最佳性能。
章节
- 0:00 - Intro
- 2:00 - Maximize shader performance
- 7:45 - Asynchronous compilation
- 10:10 - Fast runtime compilation
- 12:46 - Tune compiler options
- 16:10 - Wrap-Up
资源
相关视频
Tech Talks
WWDC22
WWDC21
-
下载
♪ ♪
大家好 我是 Gauri Jog Apple Metal Ecosystem 团队的一员 很高兴向大家介绍如何 通过 Metal 优化 GPU 渲染器
现代数字内容创建 App 和游戏引擎 让内容创作者能够 以交互的方式创建和修改 其 3D 资产的材质 在运行时处理这些复杂而动态的材质 有几种常见技术 一些 App 将材质编译为单独的着色器 而其他 App 则使用数据驱动的 解决方案 例如超级着色器 或着色器虚拟机 这些以材质为中心的 工作流程有两个主要性能目标 首先 材质的创作 应该响应迅速 以便进行快速迭代 和获得最佳体验 其次 渲染性能应尽可能优化 以实现实时交互 和高效的最终帧渲染
在这个 Blender 3D 的演示中 材质编辑具有响应性 当你在用户界面中修改材质滑块时 视口立即显示结果 而不会由于着色器的 重新编译而出现任何卡顿 修改材质后 随之而来的渲染性能 快速且具有交互性 使内容创作者能够 高效地查看他们工作的结果
为了在 App 中 实现高响应和高性能的工作流程 你可以充分利用 Metal 的关键功能并实施最佳实践 Metal 可以帮助你最大程度地 提升复杂着色器的性能 利用异步编译 来保持 App 的响应性 通过动态链接实现更快的编译速度 并使用 Metal 编译器的 新选项来调优计算着色器 优化你的着色器 是实现高性能的关键 超级着色器是一种 长而复杂的着色器 可以用于渲染任何可能的材质 这些类型的着色器包含许多分支 用于处理所有可能的组合 艺术家创建材质时 材质参数存储在 Metal 缓冲区中 并由材质着色器使用 你更改参数时 该缓冲区会更新 而无需重新编译
这种方法提供了 良好的响应性创作体验 然而 超级着色器并非最优选择 因为它们必须考虑到 所有可能的选项
为了获得最优化的着色器变体 你应使用有函数常量的 Metal 特化 只需在你的 Metal 着色器中声明函数常量 并在运行时根据需要设置其值 材质缓冲区内容 在着色器管线状态中成为常量 从而消除了动态分支 特化材质能够提供最佳性能 以下是 Blender 3D 中 两个常见测试资产 Wanderer 和 Tree Creature 的 实时性能比较 首先是使用超级着色器的 场景的基准性能 以每秒帧数表示 其次是采用了特化 着色器方法的性能 其使用了函数常量 性能更高 为了创建最快的特化着色器变体 使用函数常量 来禁用未使用的特性并消除分支
超级着色器会在运行时 从缓冲区查询材质参数 并执行条件分支以启用或禁用特性 使用函数常量 你可以为 每个材质特性声明一个常量 现在 特性代码路径的动态分支 被替换为函数常量 从而消除了所有未使用的代码 下面是同一个使用了 函数常量的超级着色器 Metal 编译器 可以将其折叠为常量布尔值 并移除未使用的代码 解析为 false 的 分支表达式将被优化掉 只留下 true 分支 所有未使用的控制流都被优化掉
特化着色器无需查询材质数据 控制流更简单 内存加载和分支已被移除 运行时性能更快
函数特化还有助于常量折叠 不变的材质参数被替换为常量 这个示例材质使用了来自 Metal 缓冲区的一组输入参数 如颜色、权重和光泽颜色等
在创建材质时 可以用函数常量 替换这些静态参数 函数常量生成最优化的代码 无需读取缓冲区 在主机端 创建特化管线状态时 通过设置常量值 来提供函数常量的值 可以使用 MaterialParameter 结构来表示 所有对于材质而言是常量的参数 IsGlossy 是一个 控制光泽度的布尔型 材质特性标志的示例 MaterialColor 是一个 用于描述颜色的向量参数的示例
要创建特化的 Pipeline State 对象 请遍历 MetalFunctionConstantValues 集合 并使用 setConstantValue 插入值
然后按照平常的方式创建渲染管线 唯一的区别是在创建片段函数时 使用带有 constantValues 参数的 newFunctionWithName 变体
最后 创建你的 Pipeline State 对象 生成的着色器是 该材质的最优化变体
请始终使用 Xcode 的 GPU 调试器性能部分 来验证使用函数常量的影响
原始的超级着色器展示了 大量的 ALU 指令和溢出现象 同时还存在大量的内存等待 内存等待的数量也相当可观
通过采用特化的方法 可以立即减轻 ALU 和溢出的压力 这是由于 死代码消除和常量折叠所致 此外 内存等待的数量显著减少
观察原始的超级着色器 在运行时的着色器执行开销 我们可以看到 GPU 在内存等待上花费了相当多的时间
而相比之下 采用特化方法 在内存等待方面花费的时间更少 从而实现了更高效的 ALU 利用率 和其他的效率优势
在 GPU 调试器的时间轴视图中 使用超级着色器渲染材质通道 需要 58 毫秒 而使用特化版本 仅需要 12.5 毫秒来完成渲染 这是一个非常显著的改进
在进行材质特化时 需要进行运行时的着色器编译 而如果你阻塞 并等待这些特化的材质被创建 可能会导致卡顿现象 Metal 的异步编译 API 允许你使用通用的超级着色器 并在后台生成特化的版本 同时保持用户体验的 互动性和响应性
为了选择异步的管道状态创建 请提供一个完成处理程序 这些调用会立即返回 从而使你能够 保持用户体验的互动性和响应性 当特化管道状态 准备就绪时 将调用完成处理程序 你可以立即切换到最优的着色器
这是一个异步材质工作流的示意图 默认情况下 当材质尚未特化时 你将使用通用的超级着色器 同时 Metal 在后台编译特化的着色器
一旦完成 你就可以立即将通用的超级着色器 替换为高效的特化材质
运行时 Metal 着色器编译 旨在提供平衡的并行性 然而 现代内容创作 App 需要提供多材质编辑工作流 因此需要频繁重新编译着色器 为了满足这种繁重的创作需求 你可以要求 Metal 最大化着色器编译的并行性 在 macOS 13.3 及更高版本中 Metal 设备引入了一个新的属性 即“should-Maximize-Concurrent-Compilation” 当你将其设置为“Yes”时 Metal 编译器 将充分利用你的 CPU 核心 最大化并行编译 对于多材质创作工作流非常有帮助 有了额外的编译作业可用 特化的材质变体 可以更快地准备就绪 下面是实际工作原理: 当材质参数发生更改时 当前的特化材质变体将失效 为保持创作流畅性 会切换回使用超级着色器 随后 将排队一个新的异步作业 一旦完成 你将立即感受到明显的性能改善 而特化的材质将被应用 由于现代 App 通常具有非常复杂的材质 准备特化变体 可能需要相当长的时间 在 Metal 中 可以使用动态库 预编译实用程序函数 以减少材质的整体编译时间 你可以通过 将功能组拆分为独立的动态库 来实现此目的 为了进一步加快运行时编译速度 实用程序库可以 在离线时进行预编译 这样 在运行时 需要编译的代码量将大大减少
如果我将之前的超级着色器 拆分为动态库 可以采用以下方法: 按照常见功能组进行拆分 例如一个数学实用程序库 和一个光照函数库
为了使函数符号可供链接器使用 你可以将其分配为“默认”可见性 此外 通过将可见性设置为隐藏 可以将符号“隐藏” 使其对外部程序不可见
如果你的 Metal 设备支持动态库 可以检查两个属性 来确认其支持情况 对于渲染管线 你应该使用 Metal 设备的 supportsRenderDynamicLibraries 属性 该属性适用于至少支持 Apple6 及 以上 GPU 系列的设备
对于计算管线 你可以查询 supportsDynamicLibraries 属性 该属性适用于 Apple6 及以上的 GPU 系列 以及大多数 Mac2 的 GPU 系列
要从现有的 Metal 库 创建动态库 只需调用 newDynamicLibrary 方法 并传递 Metal 库作为参数 如果要从 URL 创建动态库 可以调用 newDynamicLibraryWithURL 方法 并提供存储动态库的路径
你可以使用 Metal 编译工具链 离线预编译动态库 在运行时加载预编译的动态库时 完全避免了编译过程 在链接阶段指定 dylibs 时 可以将 Metal 动态库对象的数组 传递给管线描述符的 preloadedLibraries 参数 还可以通过 Metal 编译选项 在编译其他 着色器库时提供动态库数组 将大部分实用程序代码移入动态库 可以大大缩短运行时编译时间 在最终生产质量的渲染中 针对计算案例 如路径追踪 调优编译器选项非常重要 此外 还有一项额外的 Metal 特性 可提升最终渲染性能 Metal 编译器选项和占用提示 允许你针对 任何这些计算内核进行性能调优 尤其在使用动态链接时
每个 GPU 工作负载 都有其性能最佳状态 需要进行分析和评估 有一个 Metal API 可以设定 所需 GPU 占用水平 这种方法现在也适用于动态库 这可以提升现有工作负载的性能 而无需更改原始代码或算法 请注意 任何调优都应根据 GPU 架构 进行每个设备的性能特征分析 因为性能特征会有所不同
Metal 计算管线描述符的属性 允许你指定所需的占用级别 通过设置 Max-Total-Threads-Per-Threadgroup 值来实现 该值越高 你指示编译器达到的 占用率目标就越高 现在 通过动态库的 Metal-Compile-Options 属性 你可以与管线状态对象的 所需占用级别进行匹配 Max-Total-Threads-Per-Threadgroup 特性适用于 iOS 16.4 和 macOS 13.3 可用于MetalCompileOptions
你可以轻松匹配管线 状态对象的目标占有率 同时调优 Metal 动态库 实现最佳性能
这张 Blender Cycles 着色和交叉计算内核 性能图表展示了 Max-Total-Threads-Per-Threadgroup 的变化如何影响性能 那是唯一针对管道状态对象和 动态链接库改变了的变量 在这种情况下 存在一个 最佳点 即内核表现最佳的状态 每个工作负载 和设备都是独一无二的 而 Max-Total-Threads-Per-Threadgroup 的最佳值 则因内核的性质而异 最佳值并非始终是 GPU 支持的 线程组中的最大线程数 通过调整内核 寻找并确定最佳值 并将其嵌入代码中 这是一个 Blender Cycles 着色内核示例 编译器统计数据 表明该内核非常复杂 有几个参数会影响实际运行时间 包括溢出量、使用的寄存器数量 以及内存加载等其他操作 通过调整 Max-Total-Threads-Per-Threadgroup 你可以改变目标占用率 并找到性能的最佳点
一旦找到最佳点 溢出量会稍微增加 但总体占用率的提高 会显著提升内核性能
Blender 3D 3.5 中的 Cycles 路径追踪器 已经在 Metal 上进行了优化 并采用了所有今天介绍的最佳实践
请记住 通过函数特化来最大化 大型且复杂的着色器性能 使用异步编译 保持 App 的响应性 并在后台生成优化的着色器 启用动态链接 以实现运行时更快的编译 并使用新的 Metal 编译器 选项来调优计算内核 以获得最佳性能 请查看之前的讲座 了解如何 为 Apple GPU 扩展计算工作负载 并在 Metal 中 发现更多的编译工作流程 感谢观看 ♪ ♪
-
-
3:45 - Reduce Branch Performance Cost
// Reduce branch performance cost fragment FragOut frag_material_main(device Material &material [[buffer(0)]]) { if(material.is_glossy) { material_glossy(material); } if(material.has_shadows) { light_shadows(material); } if(material.has_reflections) { trace_reflections(material); } if(material.is_volumetric) { output_volume_parameters(material); } return output_material(); }
-
3:55 - Function constant declaration per material feature
constant bool IsGlossy [[function_constant(0)]]; constant bool HasShadows [[function_constant(1)]]; constant bool HasReflections [[function_constant(2)]]; constant bool IsVolumetric [[function_constant(3)]];
-
3:59 - Dynamic branch for the feature codepath is replaced with function constants
if(material.has_reflections) { trace_reflections(material); }
-
4:05 - Dynamic branch for the feature codepath is replaced with function constants
/* replaced with function constants*/ if(HasReflections) { trace_reflections(material); }
-
4:13 - Reduce branch performance cost with function constants
constant bool IsGlossy [[function_constant(0)]]; constant bool HasShadows [[function_constant(1)]]; constant bool HasReflections [[function_constant(2)]]; constant bool IsVolumetric [[function_constant(3)]]; // Reduce branch performance cost fragment FragOut frag_material_main(device Material &material [[buffer(0)]]) { if(IsGlossy) { material_glossy(material); } if(HasShadows) { light_shadows(material); } if(HasReflections) { trace_reflections(material); } if(IsVolumetric) { output_volume_parameters(material); } return output_material(); }
-
4:58 - Function constants for material parameters
// Function constants for material parameters constant float4 MaterialColor [[function_constant(0)]]; constant float4 MaterialWeight [[function_constant(1)]]; constant float4 SheenColor [[function_constant(2)]]; constant float4 SheenFactor [[function_constant(3)]]; struct Material { float4 blend_factor; }; void material_glossy(const constant Material& material) { float4 light, sheen; light = glossy_eval(MaterialColor, MaterialWeight); sheen = sheen_eval(SheenColor, SheenFactor); glossy_output_write(light, sheen, material.blend_factor); }
-
5:21 - MaterialParameter structure for constant parameters
struct MaterialParameter { NSString* name; MTLDataType type; void* value_ptr; }; MaterialParameter is_glossy{@"IsGlossy", MTLDataTypeBool, &material.is_glossy}; MaterialParameter mat_color{@"MaterialColor", MTLDataTypeFloat4, &material.color};
-
5:51 - Declare and populate MTLFunctionConstantValues
// Declare and populate MTLFunctionConstantValues MTLFunctionConstantValues* values = [MTLFunctionConstantValues new]; for(const MaterialParameter& parameter : shader_parameters) { [values setConstantValue: parameter.value_ptr type: parameter.type withName: parameter.name]; }
-
5:51 - Create pipeline render state object with function constant declarations
struct Material { bool is_glossy; float color[4]; }; struct MaterialParameter { NSString* name; MTLDataType type; void* value_ptr; }; // Declare material Material material = {true, {1.0f,0.0f,0.0f,1.0f}}; // Declare function constant paramters MaterialParameter is_glossy{@"IsGlossy", MTLDataTypeBool, &material.is_glossy}; MaterialParameter mat_color{@"MaterialColor", MTLDataTypeFloat4, &material.color}; MaterialParameter shader_parameters[2] = {is_glossy, mat_color}; // Declare and populate MTLFunctionConstantValues MTLFunctionConstantValues* values = [MTLFunctionConstantValues new]; for(const MaterialParameter& parameter : shader_parameters) { [values setConstantValue: parameter.value_ptr type: parameter.type withName: parameter.name]; } // Create MTLRenderPipelineDescriptor and create shader function from MTLLibrary MTLRenderPipelineDescriptor *dsc = [MTLRenderPipelineDescriptor new]; NSError* error = nil; dsc.fragmentFunction = [shader_library newFunctionWithName:@"frag_material_main" constantValues:values error:&error]; // Create pipeline render state object id<MTLRenderPipelineState> pso = [device newRenderPipelineStateWithDescriptor:dsc error:&error];
-
6:14 - Create MTLRenderPipelineDescriptor and create shader function from MTLLibrary
// Create MTLRenderPipelineDescriptor and create shader function from MTLLibrary MTLRenderPipelineDescriptor *dsc = [MTLRenderPipelineDescriptor new]; NSError* error = nil; dsc.fragmentFunction = [shader_library newFunctionWithName:@"frag_material_main" constantValues:values error:&error];
-
8:07 - Shader library creation
- (void)newLibraryWithSource:(NSString *)source options:(MTLCompileOptions *)options completionHandler:(MTLNewLibraryCompletionHandler)completionHandler;
-
8:09 - Render pipeline state creation
- (void)newRenderPipelineStateWithDescriptor:(MTLRenderPipelineDescriptor *)descriptor completionHandler:(MTLNewRenderPipelineStateCompletionHandler)completionHandler;
-
9:00 - Use as many threads as possible for concurrent compilation
@property (atomic) BOOL shouldMaximizeConcurrentCompilation;
-
10:58 - Assign symbol visibility to default or hidden
__attribute__((visibility(“default"))) void matrix_mul(); __attribute__((visibility(“hidden"))) void matrix_mul_internal();
-
11:19 - Verify device support
//For render pipelines @property (readonly) BOOL supportsRenderDynamicLibraries; //For compute pipelines @property(readonly) BOOL supportsDynamicLibraries;
-
11:46 - Compile dynamic libraries
//create a dynamic library from an existing Metal library - (id<MTLDynamicLibrary>) newDynamicLibrary:(id<MTLLibrary>) library error:(NSError **) error //create from the URL - (id<MTLDynamicLibrary>) newDynamicLibraryWithURL:(NSURL *) url error:(NSError **) error
-
12:18 - Dynamically link shaders
//Pipeline state MTLRenderPipelineDescriptor* dsc = [MTLRenderPipelineDescriptor new]; dsc.vertexPreloadedLibraries = @[dylib_Math, dylib_Shadows]; dsc.fragmentPreloadedLibraries = @[dylib_Math, dylib_Shadows]; //Compile options MTLCompileOptions* options = [MTLCompileOptions new]; options.libraries = @[dylib_Math, dylib_Shadows]; [device newLibraryWithSource:programString options:options error:&error];
-
13:45 - Specify desired max total threads per threadgroup
@interface MTLComputePipelineDescriptor : NSObject @property (readwrite, nonatomic) NSUInteger maxTotalThreadsPerThreadgroup;
-
14:12 - Match desired max total threads per threadgroup
@interface MTLCompileOptions : NSObject @property (readwrite, nonatomic) NSUInteger maxTotalThreadsPerThreadgroup;
-
14:25 - Tune Metal dynamic libraries
MTLCompileOptions* options = [MTLCompileOptions new]; options.libraryType = MTLLibraryTypeDynamic; options.installName = @"executable_path/dylib_Math.metallib"; if(@available(macOS 13.3, *)) { options.maxTotalThreadsPerThreadgroup = 768; } id<MTLLibrary> lib = [device newLibraryWithSource:programString options:options error:&error]; id<MTLDynamicLibrary> dynamicLib = [device newDynamicLibrary:lib error:&error];
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。