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.
@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:
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.
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
bei anderen ohne eine select-Anweisung könnte es zu kompiliert werden
das könnte so aussehen:
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.
quelle
Ich stimme mit allem überein, was in der Antwort von @Stephane Hockenhull gesagt wurde. So erweitern Sie den letzten Punkt:
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.
quelle