大多数浏览器和
Developer App 均支持流媒体播放。
-
Metal 着色器高级优化
Metal 着色语言是一种简单易用的编程语言,用于编写在 GPU 上执行的图形与计算功能。更深入地了解设计模式、内存访问模型,以及能减少瓶颈和隐藏延迟的详细着色器编码最佳做法。面向经验丰富、GPU 架构知识扎实并希望充分发挥每个循环潜力的着色器作者。
资源
-
下载
高级Metal着色器优化
大家好 我叫Fiona 这是我的同事Alex 我任职于iOS GPU处理器团队 我们的工作就是让大家的着色器 运行于最新的iOS设备上 而且让它们尽可能高效地运行 这里我要谈一下我们的演讲 高级Metal着色器优化 锻造和打磨你的Metal着色器 我们的编译器是基于LVM的 而且我们和开源社区合作 让LVM更加适用于GPU
这是其它几个Metal相关的演讲 万一你错过了的话 不用担心 你可以上网看录像 昨天讲了Metal应用的 第一部分和第二部分 今天早些时候讲了Metal新特性 的第一和第二部分 因为Metal有太多新的东西了 当然 现在这个是最后一部分 就是你们现在看的东西
所以在这个演讲中 我们将讨论一些东西 你们可以利用这些编辑器相关的东西 来让你们的代码运行地更快 这些中的一部分是针对A8处理器 和更新的图像处理器 包括一些从未被公布过的信息 其中一些也将比较通俗 我们会谈到大家看到A8的图标 在演示文稿中 是针对A8处理器的 另外 我们会谈一些潜在的陷阱 这些东西一般不会出现 就像微优化 你们经常发现的那样 但是一旦你们碰到了 很有可能会降低很多性能 相比之下 其他东西都不重要了 所以始终要确保你们不会掉入这些陷阱 这些会被标记上三角符号 正如你们看到的那样 在我们继续之前 这不是第一步 这是最后一步 进行低级别的着色器优化是没有意义的 除非之前你已经做了高级别的优化 比如观看其他的Metal演讲 优化你的绘制调用 你的引擎结构等等 优化你的着色器差不多是 你要做的最后一件事情 而且 这个演讲主要是给 经验丰富的着色器程序员 也许你在Metal方面经验丰富 而且你想要更深入的学习优化着色器 又或许你刚开始接触Metal 但你已在着色器优化方面做了很多工作 在其他平台 而且你想知道如何更好地优化 A8处理器和更新的图像处理器 这个演讲就是为你们准备的
你可能已经看过这个管线图了 若你看过任意一个之前Metal演讲 当然 我们也将聚焦在 这个管线图的编码阶段 正如你们所见的 着色器课程 首先 Alex将会讲一下 一些着色器性能的基本知识 和一些高级一点的问题 然后 我会回来讲一些低级别的 底层的 繁琐的着色器优化
着色器性能基础
谢谢Fiona 首先让我解释一下 着色器性能基础 这些是你想要确保 你们是对的 在你们深入了解源代码级别的优化 通常你们在此所做改变的影响可能弱化 或者潜在地掩盖你在其它地方 做的更加有针对性的改变 所有今天我打算谈四个方面 缓冲参数的地址空间选择 缓冲预加载 碎片函数资源的处理 和如何优化电脑内核
好 让我们从地址空间开始 因为这个功能不存在于 所有着色语言 我给大家简单的入门 所以 图像处理器有很多的途径 从内存中拿到数据 而对于不同的应用场景 这些途径都是被优化过的 所以它们有着不同的性能特征 在Metal中 我们给了开发者 使用路径的控制权 通过要求它们符合所有的缓存器 参数和指针 在着色语言中 用它们要用的地址空间 所以一部分地址空间专门用于 从内存中提取信息
其中的第一个就是设备地址空间 这是一个限制相对较少的地址空间 你可以通过地址空间来读写数据 你可以传任意多的数据 你在API级别指定的寄存器 有着相对灵活的参数要求
另一方面 你有固定的地址空间 如名字中所写 这是一个只读的地址空间 但是有另外一些约束 你能传输的数据量是有限制的 通过地址空间 另外 缓存区偏移量 你在API级别指定的 有更加严格的对齐要求 但是 这就是地址空间 被优化 为了大量数据重用的情况 所以 每当它有理的时候 你就会利用这个地址空间
找出是否这个连续地址空间 对你的缓冲器参数有意义 通常在于两个问题 第一个问题是 我是否知道我有多少数据 如果你的数据量是变化的 这通常就是你需要使用 设备地址空间的一个信号 另外 你要看一下缓冲器中的 每一个有多少被读取 如果这些可能被读取多次 这通常表示你需要 把他们放下连续地址空间 让我们用几个例子把这付诸实践 例子源于一些顶点着色器
首先 你有规则的原始顶点数据
可以看出 每个顶点有自己的数据块 而且每个数据块只被自己的顶点读 因此基本上没有重用 这是在设备地址空间 真正需要满足的条件
接着 你有投影矩阵以及其它的矩阵 现在 属于你的 是你有一个对象 这些对象被单一的顶点读 在完全的数据重用情况下 你想这个对象在常量地址空间里
把东西混合一点后 分析常量矩阵 在这种情况下 很大可能你能得到最大 bone数 这些对象是你正在处理的 但是你单个看每个bone时 矩阵可以被每一个和 那个bone相关的顶点读取 这也是潜在的大量重用 所以这也该在常量地址空间里
最后,让我们来看看每一个实例数据
如你所见,例子中的所有顶点 将会读取这个特定的数据 但是另一方面 你有一些潜在的可变实例 所以这也应该在设备地址空间里
为何地址空间的选择对于性能那么重要 我们进入到下一个话题 缓冲器预加载
Fiona将会花些时间讲一下 如何真正地在着色器中优化加载和存储 但在很多情况下 你能做的最好的事就是 把这个工作交给专门的硬件去做 以下两种情况 我们能做优化 背景缓存和顶点缓存 但是这基于 你知道着色器中访问模式 和你把它们放在了什么地址空间
让我们以常量缓存预加载 所以这里想说的是 不同于通过常量地址空间来加载 我们其实要做的是拿到你的数据 然后将它们置于特殊的常量寄存器 这些寄存器是更快的 对于ALU的读写 所以只要我们知道什么数据会被读取 我们就可这么做 如果你的偏移量是已知的编译时间 这就很直接明了了 但是如果你的偏移量在运行前是未知的 那么我们就需要一些额外的信息 关于你正在读取多少数据量
所以给编译器表明 这个通常需要两个步骤 第一 你需要确保这个数据 是在常量地址空间
另外你需要表明 你的访问时是静态绑定的
做这个最好的方式是通过传参数时 尽量使用引用而不是指针 若你只是传递一个参数或者一个结构体 这是很直接的 你可以只改变指向引用的指针 和对应地改变你的访问
如果你传递一个绑定的数组 这就会有所不同 所以你在这个例子中要做的是 你可以嵌入那个大小的数组 并将那个结构体通过引用来传递 而不是传递原来的指针 我们通过一个例子来实践一下 在前灯碎片着色器 如你在原始版本中看到的那样 我们有的是一些参数 作为普通设备指针进行传递 而且这并没有给出我们想要的信息 所以我们能比这个做得更好 相反 若我们注意到灯的数量相互关联 我们能做的就是将灯的数据 和数量一起放在这么一个结构体中
然后将这个结构体传给常量地址空间 作为像这样一个引用 这样 我们就实现了常量缓存预加载
让我们在看一个例子 看看它实际中是如何影响你的 实现延迟渲染有很多种方式 但是我们发现实际上 你选择的实现方式 会对实践中你实现的性能 有很大的影响
现在一种常用的模式是使用单一着色器 来积累出所有灯光的结果 正如你在函数的声明中看到的那样 它可能在这个场景中 读取任意或者所有的光 这意味着你的输入大小是无关联的
另一方面 如果你能结构化你的渲染 使得每一个光在它自己的 绘制调用中被处理 那么每一个光就只需要读那个光的数据 它是着色器 那意味着你可以传递 在常量地址空间和充分利用缓存预加载 其实我们发现在A8后的图像处理器上 这有一个很大的性能提升
现在让我们谈一下顶点缓存预加载 顶点缓存预加载的目的是 重用相同的专用硬件 我们会在固定的函数顶点获取中使用 我们可在常规缓存加载中做这个 就像 你在读取缓存 看上去就像固定函数顶点获取 它意味着你需要 使用顶点或者实例的ID来进行索引 现在我们可以处理一些额外的改变 为顶点或者实例ID 比如应用一个发生器 使用或者不使用基本顶点 或实例偏移量 你可能在API级别的应用
当然最简单的方式是充分利用 尽量使用Metal顶点描述功能 但是如果你写你自己的索引代码 我们强烈建议设计你的数据 使得顶点线性到达而简化缓冲区索引 意识到这并不妨碍你做自己喜欢的事情 比如你在画矩形 你想赋值 给矩形的所有顶点 你仍然可以做其它事情 像用顶点ID除以4建立索引 因为这看起来就像个分配器
让我们继续着色器阶段的一些具体问题
在iOS 10里 我们介绍了记录资源的功能 在片段功能里 对于隐藏的表面去除有着有趣的含义
在这之前你也许已经习惯这种行为 片段不需要被着色 只要一个不透明的片段过来遮挡它 所以这不再是正确的尤其 当你的片段功能正在写资源 因为这些资源记录仍需要发生 相反你的行为仅仅取决于 前面所发生的事情 确切地说 发生什么取决于 在你的片段功能里 你是否能进行早期的片段测试 如果你通过 了早期的片段测试 一旦被栅格化 只要它通过早期的深度和模块测试 如果你没有指定早期的片段测试 那么只要它被栅格化了 你的片段就会被着色 所以从最小化你的着色器的角度看 你要做的是尽量早地使用早期片段测试 但是有一些其它事情 你可以做的 来提高你得到的拒绝
且这其中大部分归结于绘制顺序 你想要绘制这些图案 片段函数进行资源写的图案 在不透明的图案的后面 而且如果你使用这些图案 来更新的你的深度和画笔缓存 我们强烈建议 你把这些缓存按照从前到后进行排序 注意这个指导应该听上去相当的熟悉 如果你曾经处理过片段函数 丢弃或者修改你每一像素点的深度
现在让我谈一谈计算机内核 因为计算机内核的决定性特征 决定了计算能力 我们来说说什么因素影响了 你在iOS中是怎么做的
首先 我们有计算机线程启动的限制
所以在A8及以后的图像处理器上 有一定量的时间 被花在了启动一些计算机线程 所以如果你的在一个计算机线程中 没有做足够多的工作 这就会使硬件没有被充分利用 这就降低了性能
一个处理这个问题 比较好的方式和一个好的模式 对于在iOS上写计算机内核 是在一个计算线程上 处理多个概念的工作条目 尤其我们找到一种工作得很好的模式是 重用数值 不是通过线程组内存传递 而是通过重用加载的数据 为下一个工作条目 当你在同一个计算线程中 处理下一个工作条目 最好通过一个例子来阐述这点
这是一个音节过滤内核 这是一种最直接的版本了 它读取三个不发音的区域 并产生一个输出像素点
所以如果应用处理多工作条目的模式 在单一计算线程中 我们就得到类似于这样的东西 注意现在我们是在以 一次两个像素点向前迈进 所以处理第一个像素点 和它以前做的没区别 我们读取这个3乘3的区域 我们应用了这个过滤器 且写上去了这个数值
但让我们看看 第二个像素点是如何被处理 支架是以一次两个像素点迈进的 我们需要确保有第二个像素点被处理 现在我们读取了它的数据 注意到这个像素点 所需要的三分之二的区域 已经被之前的像素点加载 所以我们不需要重新加载 我们可重用这些原有值 现在我们所需要加载是 这个像素点的新的三分之一
之后 我们就能应用过滤器完成任务
注意 最后我们不是做12个纹理读取 不是原有的9个 而是我们在创建2个像素点 在大量的单个像素点的纹理获取中 这是个有意义的缩减 当然这个模式并不适用于 所有的计算使用案例 有时你仍然要通过线程组内存传递数据 这种情况下 当你同步 一个线程组中的线程的时候 需要记住一个重要的事情是 你想要使用栅栏在尽量小的范围内 为你需要同步的线程 尤其若你的线程组在一个SIMD里面 在Metal中 通常的线程组闸函数是不需要的 你可以使用的 是新的SIMD组闸函数 这是在iOS 10中引入的
我们发现的实际上是线程组 符合单一SIMD和 使用SIMD组闸通常要快于 使用一个更大的线程组 为了挤压另外的重用 但是不得不使用线程组闸作为结果
总的来说
确保你使用正确的地址空间 为每一个缓存参数 根据我们说的规则
机构化你的数据和渲染 最大程度地利用常量 和顶点缓存预加载
确保使用早期片段测试来过滤 尽量多的片段 当你在做资源写操作的时候
给每一个计算给予线程足够的工作量 这样你就不会被限制 在计算线程启动 给任务使用最小的栏栅当你需要 同步线程池中的线程的时候 那么 我想让Fiona详细地 讲一下调制着色器代码
谢谢Alex 在进入这些细节之前 我想要讲一下 一些图像处理器的普通特性 和一些你们可能会遇到的瓶颈 可能你们中所有的人都熟悉它 但是我觉得还是有必要快速的过一下 对于图像处理器 通常来讲你有一组资源 经常会发生的事情是着色器 被其中的一个资源给卡住了 举个例子 你卡在了 内存带宽 提高你的着色器中的其它东西 通常不会带来明显的性能提升 识别出这些瓶颈 和聚焦在这些瓶颈上 以提高性能是很重要的 这实际上对于非瓶颈的东西也是有益的 比如 在那个例子当中 如果内存使用成为了瓶颈 你通过提高算法变得更加高效 你仍然节约了电量 即使你没有提高帧速率 当然在移动设备上 节约电量始终是是重要的 所以这不是一个能忽略的东西 只是因为帧速率在那个例子中没有上升 所以在着色器这里 你需要记住四个典型的瓶颈 第一个是非常直接的ALU带宽 图像处理器能处理的数学计算量 第二是内存带宽 同样也是非常直接的 图像处理器能从系统内存中 加载的数据量 另外两个相对就没那么明显 第一是内存问题率 它表示能执行的内存处理量 这会出现在以下的场景中 当你的内存处理比较小的时候 或者你使用很多线程组内存等等 最后一个 也是我等会儿会深入讲的一个 是延迟占用寄存器使用 也许你已经同说过它来 但是我会把它留在最后
所以为了缓解其中的一些瓶颈 和提高着色器的性能和效率 我们将一起看一下四类优化机会 第一类是数据类型 首先要想到 当你优化着色器时 是选择数据类型 当你选择数据类型时 最重要的一件事是A8 及更新的图像器有16位的寄存器单元 这意味着当你使用32位数据类型时 那就是两倍的寄存器空间 两倍的带宽 可能是两倍的电量等等 总之所有东西都是翻倍的 所以 对应的 需要节约寄存器 这样会得到更快的性能 更低的能耗 通过使用更小的数据类型 在运算中尽可能地使用 半字节和短字节的变量 从能耗看 半字节比浮点型变量节能多了 浮点型比整型又要节能 甚至在整型当中 短整型又要比长整型节能 你节省寄存器最有效的方式 是给纹理和插值使用半字节型 因为通常情况下你并不需要浮点型 注意我不是指纹理格式 我指的是数据类型 这数据类型是你用来存储 纹理样本或者插值的结果
A8及更新的图像处理器中 是相当方便的 使得比在其它图像处理器上 使用更小的数据类更加的简单 是因为数据类型转换一般是免费的 甚至在浮点型和半字节型之间 这意味着你不需要担心 我会不会在使用半字节类型的过程中 引入了太多的类型转化 这会导致消耗太多吗 这是否值得 不 因为类型转换是自由的 所以很可能是更快了 无论何时你都可以使用半字节类型 不需要担心这个部分 需要记住的一点是半字节精度的数值 和限制与浮点型数据是不同的 经常出现的一个错误 如写65,535这样一个半字节类型 但它实际上是无穷大 因为这比最大的半字节类型还要大 所以要注意有哪些限制条件 最好能知道哪里应该用 半字节类型 哪里不应该用 降低在着色器中发生意外错误的可能性 一个例子应用 对于使用短整型数据类型 是线程ID 你们中与计算机内核打过交道的 应该知道 线程ID在程序中是广泛被使用的 所以把它们弄得小一点 能很大程度地提高 运算的性能 可以节省寄存器等等 对于本地线程ID 在这个例子中 没有理由去使用无符号整型 因为本地线程ID 不能用那么多的线程ID 对于全局线程ID 通常你可以使用无符号短整型 因为大多数情况下 你不会有那么多的线程ID 当然这要取决于你的程序 但是在绝大多数的情况下 你不会超过2的16次方减1 所以你可使用无符号短整型 这会降低功耗 也会更快 因为所有线性ID相关的运算都变快了 所以我强烈建议你尽可能的这么做
另外 记住在类C的语言中 当然这也包括Metal 运算的精度是通过 输入类型的大小来定义的 比如 一个浮点型乘以一个半字节型 这是一个浮点型运算而非半字节型运算 它会向上提升 所以对应的 确保尽量不要使用浮点型 因为这会把一个半字节运算 输入半字节型 返回半字节型 变成一个浮点型运算 因为根据语法 这实际上是一个浮点型运算 因为输入值中至少有一个是浮点型 所有你可能想要做么做 事实上 这就会是一个半字节型运算 而且会变得更快 这可能就是你所说的 所以要注意 不要在不经意间 引入浮点型精度的运算 在你的代码中 当你其实并不想要它时
虽然我刚才说了数据类型越小越好 有一个例外是字符型 在A8及更新的处理器上 原生的数据类型大小 是16位的 而不是8位的 所以字符型并不会节省空间 或者能耗或者其它一些东西 进一步说 没有原生的8位运算 所以它似乎要被仿真 如果你需要的话 它不会过度的耗资源 随便用 但是它可能会导致额外的指令 所以不要把变量压缩为字符型 在没有必要的时候
所以接下来我们会有计算优化 在这个类别中 几乎所有的都会影响ALU带宽 你能做的第一件事情始终 是尽量使用Metal内置 它们是给各种不同的函数优化实现的 它们已经为硬件进行了优化 通常来讲 会比你自己实现的要好 特别的 实际上 他们中的一些通常是免费的 这是因为图形处理器通常都有修改工具 运算可以通过输入输出指令免费地执行 对于A8及以后的图形处理器 通常包括非门 绝对值和饱和值 像你们看到的一样 这三种绿色的运算符 所以 没有必要尝试着 让你的代码更加聪明或者加速它 通过避免这些 同样是因为他们几乎总是免费的 因为他们是免费的 你不可能比免费更加好 无法在免费的基础上再进行优化了
A8及更新的图形处理器 和很多其它处理器一样 是标量机器 而着色器通常是用向量表示的 编译器会把它们在内部分离开来 当然 写向量代码并没有什么坏处 它经常更加清晰 也更容易维护 而且它符合你想要的东西 但是它也不会比写标量代码更加好 从编译器的角度 和你要得到的代码 所以没有必要去向量化代码 那并不符合向量格式 因为最后它只会导致一样的东西 而你浪费了你的时间 但是 从一个侧面说明 我等会儿会深入说一下 在A8及更新图像处理器上 确实有向量加载在存储里 尽管他们没有向量运算 所以这只是应用于算术
指令级别并行 你们中有些人可能已经优化过了 特别是若你做过中央处理器相关的工作 但是在A8及更新的图形处理器上 这通常不是一个好事情 去尝试优化它 因为通常它要与寄存器使用竞争 而寄存器使用通常更重要 所以你可能见过的一种普通模式是一种 你有多个有序的加法器 在图形处理器上更好地处理延迟 但是在A8及更新的图形处理器上 这可能是降低效率的 你最好只使用一个累加器 当然 这会导致更多的复杂例子 比人工的简单例子 简而言之 不要尝试去重新结构化你的代码 从中得到更多的ILP 这可能并不会对你有所帮助 最糟糕的是 你的代码可能变得更差
所以在A8及更新的图形处理器上 有个相当好的功能 就是它们有着非常快速的选择指令集 它们是三元运算符 过去 使用一些小聪明是非常普遍的 就像这个 在三元运算符中执行选择操作 为了避免那些分支等等 但是在现代图形处理器上 这些通常会是起相反的效果 特别是对于A8及更新的图形处理器 因为编译器不会考虑这些小聪明 它不会明白你的真实意图 真的 这是非常糟糕的 你可能刚刚写了这个 而且它会更快 更短 它会展示你的意图 像以前一样 太过聪明反而会变得复杂 你所做的会使编译器困惑
现在 它就是一个潜在的陷阱 希望这不会经常发生 对于相对的图形处理器 它们中的大部分都没有整数除法运算 或者取余数指令集 整数型而不是浮点型 所以避免除法运算 不是字面的或者函数常量 这个新特性在前面当然演讲中提到过 所以在这个例子中 我们想说的是 分母是一个变量 那就会非常非常得慢 想象一下成百上千的时钟秒针 但是这另外两个例子 它们会非常得快 它们是好的 不要觉得你必须要避免它
最后 快速数学运算的主题 在Metal中 快速运算是默认的 这是因为编译器是经过 快速运算优化过的 这对Metal着色器的性能来说 至关重要 这能提供50%或者更多的性能提升 相较于没有快速运算 这也是为什么它是默认开启的 那么在快速运算模式下 我们到底做了什么事情呢 第一 一部分Metal中内置的函数 对于有没有快速运算 有着不用的精度保证 所以在某些函数中 它们会有稍微低一点的精度 在快速运算模式中会有更好的性能
编译器会提高中间件的精度 对于你的运算 比如通过组成一个多个加法指令集 这不会降低中间件的精度 所以举个例子 如果你写了一个浮点型的运算 你的这个运算至少是浮点型的运算 不是一个数学运算 若你想要写一个半字节型的运算 你最好就自己写 编译器是不会帮你做的 因为这是不被允许的 它不会让你的精度达到那样 我们确实会忽略 如果不是一个数字 无穷量 和符号零 这是很重要的 如果不那样的话 你不能真正证明 x乘以0等于0 但是我们不会引进一个新的NaN 不是一个数字 因为现实中 那是一个非常好的方式 来激怒开发者 毁坏他们的代码 我们不想要这么做 编译器会执行算术重整合 但是不会做算术分配 而且实际上 这不会毁坏代码 而且会使得它更快 我们不想毁坏代码
所以无论什么原因 如果绝对不能使用快速运算 还是有方法恢复一部分性能 Metal有一个内置的 融合的乘法加法 它允许你直接请求 融合的乘法加法指令集 当然如果快速运算被停用 编译器就不会被允许编译它们 它不会改变你的四舍五入的一位 这是被禁止的 所以如果你想要使用融合的乘法加法 而且快速运算是停用的 你需要使用内置的 而且那会重新增加一部分的性能 不是所有的性能 但至少有一部分
所以 我们的第三个话题 控制流程 预测的GP控制流程不是一个新的话题 你们中的一些人 可能对此已经非常熟悉了 但我快速回顾一下 这对你来说有什么意义 控制流程在SIMD中是一致的 每一个线程都在做同一件事情 相当的快速 就算编译器看不到它 这也是真的 所以如果你的程序看上去不一致 但是只有当运行的时候它才一致 那仍然是很快的 类似的 这个分歧的另一面 不同的道路做不同的事情 在那个例子中 它可能不得不运行 于所有的路径 同时地 不同于中央处理器在一个时间点 只会选择一个路径 因为它会做更多的工作 当然也就意味着不高效的控制流程 会影响任意一个瓶颈 因为它只是直接地表明 图像处理器做了更多事情 无论是什么事情
所以 在控制流程这个话题上 我给大家的一个建议 是避免切换fall-through 这在中央处理器的代码中非常的普遍 但是对于图像处理器 它们可能会成为低效 因为编译器不得不做相当严重的转型 为了让它们符合 图像处理器的控制流程模型 而且经常的 这会导致冗余的代码 和各种各样的麻烦的东西 你可能不希望发生这样的事情 若你能找到好方法来避免 代码中fall-through切换 你可能会变的更好
现在到了我们最后一个话题 内存访问 我们现在先从大家最可能碰到 的陷阱开始说 那是动态地索引非常量堆数组 这是非常有争议的 但你们中很多人可能已熟悉这些代码了 大致上看上去着这样的 你有一个包含数值的数组 在运行时被定义 在每一个线程或者 每一个函数调用中变化 而且你索引这个数字 通过另外一个也是变量的值 这就是动态索引非常量堆 在我们继续之前 我不会想当然的认为 对于图形处理器 堆栈是慢的 我会解释为什么 所以 对于中央处理器 通常你有多个线程 或者几十个线程 且你有几MB的缓存分配在这些线程中 所以每一个线程可能有 成千上百KB的堆栈空间 在它们变慢和不得不去处理主内存之前 对于图形处理器 通常会有成千上万的线程在同时跑 而且它们都在分享一个小得多的缓存 所以平均下来 每一个线程只有非常小的空间 给数据和堆栈 这不单单意味着那个这不是很高效 作为一个惯例 对于绝大多数的图形处理器程序 如果你使用堆栈 你已经输了 这会非常慢 使得几乎其他任何东西都本有可能更好 一个真实世界的应用 程序开始时 它需给向量 从两个浮点型数据中选择一个 它用了32字节的数组 一组两个浮点型数据 在他它们中选择 使用这个堆栈数组 这会导致30%的性能损失 在这个程序中 尽管只在开始时做一次
这也会是相当的重要 当然每次我们提高编译器的时候 我们要尽量避免 尽量 避免产生这些堆栈访问 因为这是不好的 现在我要给大家展示两个好的例子 另外一个 哪些是常量 不是变量 那不是一个非常量堆栈数组 没关系 因为每一个线程的值不会变化 它们不需要在每个线程中被复制 所以这是可以的
这个也是可以的 等等 为什么? 这仍然是一个动态索引非常量堆栈数组 但是它只是做动态索引 因为这个回路 而且编译器会展开这个回路 实际上 你的编译器展开任意的回路 那会访问这个堆栈 为了让它停止这么做 所以在这个例子中 它被展开后 就不再被动态索引了 它会变得很快 值得提出来的是 因为在大量的图形学代码中 这是非常普通的模式 而且我不想吓唬你不要这么做 当它可能还行的时候
既然我们已经讲了这个主题 关于如何不要做加载和存储这些类型 让我们继续讲加载和存储 我们会讲得快一些 当A8及更新的图像处理器 都是用标量算法 正如我前面说过的那样 它们确实有向量内存单元 一个大的向量加载资源自然比 多个小向量要快 当小向量相加大小和这一个大向量一样 这通常会影响内存处理速率瓶颈 因为你通过负载来运行 那没多少负载 对于iOS 10 我们新的编译器优化中的一点 是我们会去向量化一些负载和存储 会去尽可能地邻接内存位置 同样是因为它可以给出好的性能提升 虽然如此 这是一个处理编译器时的例子 可能会很有帮助 我给大家举个例子 正如你们看到的这样 这是一个简单的回路 它做了一些计算和 读取了一个结构体数组 但是在每一步循环 它只读取两个加载 现在如果可以 我们想要它变成一个 因为一个比两个要好 而且编译器也想这样 它想要向量化这个 但是它做不到 因为A和C在内存中不是紧挨着的 所以它什么都做不了 编译器是不被允许重新安排结构体的 所以我们有两个负载 对此有两个解决办法 第一 当然是把它变成一个浮点型 它就是向量负载了 结束了 一个负载 两个一组 什么都好了 而且 对于iOS 10 这也一个相同的快速 因为在这里 我们重拍了我们的结构体 让值一个接一个 那样编译器可以向量化负载 当它做这个的时候 这同样是一个处理编译器的例子 编译器被允许做一些 它以前做不了的事情 因为你知道到底发生了什么 你知道如何选择模式 使得编译器开心 让它可以做
所以 另外一个要记住的 关于负载和存储的事情 是A8和更新的图像处理器 有专门的硬件 给设备内存地址分配 但是这个硬件是有限制的 访问设备内存的偏移量 必须在有符号整型的范围内 小一点的数据类型 比如短整型和无符号短整型也是可以的 实际上 它们是被强烈推荐的 因为那些也是在有符号整型的范围的 但是 无符号整型当然不是的 因为他可能有值超出了 有符号整型的范围 所以如果编译器发生一种情况 当偏移量是一个无符号整型 就不能保证 它会很好的待在有符号整型的范围内 必须手动地计算地址 而不是让专用硬件来做这个 那样会浪费电量 它会浪费ALU性能等等 这是不好的 所以 把你的偏移量转为整型 这样问题就解决了 当然利用这个通常会节省ALU带宽
所以在我们的最后一个话题上 我前面有所掩盖 延迟和占用 现代图像处理器的核心设计之一 是隐藏延迟 通过使用大规模的多线程 所以当它们等待一些慢的东西结束 比如纹理读取 它们只是运行另一个线程 而不是坐着什么都不干 只是等待 这是相当重要的 因为纹理读取通常要花好几百个循环 才能结束 平均下来
所以你在着色器中延迟越多 你就需要更多的线程来隐藏延迟 你有多少线程呢? 这限制于你有一定数量的资源 它们被一个线程组中的线程共享 所以显然基于每一个线程的使用量 你需要限制线程的数量 相冲突的两件事情是 寄存器和线程组存储器的数量 所以在每个线程中你使用寄存器越多 你就不能使用这么多线程 太简单了 如果你在每个线程中使用的线程组越多 你又会遭遇同样的问题 每一个线程对于你的线程 你可以检查着色器的实际占用率 用MTLComputePipeLineState 导致maxTotalThreadsPerThreadgroup 这将告诉你着色器的实际占有率是多少 基于寄存器的使用率 和线程组存储器的使用率 所以当我们说着色器延迟限制 这意味着用来 隐藏着色器延迟的线程太少 这时 你可以做两件事情 你可以减少着色器的延迟 保存寄存器 另外一件事是 避免使用更多的线程
所以 对于一个非常大而复杂的着色器 克服延迟是有点困难的 我将会重温一些伪代码实例 可能会给你强烈的直觉 在你的着色器中 如何考虑延迟和怎样略微理智地建模
所以 这儿有一个REAL的依赖案例 有一个纹理样本 然后我们使该纹理样本 执行if语句 然后我们在x语句里 创建另一个纹理样本 我们必须等两次 因为我们在执行if语句前必须等一次 在使用值之前又必须等一次 第二个纹理样本的 两次连续的纹理访问造成总共两次延迟
这儿有一个错误的依赖案例 该依赖看来很像另个 除非我们在if语句中不使用它 但是通常我们不能等待跨控制流程 这种情况下if语句 成为了一个严重的障碍 所以我们不得不等 即使没有数据依赖 所以我们仍然有两次延迟 当你意识到GPU并不在乎数据依赖 它只在乎出现什么依赖 这样第二个依赖的 延迟时间和第一个是一样的 即使那儿没有数据依赖
最后有个简单的依赖 在顶端你仅仅获取两个纹理 它们都被并行获取 这样我们就只等一次 所以就是1 x延迟而不是2 x延迟 所以 运用这个知识你将会干什么呢? 在很多实际着色器中 你有机会 在延迟和吞吐量之间权衡 一个常见的案例是 你能决定代码以一个纹理获取为依据 在这个着色器中我们不必做任何事 还是早点放弃 而且这是非常有意义的 因为在这种情况下你要做的事情 将不必做 你正在保存所有的工作 太棒了 但是当你增加吞吐量 通过减少你需要干的工作 但是你也增加了延迟 因为现在必须获取第一个纹理 然后等待纹理获取 接着做早期的终止检查 接着做其它的纹理获取 呃 会更快吗?难道不是吗? 时常你只需要测试一下 因为哪个更快真正依赖的是着色器 但是值得考虑的事时常是真正的权衡 你时常要测试来明确什么是正确的 虽然没有通用规则 我可以针对A8和后期的GPUs 给你一个独特的的指南 那就是同一时刻 硬件至少需要两个纹理获取 来获得足够的性能来避免延迟 一个是不够的 如果你只能做一次 没有问题 但是如果你有一些选择 来分配着色器中的纹理获取 如果你同一时刻允许它做两次 你会获得更好的性能
所以 总结就是 确保你选择了 正确的地址空间 数据结构 布局等等 因为把这个弄错会导致 这个演讲中的其它东西都不重要了
用编译器工作 写下你想的 不要尝试着太聪明 否则编译器不知道你想什么而迷失 且不能做好它自己的任务 另外 写下你想的是很简单的
注意大陷阱而不仅仅是极小的优化 它们时常不明显而且它们也不时常发生 但当它们发生时 会导致严重的问题 它们导致的问题如此严重 以至于再多的小优化都无法弥补
随意试验 会发生很多定律权衡 根本就没有单一的定律 全部都试一下 看哪种更快
如果你想要更多的信息 上网查询 演讲视频就在网上
还有其它的演讲 如果你又错过了 视频在网上都会有的
谢谢 -
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。