==是否会导致GLSL中的分支?


27

试图找出导致分支的原因以及在GLSL中没有导致分支的原因。

我在着色器中经常这样做:

float(a==b)

我用它来模拟if语句,没有条件分支...但是有效吗?我现在程序中的任何地方都没有if语句,也没有任何循环。

编辑:为了澄清,我在我的代码中做了这样的事情:

float isTint = float((renderflags & GK_TINT) > uint(0)); // 1 if true, 0 if false
    float isNotTint = 1-isTint;//swaps with the other value
    float isDarken = float((renderflags & GK_DARKEN) > uint(0));
    float isNotDarken = 1-isDarken;
    float isAverage = float((renderflags & GK_AVERAGE) > uint(0));
    float isNotAverage = 1-isAverage;
    //it is none of those if:
    //* More than one of them is true
    //* All of them are false
    float isNoneofThose = isTint * isDarken * isAverage + isNotTint * isAverage * isDarken + isTint * isNotAverage * isDarken + isTint * isAverage * isNotDarken + isNotTint * isNotAverage * isNotDarken;
    float isNotNoneofThose = 1-isNoneofThose;

    //Calc finalcolor;
    finalcolor = (primary_color + secondary_color) * isTint * isNotNoneofThose + (primary_color - secondary_color) * isDarken * isNotNoneofThose + vec3((primary_color.x + secondary_color.x)/2.0,(primary_color.y + secondary_color.y)/2.0,(primary_color.z + secondary_color.z)/2.0) * isAverage * isNotNoneofThose + primary_color * isNoneofThose;

编辑:我知道为什么我不想分支。我知道是什么分支。我很高兴您正在教孩子们关于分支的知识,但我想了解布尔运算符(以及按位运算符,但我很确定它们很好)

Answers:


42

导致GLSL分支的原因取决于GPU模型和OpenGL驱动程序版本。

大多数GPU似乎都具有“选择两个值之一”操作的形式,而没有分支成本:

n = (a==b) ? x : y;

有时甚至是:

if(a==b) { 
   n = x;
   m = y;
} else {
   n = y;
   m = x;
}

将减少为几个选择值运算,而没有分支代价。

某些GPU /驱动程序在两个值之间的比较运算符上有(但有一些)惩罚,但在与零进行比较时操作更快。

哪里可能做得更快:

gl_FragColor.xyz = ((tmp1 - tmp2) != vec3(0.0)) ? E : tmp1;

而不是(tmp1 != tmp2)直接进行比较,但这取决于GPU和驱动程序,因此,除非您针对的是非常特定的GPU,并且没有其他目标,我建议您使用compare操作并将优化工作留给OpenGL驱动程序,因为另一个驱动程序可能存在较长格式的问题并以更简单,更易读的方式更快。

“分支”也不总是一件坏事。例如,在OpenPandora中使用的SGX530 GPU上,此scale2x着色器(30毫秒):

    lowp vec3 E = texture2D(s_texture0, v_texCoord[0]).xyz;
    lowp vec3 D = texture2D(s_texture0, v_texCoord[1]).xyz;
    lowp vec3 F = texture2D(s_texture0, v_texCoord[2]).xyz;
    lowp vec3 H = texture2D(s_texture0, v_texCoord[3]).xyz;
    lowp vec3 B = texture2D(s_texture0, v_texCoord[4]).xyz;
    if ((D - F) * (H - B) == vec3(0.0)) {
            gl_FragColor.xyz = E;
    } else {
            lowp vec2 p = fract(pos);
            lowp vec3 tmp1 = p.x < 0.5 ? D : F;
            lowp vec3 tmp2 = p.y < 0.5 ? H : B;
            gl_FragColor.xyz = ((tmp1 - tmp2) != vec3(0.0)) ? E : tmp1;
    }

最终比同等着色器(80毫秒)快得多:

    lowp vec3 E = texture2D(s_texture0, v_texCoord[0]).xyz;
    lowp vec3 D = texture2D(s_texture0, v_texCoord[1]).xyz;
    lowp vec3 F = texture2D(s_texture0, v_texCoord[2]).xyz;
    lowp vec3 H = texture2D(s_texture0, v_texCoord[3]).xyz;
    lowp vec3 B = texture2D(s_texture0, v_texCoord[4]).xyz;
    lowp vec2 p = fract(pos);

    lowp vec3 tmp1 = p.x < 0.5 ? D : F;
    lowp vec3 tmp2 = p.y < 0.5 ? H : B;
    lowp vec3 tmp3 = D == F || H == B ? E : tmp1;
    gl_FragColor.xyz = tmp1 == tmp2 ? tmp3 : E;

您永远不会事先知道特定的GLSL编译器或特定的GPU将如何执行,直到您对其进行基准测试。


为了添加该点(即使我没有实际的时序号和着色器代码也无法向您展示这一部分),我目前将其用作常规测试硬件:

  • 英特尔高清显卡3000
  • 英特尔HD 405显卡
  • nVidia GTX 560M
  • nVidia GTX 960
  • AMD Radeon R7 260X
  • nVidia GTX 1050

作为广泛的不同通用GPU模型进行测试。

使用Windows,Linux专有和Linux开源OpenGL&OpenCL驱动程序进行测试。

每次我尝试对某个特定GPU /驱动程序组合进行GLSL着色器的微优化(如上述SGX530示例)或OpenCL操作时, 我最终都会同样损害多个以上GPU /驱动程序之一的性能。

因此,除了明显降低高级数学复杂度(例如:将5个相同的除法转换为一个倒数和5个乘法)并减少纹理查找/带宽外,这很可能会浪费您的时间。

每个GPU都与其他GPU不同。

如果您要专门使用特定GPU在一个或多个游戏机上工作,那就大不相同了。

另一个方面(对于小型游戏开发人员来说意义不大,但仍然值得注意)是计算机GPU驱动程序有一天可能会使用针对该特定GPU优化的自定义重写的着色器(如果您的游戏变得足够流行)静默替换您的着色器。一切都为您工作。

他们将针对经常用作基准的热门游戏进行此操作。

或者,如果您允许播放器访问着色器,以便他们可以自己轻松地对其进行编辑,则其中一些可能会挤出一些额外的FPS以使自己受益。

例如,有用于Oblivion的风扇制造的着色器和纹理包,可在原本无法播放的硬件上显着提高帧速率。

最后,一旦您的着色器变得足够复杂,您的游戏就差不多完成了,并且您开始在不同的硬件上进行测试,您将很忙,仅修复您的着色器就可以在各种GPU上运行,这是因为您不会遇到各种错误有时间将它们优化到那个程度。


“或者,如果让您的玩家访问着色器,以便他们可以自己轻松地对其进行编辑……”既然您已经提到了这一点,那么wallwall着色器之类的方法可能是什么?荣誉系统,已验证,报告...?我喜欢将大厅限制为相同的着色器/资产的想法,无论它们是什么,因为关于最大/最小/可缩放现实主义,漏洞利用等的立场应将玩家和修改者聚集在一起,以鼓励进行审查,协作等。记得这是Gary的Mod的工作方式,但是我很了解。
约翰·P

1
@JohnP Security明智的做法是,假设客户端没有受到损害,则无论如何都行不通。当然,如果您不希望人们编辑其着色器,则没有必要公开它们,但这对安全性没有多大帮助。您的检测壁垒之类的事物的策略应该将客户端混乱的事物作为较低的第一障碍,并且可以说,如果不对玩家造成可检测的不公平优势,则允许轻度修改可能会有更大的好处,例如此答案。
立方'18年

8
@JohnP如果您不希望玩家也看穿墙壁,请不要让服务器向他们发送任何有关墙壁后面的信息。
Polygnome

1
就是这样-我不反对出于任何原因而喜欢它的玩家之间的隔墙攻击。不过,作为一名球员,我放弃了多个AAA冠军头衔,因为(除其他原因外)他们在金钱/ XP /等方面成为了审美修改者的榜样。黑客毫发无损(他们从受挫足以支付的那些人中赚了真钱),人员不足并自动执行了他们的报告和申诉系统,并确定了游戏的生存和死机,因为它们需要维持生存的服务器数量。我希望开发人员和玩家都可以采用更分散的方法。
约翰·P

不,我不会在任何地方进行内联。我只做float(boolean statement)*(某事)
Geklmintendon's Awesome '18的

7

@Stephane Hockenhull的答案几乎为您提供了您需要了解的内容,它完全取决于硬件。

但是,让我给你一些例子如何它可以依赖于硬件,为什么分支甚至可以在所有的问题,什么是GPU分支时做幕后确实发生。

我的工作重点主要是在Nvidia上,我对低层CUDA编程有一些经验,并且看到生成了什么PTX(用于CUDA 内核的IR,例如SPIR-V,但仅用于Nvidia),并查看进行某些更改的基准。

为什么在GPU架构中分支如此重要?

首先为什么分支不好?为什么GPU首先要避免分支?因为GPU通常使用线程共享同一指令指针的方案。GPU遵循SIMD架构通常,虽然其粒度可能会发生变化(例如,Nvidia为32个线程,AMD为64个线程),但在某些级别上,一组线程共享相同的指令指针。这意味着这些线程需要查看相同的代码行才能共同解决同一问题。您可能会问他们如何使用相同的代码行并做不同的事情?它们在寄存器中使用不同的值,但是在整个组中,这些寄存器仍在同一行代码中使用。如果情况不再如此会怎样?(IE是分支吗?)如果程序确实无法解决问题,它将分散该组(Nvidia这样的32个线程束被称为Warp,对于AMD和并行计算学术界来说,它被称为波前))分成两个或多个不同的组。

如果最后只有两行不同的代码行,那么工作线程将分为两组(在这里,我将它们称为扭曲)。让我们假设Nvidia体系结构,其中扭曲大小为32,如果这些线程的一半发散,那么32个活动线程将占用2个扭曲,这使从计算到结束的工作效率降低了一半。在许多体系结构上,GPU将尝试通过在到达同一指令发布分支后将线程重新汇聚为单个扭曲来解决此问题,或者编译器将显式放置一个同步点,该点告诉GPU将线程汇聚回或尝试将其收敛。

例如:

if(a)
    x += z * w;
    q >>= p;
else if(c)
    y -= 3;
r += t;

线程具有很大的发散潜力(不同的指令路径),因此在这种情况下,您可能会在r += t;指令指针再次相同的情况下发生收敛。超过两个分支也会发生发散,从而导致更低的warp利用率,四个分支意味着将32个线程分成4个warp,吞吐量利用率为25%。但是,收敛可能会掩盖其中的一些问题,因为25%的吞吐量不会在整个程序中保持不变。

在不太复杂的GPU上,可能会发生其他问题。他们只是计算所有分支,然后最后选择输出,而不是求和。这可能看起来与发散相同(两者都具有1 / n的吞吐量利用率),但是复制方法存在一些主要问题。

一个是电源使用情况,当发生分支时,您正在使用更多的电源,这对移动GPU不利。其次,只有在相同线程束的线程采用不同的路径并因此具有不同的指令指针(从Pascal开始共享)时,差异才发生在Nvidia GPU上。因此,如果Nvidia GPU的出现次数是32的倍数,或者仅发生在几十个扭曲中,您仍然可以分支,而不会遇到吞吐量问题。如果可能发生分支,则发生分支的线程的可能性更大,而且您也不会遇到分支问题。

另一个较小的问题是,当您将GPU与CPU进行比较时,由于这些机制占用了多少硬件,它们通常没有预测机制和其他强大的分支机制,因此您经常会在现代GPU上看到无操作填充

实际的GPU架构差异示例

现在,让我们以Stephanes为例,看看两种理论体系结构上的无分支解决方案的组装形式。

n = (a==b) ? x : y;

就像史蒂芬(Stephane)所说的那样,当设备编译器遇到分支时,它可能决定使用一条指令来“选择”元素,而该元素最终将没有分支代价。这意味着在某些设备上,它将被编译为类似

cmpeq rega, regb
// implicit setting of comparison bit used in next part
choose regn, regx, regy

在没有选择指令的情况下,可能会被编译为

n = ((a==b))* x + (!(a==b))* y

可能看起来像:

cmpeq rega regb
// implicit setting of comparison bit used in next part
mul regn regcmp regx
xor regcmp regcmp 1
mul regresult regcmp regy
mul regn regn regresult

它是无分支的和等效的,但是需要更多指令。由于Stephanes示例可能会在各自的系统上编译,因此尝试手动计算数学以删除自己的分支并没有多大意义,因为第一种体系结构的编译器可能会决定编译为第二种形式,而不是更快的形式。


5

我同意@Stephane Hockenhull的回答中所说的一切。要扩展最后一点:

您永远不会事先知道特定的GLSL编译器或特定的GPU将如何执行,直到您对其进行基准测试。

绝对真实。此外,我看到这种问题经常出现。但是实际上,我很少看到片段着色器成为性能问题的根源。更常见的是其他因素导致了问题,例如从GPU读取过多状态,交换过多缓冲区,在一次绘制调用中进行过多工作等。

换句话说,在担心微优化着色器之前,请对整个应用程序进行概要分析,并确保着色器是导致速度下降的原因。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.