“字节码”而不是硬编码着色器性能
Posted
技术标签:
【中文标题】“字节码”而不是硬编码着色器性能【英文标题】:"Bytecode" instead of hardcoded shader performance 【发布时间】:2017-05-23 20:08:23 【问题描述】:我正在制作一个生成模型的图形程序。当用户执行一些动作时,着色器的行为需要改变。这些操作不仅会影响数值常量,也不会影响输入数据,它们还会影响一系列计算步骤的数量、顺序和类型。
为了解决这个问题,我想到了两个解决方案:
-
在运行时生成着色器代码,然后编译它。这非常依赖于 CPU,因为编译可能需要一些时间,但它对 GPU 非常友好。
使用同一着色器在运行时解释的某种字节码。这消除了再次编译着色器的需要,但现在 GPU 需要处理大量的簿记工作。
我为这两种方法开发了原型,结果比我预期的要极端。
编译时间很大程度上取决于着色器的其余部分(我猜有很多函数内联),我认为我可以重构着色器以减少每个线程的工作并缩短编译时间。但是,我现在不知道这是否足够,而且我不太喜欢运行时重新编译的想法(非常依赖于平台,更难调试,更复杂)。
另一方面,字节码方法的运行速度(不考虑第一种方法的编译时间)慢了 25 倍。
我知道字节码方法会变慢,但没想到会这样,尤其是在优化之后。
解释器通过从统一缓冲区对象中读取字节码来工作。这是它的简化,我在有用(非簿记)代码所在的位置放置了一个“...”,该部分与另一种方法相同(显然,它不在带有大 if/else 的循环内选择正确的指令):
layout (std140, binding=7) uniform shader_data
uvec4 code[256];
;
float interpreter(vec3 init)
float d[4];
vec3 positions[3];
int dDepth=0;
positions[0]=init;
for (int i=0; i<code[128].x; i+=3)
const uint instruction=code[i].x;
const uint ldi=code[i].y;
const uint sti=code[i].z;
if (instruction==MIX)
...
else
if (instruction<=BOX)
if (instruction<=TRANSLATION)
if(instruction==PARA)
...
else//TRANSLATION;
...
else
if (instruction==EZROT)
...
else//BOX
...
else
if (instruction<=ELLI)
if (instruction==CYL)
...
else//ELLI
...
else
if (instruction==REPETITION)
...
else//MIRRORING
...
return d[0];
我的问题是:你知道为什么这么慢(因为我没有在解释器中看到这么多的簿记)吗?你能猜出这个解释器的主要性能问题是什么吗?
【问题讨论】:
"这个解释器的主要性能问题是什么?" Branches. 我知道分支会导致性能损失,但在这种情况下,分支是完全一致的(非发散的):所有线程都将采用相同的分支。您仍然认为分支是主要问题吗?我尝试使用 switch/case 和另一个 if/else 结构,但这是性能更高的版本。我什至把 MIX 指令放在首位,因为 MIX 指令几乎代表了普通字节码的 50%。 我的阅读理解失败了,我想我需要一些下午的咖啡 :) 对统一分支的良好调用,除了我猜测着色器大小和/或着色器编译器无法优化以及它可以在“标准”着色器上:/ 【参考方案1】:GPU 在最好的情况下不喜欢条件分支。因此,字节码解释是您在 GPU 上可能做的最糟糕的事情之一。
当然,在您的情况下,分支的原理问题并没有那么糟糕,因为您的“字节码”都在统一的内存中。即便如此,由于所有的分支,它会运行得过慢。
最好更好地处理高级着色器的可能性,然后使用非常少 数量的分支来决定整个着色器的行为。这些不会是字节码级别的。它们更像是“使用矩阵蒙皮计算位置”或“使用此 BRDF 计算照明”或“使用阴影贴图”。
这就是所谓的“ubershader”方法:一个着色器,具有许多由几个统一设置确定的大而不同的代码路径。
如果您不能这样做,那么除了在需要时重新编译之外,您真的无能为力。这会对 CPU 造成伤害;您不能期望在开始重新编译它的帧(或之后的几帧,很可能)使用着色器。 SPIR-V 着色器可能有助于重新编译性能,但可能没有那么多。
虽然有一点延迟(~100ms)并没有那么糟糕,因为它不是游戏
我说测量进行着色器编译所需的时间。如果它小于 100 毫秒(或任何您认为具有足够交互性的时间),请使用它。
但是,请注意,许多 OpenGL 实现在单独的线程上重新编译着色器。所以当glLinkProgram
完成时,着色器可能还没有完成。为了准确地分析这个过程,您需要强制重新编译已经发生。获取GL_LINK_STATUS
应该可以解决问题。
另一个性能技巧:不要使用glCompileShader
和glLinkProgram
。相反,请改用glCreateShaderProgramv
。它创建了一个可分离的程序(仅包含一个着色器阶段),但该过程可能比必须编译和链接为单独的操作更快。
【讨论】:
“如果你做不到”我做不到。而且我无法在后台编译它,因为用户引发了重新编译,他会期望/需要那个时候的结果(尽管有一个小的延迟(~100ms)并不是那么糟糕,因为它不是游戏)。 “SPIR-V 着色器可能会有所帮助” 我最近一直在尝试使用它们,但我不确定它们在这方面有何帮助,为什么您认为它可能会有所帮助?您是考虑在运行时使用 SPIR-V 编译器还是使用 SPIR-V 编译器来编译解释器? 我的意思是 SPIR-V 着色器理论上直接编译比 GLSL 快。因此,它可能会使着色器重新编译选项变得不那么慢。另外,请参阅我添加到答案中的注释。 你的意思是......通过我自己生成SPIR-V而不是生成GLSL?您认为这是可以合理实现的吗? @dv1729:你生成的字节码很好,不是吗?只需使用它来创建 SPIR-V。 SPIR-V 可能有点奇怪,尤其是它的静态单赋值方面,但它旨在成为编译过程的目标。您生成此着色器是一个编译过程。应该不会太难。但在进行这项工作之前,请先配置文件以确保 1) GLSL 对您而言编译速度太慢,并且 2) SPIR-V 对您而言编译速度足够快。 谢谢!我将更多地研究编译方法(尝试减少编译时间、分析、SPIR-V...)以上是关于“字节码”而不是硬编码着色器性能的主要内容,如果未能解决你的问题,请参考以下文章