Verursacht == eine Verzweigung in GLSL?


27

Der Versuch, genau herauszufinden, was Verzweigungen verursacht und was nicht in GLSL.

Ich mache das viel in meinem Shader:

float(a==b)

Ich verwende es, um if-Anweisungen ohne bedingte Verzweigung zu simulieren ... aber ist es effektiv? Ich habe jetzt nirgendwo in meinem Programm if-Anweisungen und auch keine Schleifen.

EDIT: Zur Verdeutlichung mache ich so etwas in meinem Code:

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;

EDIT: Ich weiß, warum ich keine Verzweigung will. Ich weiß, was Verzweigung ist. Ich bin froh, dass Sie den Kindern das Verzweigen beibringen, aber ich würde mich gerne über boolesche Operatoren (und bitweise Operationen, aber ich bin mir ziemlich sicher, dass diese in Ordnung sind) informieren.

Antworten:


42

Welche Ursachen eine Verzweigung in GLSL verursachen, hängt vom GPU-Modell und der OpenGL-Treiberversion ab.

Die meisten GPUs haben anscheinend die Form einer Operation "Wählen Sie einen von zwei Werten", die keine Verzweigungskosten verursacht:

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

und manchmal sogar Dinge wie:

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

wird ohne Verzweigungsstrafe auf wenige Auswahlwertoperationen reduziert.

Einige GPU / Treiber haben (hatten?) Eine gewisse Strafe für den Vergleichsoperator zwischen zwei Werten, aber eine schnellere Operation beim Vergleich gegen Null.

Wo es schneller gehen könnte:

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

Anstatt (tmp1 != tmp2)direkt zu vergleichen , ist dies jedoch sehr abhängig von der GPU und dem Treiber. Daher empfehle ich, die Vergleichsoperation zu verwenden und den Optimierungsjob dem OpenGL-Treiber zu überlassen, da ein anderer Treiber möglicherweise ein Problem mit der längeren Form hat und seien Sie schneller mit der einfacheren, lesbareren Weise.

"Branches" sind auch nicht immer schlecht. Zum Beispiel auf der SGX530-GPU, die in OpenPandora verwendet wird, dieser scale2x-Shader (30 ms):

    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;
    }

Endete dramatisch schneller als dieser äquivalente Shader (80ms):

    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;

Sie wissen nie im Voraus, wie ein bestimmter GLSL-Compiler oder eine bestimmte GPU funktionieren wird, bis Sie einen Benchmark durchführen.


Um den To-Point hinzuzufügen (obwohl ich keine aktuellen Timing-Nummern und Shader-Codes habe, die Ihnen für diesen Teil zur Verfügung stehen), verwende ich derzeit als reguläre Testhardware:

  • Intel HD Graphics 3000
  • Intel HD 405-Grafik
  • nVidia GTX 560M
  • nVidia GTX 960
  • AMD Radeon R7 260X
  • nVidia GTX 1050

Als breite Palette von verschiedenen, gängigen GPU-Modellen zum Testen.

Testen jeweils mit Windows-, Linux-proprietären und Linux-Open-Source-OpenGL- und OpenCL-Treibern.

Und jedes Mal, wenn ich versuche, GLSL-Shader (wie im obigen SGX530-Beispiel) oder OpenCL-Vorgänge für eine bestimmte GPU / Treiber-Kombination zu optimieren , wird die Leistung auf mehr als einer der anderen GPUs / Treiber gleichermaßen beeinträchtigt.

Abgesehen von einer deutlichen Reduzierung der mathematischen Komplexität auf hohem Niveau (z. B. Umwandlung von 5 identischen Divisionen in ein einzelnes Reziprok und 5 Multiplikationen) und einer Reduzierung der Textur-Lookups / Bandbreite ist dies höchstwahrscheinlich eine Zeitverschwendung.

Jede GPU unterscheidet sich zu stark von den anderen.

Wenn Sie speziell an (einer) Spielekonsole (n) mit einer bestimmten GPU arbeiten würden, wäre dies eine andere Geschichte.

Der andere (für kleine Spieleentwickler weniger wichtige, aber immer noch bemerkenswerte) Aspekt ist, dass die GPU-Treiber eines Tages möglicherweise stillschweigend Ihre Shader ersetzen ( wenn Ihr Spiel populär genug wird ), und zwar durch benutzerdefinierte, für diese bestimmte GPU optimierte Shader . Das alles für Sie zu tun.

Sie tun dies für beliebte Spiele, die häufig als Benchmarks verwendet werden.

Wenn Sie Ihren Spielern Zugriff auf die Shader gewähren, damit sie sie leicht selbst bearbeiten können, können einige von ihnen zu ihrem eigenen Vorteil einige zusätzliche FPS einsparen.

Zum Beispiel gibt es fan-made Shader & Textur-Packs für Oblivion, um die Framerate auf sonst kaum spielbarer Hardware dramatisch zu erhöhen.

Und wenn Ihr Shader erst einmal komplex genug ist, Ihr Spiel fast abgeschlossen ist und Sie mit dem Testen auf einer anderen Hardware beginnen, sind Sie beschäftigt genug, um Ihre Shader so zu reparieren, dass sie auf einer Vielzahl von GPUs funktionieren, da dies auf verschiedene Fehler zurückzuführen ist, die Sie nicht kennen Zeit haben, sie zu diesem Grad zu optimieren.


"Oder wenn Sie Ihren Spielern Zugriff auf die Shader gewähren, damit sie sie leicht selbst bearbeiten können ..." Wie gehen Sie mit Wallhack-Shadern und dergleichen um, seit Sie dies erwähnt haben? Ehrensystem, verifiziert, Berichte ...? Ich mag die Idee, Lobbys auf die gleichen Shader / Assets zu beschränken, was auch immer sie sein mögen, da Positionen zu maximal / minimal / skalierbarem Realismus, Exploits usw. Spieler und Modder zusammenbringen sollten, um die Überprüfung, Zusammenarbeit usw. zu fördern daran zu erinnern, dass Garys Mod so funktioniert hat, aber ich bin nicht auf dem Laufenden.
John P

1
@JohnP Was die Sicherheit angeht, funktioniert alles, was davon ausgeht, dass der Client nicht gefährdet ist, nicht. Natürlich, wenn Sie nicht möchten, dass Leute ihre Shader bearbeiten, ist es sinnlos, sie freizulegen, aber es hilft nicht wirklich viel bei der Sicherheit. Ihre Strategie zum Erkennen von Dingen wie Wallhacks sollte das Herumspielen von Dingen auf der Clientseite als niedrige erste Barriere behandeln, und es könnte möglicherweise einen größeren Vorteil für das Ermöglichen von Light Modding geben, wie in dieser Antwort, wenn dies nicht zu einem erkennbaren unfairen Vorteil für den Spieler führt .
Cubic

8
@JohnP Wenn Sie nicht möchten, dass Spieler auch durch Wände sehen, lassen Sie sich vom Server keine Informationen darüber senden, was sich hinter der Wand befindet.
Polygnome

1
Das ist nur so - ich bin nicht gegen Wall-Hacking zwischen Spielern, die es aus irgendeinem Grund mögen. Als Spieler habe ich jedoch einige AAA - Titel aufgegeben, weil sie unter anderem Beispiele für ästhetische Modder gemacht haben, während money / XP / etc. Hacker blieben unversehrt (und verdienten echtes Geld mit denjenigen, die frustriert genug waren, um zu zahlen), unterbesetzt und automatisierten ihr Report- und Appeal-System und stellten sicher, dass die Spiele von der Anzahl der Server, die sie am Leben hielten, überlebt und gestorben sind. Ich hatte gehofft, dass es als Entwickler und als Spieler einen dezentraleren Ansatz geben könnte.
John P

Nein, ich mache keine Inline wenn irgendwo. Ich schwebe nur (boolesche Aussage) * (etwas)
Geklmintendon't of Awesome

7

@Stephane Hockenhulls Antwort gibt Ihnen ziemlich genau das, was Sie wissen müssen, es wird vollständig hardwareabhängig sein.

Aber lassen Sie mich Ihnen einige Beispiele , wie es die Hardware abhängig sein kann und warum Verzweigung ist auch ein Problem überhaupt, was macht die GPU hinter den Kulissen tun , wenn Verzweigung tut stattfinden.

Mein Fokus liegt hauptsächlich auf Nvidia, ich habe einige Erfahrungen mit CUDA-Programmierung auf niedriger Ebene und ich sehe, was PTX ( IR für CUDA- Kernel , wie SPIR-V, aber nur für Nvidia) generiert wird, und sehe die Benchmarks, um bestimmte Änderungen vorzunehmen.

Warum ist das Verzweigen in GPU-Architekturen so wichtig?

Warum ist es schlecht, überhaupt zu verzweigen? Warum versuchen GPUs zu vermeiden, überhaupt zu verzweigen? Weil GPUs normalerweise ein Schema verwenden, bei dem Threads denselben Befehlszeiger verwenden . GPUs folgen einer SIMD-ArchitekturTypischerweise und während sich die Granularität davon ändern kann (dh 32 Threads für Nvidia, 64 für AMD und andere), teilen sich auf einer bestimmten Ebene eine Gruppe von Threads den gleichen Befehlszeiger. Dies bedeutet, dass diese Threads dieselbe Codezeile betrachten müssen, um gemeinsam an demselben Problem zu arbeiten. Sie fragen sich vielleicht, wie sie in der Lage sind, dieselben Codezeilen zu verwenden und verschiedene Dinge zu tun? Sie verwenden unterschiedliche Werte in Registern, aber diese Register werden weiterhin in der gesamten Gruppe in denselben Codezeilen verwendet. Was passiert, wenn das nicht mehr der Fall ist? (IE eine Verzweigung?) Wenn das Programm wirklich keinen Weg daran vorbei hat, teilt es die Gruppe auf (Nvidia bezeichnet solche Bündel von 32 Threads als Warp , für AMD- und Parallel-Computing-Akademien als Wellenfront)) in zwei oder mehr verschiedene Gruppen.

Wenn es nur zwei verschiedene Codezeilen gibt, auf denen Sie landen würden, werden die Arbeitsthreads auf zwei Gruppen aufgeteilt (von hier aus nenne ich eine Warps). Nehmen wir an, die Nvidia-Architektur hat eine Warp-Größe von 32. Wenn die Hälfte dieser Threads voneinander abweicht, sind 2 Warps mit 32 aktiven Threads belegt, was die Effizienz von der Berechnung bis zum Put-Ende halbiert. Auf vielen Architekturen versucht die GPU, dies zu beheben, indem sie Threads zurück zu einem Warp zusammenführt, sobald sie denselben Zweig nach dem Befehl erreichen, oder der Compiler setzt explizit einen Synchronisationspunkt, der die GPU anweist, Threads zurück zu konvergieren oder dies zu versuchen.

beispielsweise:

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

Der Thread kann stark divergieren (ungleiche Anweisungspfade), sodass in einem solchen Fall Konvergenz auftreten kann, r += t;bei der die Anweisungszeiger wieder dieselben wären. Divergenz kann auch bei mehr als zwei Verzweigungen auftreten, was zu einer noch geringeren Kettauslastung führt. Vier Verzweigungen bedeuten, dass 32 Fäden in 4 Kettfäden aufgeteilt werden, was einer Durchsatzauslastung von 25% entspricht. Konvergenz kann jedoch einige dieser Probleme verbergen, da 25% nicht den gesamten Durchsatz des Programms ausmachen.

Auf weniger anspruchsvollen GPUs können andere Probleme auftreten. Anstatt zu divergieren, berechnen sie lediglich alle Zweige und wählen dann den Ausgang am Ende aus. Dies mag wie eine Divergenz aussehen (beide haben eine 1 / n-Durchsatzauslastung), es gibt jedoch einige Hauptprobleme beim Duplizierungsansatz.

Eines ist der Stromverbrauch, Sie verbrauchen immer viel mehr Strom, wenn ein Zweig passiert. Dies wäre schlecht für mobile GPUS. Zweitens tritt Divergenz nur bei Nvidia gpus auf, wenn Threads desselben Warps unterschiedliche Pfade nehmen und daher einen anderen Befehlszeiger haben (der ab Pascal gemeinsam genutzt wird). Sie können also weiterhin Verzweigungen und keine Durchsatzprobleme bei Nvidia-GPUs haben, wenn diese in Vielfachen von 32 auftreten oder nur in einem Warp von Dutzenden auftreten. Wenn es wahrscheinlich ist, dass eine Verzweigung auftritt, laufen mit größerer Wahrscheinlichkeit weniger Threads auseinander und Sie haben sowieso kein Verzweigungsproblem.

Ein weiteres kleineres Problem ist, dass beim Vergleich von GPUs mit CPUs häufig keine Vorhersagemechanismen und andere robuste Verzweigungsmechanismen vorhanden sind, da diese Mechanismen viel Hardware beanspruchen. Aus diesem Grund ist bei modernen GPUs häufig ein No-Op-Fill zu beobachten.

Praktisches Beispiel für einen Unterschied in der Architektur der GPU

Nehmen wir nun Stephanes Beispiel und sehen, wie die Baugruppe für verzweigungslose Lösungen auf zwei theoretischen Architekturen aussehen würde.

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

Wie Stephane sagte, kann der Geräte-Compiler, wenn er auf eine Verzweigung stößt, beschließen, einen Befehl zu verwenden, um ein Element zu "wählen", das am Ende keine Verzweigungsstrafe haben würde. Das heißt auf manchen Geräten würde dies zu so etwas wie kompiliert werden

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

bei anderen ohne eine select-Anweisung könnte es zu kompiliert werden

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

das könnte so aussehen:

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

Das ist branchenlos und äquivalent, benötigt aber weitaus mehr Anweisungen. Da Stephanes Beispiel wahrscheinlich für beide Systeme kompiliert wird, ist es nicht sehr sinnvoll, die Mathematik manuell zu berechnen, um die Verzweigung selbst zu entfernen, da der Compiler der ersten Architektur möglicherweise entscheidet, statt in die zweite Form zu kompilieren die schnellere Form.


5

Ich stimme mit allem überein, was in der Antwort von @Stephane Hockenhull gesagt wurde. So erweitern Sie den letzten Punkt:

Sie wissen nie im Voraus, wie ein bestimmter GLSL-Compiler oder eine bestimmte GPU funktionieren wird, bis Sie einen Benchmark durchführen.

Absolut wahr. Außerdem sehe ich, dass diese Art von Frage ziemlich häufig auftaucht. In der Praxis habe ich jedoch selten einen Fragment-Shader als Ursache für ein Leistungsproblem gesehen. Es kommt viel häufiger vor, dass andere Faktoren Probleme verursachen, z. B. zu viele Statuslesevorgänge von der GPU, zu viele Puffer, zu viel Arbeit in einem einzelnen Draw-Aufruf usw.

Mit anderen Worten, bevor Sie sich Gedanken über die Mikrooptimierung eines Shaders machen, profilieren Sie Ihre gesamte App und stellen Sie sicher, dass die Shader die Ursache für Ihre Verlangsamung sind.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.