大多数浏览器和
Developer App 均支持流媒体播放。
-
了解提升 Metal 着色器性能的最佳实践
探索如何使用 Apple GPU 的一些最新改进来提升 Metal 着色器性能。了解通过配置函数常量来减少着色器的执行时间,并研究通过函数组优化编译器的方法。掌握如何通过提高着色器的执行和并行使用资源的能力来节省运行时间。探索 Apple 系列 9 GPU 功能并利用硬件加速进行光线追踪。
资源
相关视频
WWDC23
Tech Talks
WWDC20
-
下载
大家好 我叫 Srividya Karumuri 是 Apple GPU 编译器工程师 今天 我在这里分享一些 可以提高 Metal 着色器性能的技巧
M3 和 A17 Pro 中的 全新 Apple 系列 9 GPU 有一些新的改进 供你用于 App 开发 我对 Apple 系列 9 GPU 有一些建议 你也可以参考适用于每代 Apple GPU 的指导技巧和窍门
你可以通过使用 Metal 着色语言中的功能 减少运行时间 以提高着色器的性能 通过提高着色器 资源利用率 来提高并行性
并充分利用 Apple 系列 9 GPU 中的光线追踪加速硬件
Metal 具有多种功能 可以有效缩短着色器的运行时间 包括可以有效地专门化 着色器的函数常量 以及可以使用间接函数调用 优化着色器的函数组
Metal 函数常量 可有效地专门化着色器 消除运行时无法执行到的代码 例如超级着色器通常 受益于函数常量
超级着色器通常很复杂 因为它可以在运行时处理 许多不同的可能性 例如在 3D 应用程序中 渲染不同的材质类型
开发者有时会制作超级着色器 从缓冲区读取材质参数 然后材质着色器在运行时 根据缓冲区的内容 选择不同的控制部分
这种方法让着色器无需重新编译 即可渲染新的材质效果 因为唯一更改的 是缓冲区中的参数
例如 管道中的这个超级着色器 渲染光泽材质 因为绘制命令中的 Metal 缓冲区 is_glossy 参数等于 true 当缓冲区的 is_glossy 参数等于 false 时 同一着色器会 渲染哑光材质
两种材质效果的渲染管道是相同的 因为行为变化来自缓冲区中的内容
这种响应式方法在开发过程中很有用 但着色器必须考虑多种可能性 并从额外的缓冲区读取 这可能会影响 App 性能 另一种方法是在编译时 而不是运行时专门化着色器 通过使用预处理器宏 离线构建着色器变体
这是一个专门使用宏的 超级着色器
每个专用着色器都有 自己的渲染管道 并且仅具有渲染 特定材质效果所需的代码
这种方法意味着你必须离线编译 所有可能的变体组合 例如 光泽变体可以是启用 is_glossy 和 use_shadows 宏的组合 通过禁用其余宏来实现
同样 遮罩函数变体可以是 use_shadows 和 has_reflections 宏的组合
光泽反射变体启用 is_glossy 和 has_reflections 宏等等
使用宏实现超级着色器 可能意味着要编译大量变体 例如为每种可能的宏组合 编译一个变体 其中一些你的 App 可能永远不会使用
即使你提前离线编译它们 每个变体的累加也会显着增加 Metal 库的大小 它还会增加编译时间 因为每种变体都 必须从 Metal 源码开始编译
函数常量可以提供另一种方式 来专门化着色器 与使用宏相比 它可以减少编译时间 和 Metal 库的大小 借助函数常量 你可以将超级着色器从源代码 一次性编译为中间 Metal 函数 从该函数中 你只需根据你定义的函数常量 创建 App 所需的变体
函数常量让你能够灵活地 根据需要随时 创建多个专用变体 并为所有其余的可能性 重用中间 Metal 函数
通过这种方法你可以仅创建所需的 着色器变体和渲染管道 来节省时间和空间
创建 Metal 函数时 通过在 Metal 函数代码中 声明函数常量 你可以创建这些专门的变体 然后分别定义它们的值
你还可以使用函数常量 初始化你在常量地址空间中 声明的程序范围变量
你可以使用这些函数常量 在着色器中启用不同的代码部分 而不是从 Metal 缓冲区读取值
通过函数常数 Metal 在编译着色器的 专业化变体时 可以将其折叠为常量布尔值
以及其他优化 例如消除 无法执行到的代码部分
这可以删除未使用的控制流
通过使用函数常量专门化着色器 你不再需要从缓冲区 查询材料参数 这种方法通过简化其控制流程 减少了着色器的运行时间 并删除未使用的代码部分
建议你观看《使用 Metal 优化 GPU 渲染》 其中详细介绍了如何在运行时 设置函数常量值 它还介绍了如何 通过同步编译来 缓解运行时编译开销
你还可以通过添加 用于间接函数调用的函数组属性 来减少着色器运行时间
间接函数是一种 不直接通过名称调用的着色器函数 例如使用函数指针或可见函数表调用
着色器可以通过静态 或动态链接调用间接函数 间接函数调用使代码可扩展 并为你的 App 提供更多灵活性选项 但是 间接函数调用可能会 妨碍 Metal 充分的优化着色器 尤其是在调用点附近
对于静态链接的函数 你可以使用 Metal 函数组的功能 让 Metal 对通过间接函数调用 的着色器也进行优化
这个着色器调用三个不同的间接函数 包括通过函数指针调用光照 和材质 Metal 无法跨越这些函数指针 调用点进行优化 因为它不知道 着色器正在调用哪些函数 但当你了解 函数指针只能指向 特定函数组中的一个时 你可以使用该函数组的属性 例如 着色器可以调用的唯一函数 是着色器管道状态中 所有链接的函数
并且你可能知道 lighting 函数只能调用 area、spot 或 sphere 函数 在这种情况下 你可以将这些函数 组合到 lighting 函数组中 同样 如果 material 函数指针只能调用 wood、glass 或 metal 函数 那你可以将它们组合到 material 函数组中
你可以在调用点 添加函数组的属性来向 Metal 提示 如何优化间接调用
你可以将字典分配给 链接函数组的属性来定义函数组 每个字典条目都是一个字符串键 它是函数组的名称 值是属于该组的函数数组 请注意 此方法仅有助于 静态链接的函数 编译到二进制库的 函数将不会从中受益
观看这两个视频 了解有关 Metal 函数指针 和各种编译工作流程的更多信息
总之 减少着色器运行时间的两种方法如下 一是函数常量 它可以有效地创建 着色器的专用变体 二是函数组 它可以在调用间接函数时 优化着色器
了解过一些可以减少着色器 执行时间的 Metal 功能后 我们来看看一些可提高资源利用率 并提高并行性的方法
对于消除着色器执行中的 潜在延迟而言 增加线程占用率非常重要
线程占用率实际上取决于 可用资源的数量 无论是寄存器还是内存
因此优化着色器数据的使用 可以增加线程占用率
Apple 系列 9 GPU 在占用管理方面 得到改进 如需了解更多详情 请查看 《探索 M3 和 A17 Pro 中 的 GPU 改进》
要了解如何分类 较低的线程占用瓶颈 请查看 《探索适用于 M3 和 A17 Pro 的新 Metal 剖析工具》
内存对象的地址空间 和 ALU 运算中使用的数据类型 会影响资源利用率
为内存对象选择正确的地址空间 对于提升内存利用率 和线程占用率 非常重要
在 Metal 着色语言中 地址空间被设计为 支持不同的访问模式
并指定分配内存对象的 内存区域
选择正确的地址空间将直接影响 着色器的性能 我们将集中关注常量 设备 和线程组地址空间 常量地址空间让你 创建只读的内存对象
这些访问针对数据进行了优化 此类数据在所有线程软件 分派或绘制中都是不变的
如果对象的大小是固定的 并且不同线程多次读取该对象 则在常量地址空间中创建这些对象
你可以在设备地址空间中 创建读/写缓冲区 如果正在访问的数据在线程之间变化 或者缓冲区的大小不固定 则你可以在设备地址空间中 创建此类缓冲区
查看《为搭载 Apple 芯片的 Mac 优化 Metal 性能》 了解有关常量的更多详细信息 以及设备地址空间建议和示例
线程组地址空间 也适用于读/写内存对象 线程组中的线程可以通过 共享线程组内存中的数据一起工作
在大多数情况下它们通常更快
在某些用例中 线程组内存被用作 软件管理的设备缓存或常量缓冲区 例如 将设备内存块复制 到线程组内存进行操作 在某些情况下这可能会更快
随着 Apple 系列 9 GPU 着色器代码内存的新改进 何时使用 线程组内存的权衡 可能与以前的 GPU 有所不同
在着色器中 如果线程组内存的使用 主要用作设备的软件管理缓存 或常量缓冲区 那么直接读取这些缓冲区 可能比复制到线程组内存 有更好的性能
借助 Apple 系列 9 GPU 动态着色器核心内存 和灵活的片上内存功能 线程组设备常量内存类型使用 相同的缓存层次结构 因此 如果你的工作集大小适合缓存 那么缓冲区和线程组内存访问 可能具有相似的性能特征 在这些情况下 着色器可以仅使用 设备或常量缓冲区进行操作 而非在线程组和设备 或常量地址空间中 创建内存副本 并避免了复制到线程组内存 所涉及的延迟
关于将数据仅保留在设备 或常量缓冲区中 是否有益的其他指导 可以使用 Xcode 中的 Metal 调试器 剖析工作负载来评估
与基于地址的选择类似 数据类型也会影响性能 例如 16 位数据类型有助于减少 寄存器和内存占用
尽可能使用 16 位数据类型 可以获得更好的性能 例如使用 half 和 short 而非 float 和 int 转换无需开销 因此不必担心 类型之间的转换 例如 half 和 float 之间的转换 Bfloat 是 float 的 16 位缩减版本
最适合加速机器学习应用程序
它以低精度允许宽泛的值域
自 Metal 3.1 起 bfloat 数据类型 就得到支持 如果你的应用程序的精度要求 与 bfloat 支持的精度要求相匹配 强烈建议使用此数据类型
使用 16 位数据类型 而不是 32 位数据类型 会导致着色器使用更少的寄存器 如果该数据存储在内存中 还有助于减少内存占用 并改善带宽 因此可以带来更高的线程占用率 使用 16 位数据类型 还可以提高能源效率
当编写以 half 精度计算的 表达式时 请确保 在任何文字量上使用 h 后缀 否则整个表达式将以浮点精度 进行计算 并失去 使用较小类型的优势
在某些着色器中 通过使用 half 类型 可以实现更好的指令混合 例如混合使用 float half 和 int 类型指令 这可以更好地利用 Apple 系列 9 GPU 中的 ALU 管道 并且可以提高指令吞吐量
总而言之 根据内存使用模式 选择正确的地址空间 可以提高资源利用率 选择 16 位数据类型有助于 减少寄存器 和内存占用 在某些情况下它可以 更好地利用 Apple 系列 9 GPU 中的 ALU 并行性 对于光线追踪着色器来说 至关重要的是 减少着色器执行时间 并提高资源利用率 以便提高性能
要使用 Metal 光线追踪进行渲染 第一步 是定义场景几何体 并构建加速结构 以实现高效相交
相交是通过创建光线的 GPU 函数执行的 该 GPU 函数使一个相交器对象 执行相交 从相交返回的结果 将包含你对像素 进行着色或进一步处理 可能需要的所有信息
此过程的相交器组件 在 Apple 系列 9 GPU 上 由硬件加速
硬件相交器负责 遍历加速结构 调用相交函数 并根据相交的结果 更新遍历的状态
相交器是 Metal 光线追踪的 基础 API 通过优化方式使用相交函数 光线负载 相交标记以及相交检测 可以提高光线追踪性能
自定义相交函数是定义 光线如何撞击表面的有力工具 但仅在必要时才使用自定义相交函数
自定义相交函数对于 实现 Alpha 测试等功能非常重要 Alpha 测试用于向场景添加更多 几何细节 例如该图像中的链条和树叶 通过使用自定义相交函数来 实施 Alpha 测试
当光线遍历加速结构时 自定义相交函数内部的逻辑 负责接受或拒绝相交
在本例中 自定义相交函数逻辑 将拒绝第一个相交
但它会接受第二个相交 因为不透明表面已相交
自定义相交函数可以启用额外的逻辑 以在着色器调用上执行 仅在必要时使用它 不透明三角形相交器是最快的路径
如果你需要自定义相交函数 需要注意的是 硬件将按相交函数 进行排序和分组 使用很多的相交函数 会使 查找匹配项和分组变得更加困难
因此请避免重复的相交函数 以实现最佳分组
此外 利用 Metal 相交函数 表索引机制来创建 每个函数一个条目的简表
为了运行相交测试 硬件相交 为多条光线创建 SIMD 组 然后针对多个 图元并行测试每条光线
由于自定义相交函数并行运行 因此如果它们执行 任何有副作用的操作 都会需要将其序列化 这包括对负载 或其他设备内存的内存写入 同样 任何引入分支的操作 (例如间接函数调用) 也会降低 相交函数执行的并行性
最好在相交函数中 尽可能晚地执行这些操作 以在该点之前实现最大并行度
在此示例中 首先更新光线负载 然后执行一些与负载更新 无关的工作
这将导致负载更新后的 所有代码串行运行 相反 你可以修改相交函数 以先完成与负载更新 无关的所有工作 然后再更新负载 这将让相交函数的并行性最大化
回到硬件相交模型 此流程图解释了该过程 但它忽略了一个重要元素
在相交过程中 光线追踪暂存空间 用于存储遍历的状态 并将结果返回给 调用相交的 GPU 函数
相交器 API 支持 每条光线的负载 负载结构越大 对光线追踪性能的影响就越大
当涉及到光线负载时 相交结果可能包含所需的大部分数据 并且最好避免使用任何光线负载 如果你需要负载 请避免使用全局全能负载结构 最好专门化每个相交调用的结构
最小化压缩数据类型结构的大小 并删除任何不需要的字段 优化光线负载的使用有助于 让更多光线被处理
例如 考虑一个基本负载 与相交位置 指示命中的标志和颜色 在内存中字段会这样布局
位置成员将位于开头 并且由于其大小和对齐方式 命中标志将距离开头 16 个字节
但随后 RGB 成员的 字节偏移量为 32 使得整个结构大小为 48 字节
通过将 float3 值 更改为它们的压缩等效值 可以减少因对齐而损失的空间 可以删除命中标志 因为在使用 Metal 光线追踪 API 时 不需要它 你只需检查相交结果中的 相交类型即可 这易于使用且性能更高 特别是对于阴影和遮挡等可见光线 同样地 可以基于光线的原点、方向 以及与结果的相交距离 来计算位置
然后 为了进一步减小大小 可以使用 Metal 着色语言中的 打包方法 在交集函数中 将 RGB 颜色打包到四个字节
在此示例中 光线数据负载结构开始时 大小为 48 字节 最后减少为 4 字节 通过使用此类方法 你可以优化光线负载 以提高光线追踪性能
与光线负载一样 相交标签也会 以类似的方式影响光线追踪性能
另一个会使用到光线追踪暂存使用的 是相交器上的相交标签 这些标签是要追踪的 遍历的附加状态
此声明中的世界空间数据标记 意味着 必须为每条光线存储 对象到世界和世界到对象的矩阵 这会增加光追暂存的使用 并将影响相交调用期间的占用率
关于标记 需要注意的 另一个重要事项是 它们需要在相交器 及其调用的相交函数之间进行匹配
相交器优于相交查询 因为相交查询 API 会影响 光线追踪性能
从硬件相交器模型来看 它非常适合 着色语言中的相交器
相交查询定义不使用 自定义相交函数的对象 相交代码在 原始 GPU 函数中执行 并且硬件相交器需要等待 代码完成后才继续遍历
如果选择使用相交查询 硬件没有自定义相交函数来排序 并且无法对执行进行分组
它还需要使用更多的 光线追踪暂存内存 以便它可以返回 GPU 函数
相交查询是光线相交的 替代模型 以支持 其他着色语言的可移植性 由于相交器与硬件实现保持一致 因此相交器优先于相交查询
如果需要使用相交查询 尽可能使用少的查询对象 如果需要多个相交查询 请尝试重用查询对象 仅仅更改属性 这样可以在一个查询中 重复使用光线追踪暂存 例如 你有一个相交查询对象 IQ1 用于执行一些光线追踪工作 如果在将不透明度设置为不透明后 你需要执行更多光线追踪工作 无需创建新的相交查询对象 只需使用相交参数 即可重置现有不透明度为不透明的 相交查询对象
这样你就可以重用光线追踪暂存内存
使用多个相交查询时 避免在它们之间切换 以及重叠它们的遍历 这避免了于正在进行的硬件遍历之间 进行高开销的交换
例如 在光线追踪工作中 不要从 IQ1 切换到 IQ2 然后再返回到 IQ1 连续使用 IQ1 并用它完成光线追踪工作 然后再切换到 IQ2 我们来总结一下光线追踪最佳实践 仅在必要时使用自定义相交函数
优化光线负载
尽量减少相交标签的数量
优先使用相交器而不是相交查询
要了解有关 Metal 光线追踪的更多信息 请观看《Metal 光线追踪指南》 要了解如何使用 Xcode 中 Metal 调试器的 新光线追踪计数器 请查看《探索 M3 和 A17 Pro 中的新 Metal 剖析工具》
我们来总结一下 为了提高 Metal 着色器的性能 你可以通过使用函数常量和 函数组等 Metal 功能来 减少着色器执行时间 使用此类功能可以在 Metal 中 实现更多优化机会 提高线程占用率 并通过更好的资源利用率 来提高并行性
应用相交函数 光线负载、相交标签 和相交器的最佳实践 以充分利用硬件加速光线追踪
谢谢大家
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。