web-dev-qa-db-de.com

Verlangsamen bedingte Anweisungen Shader?

Ich möchte wissen, ob "if-Anweisungen" in Shadern (Vertex/Fragment/Pixel ...) die Shader-Leistung wirklich verlangsamen. Beispielsweise:

Ist es besser, dies zu verwenden:

vec3 output;
output = input*enable + input2*(1-enable);

anstatt dies zu benutzen:

vec3 output;
if(enable == 1)
{
    output = input;
}
else
{
    output = input2;
}

in einem anderen Forum wurde darüber gesprochen (2013): http://answers.unity3d.com/questions/442688/shader-if-else-performance.html Hier sagen die Jungs, dass Die If-Anweisungen sind wirklich schlecht für die Leistung des Shaders.

Auch hier wird darüber gesprochen, wie viel in den if/else-Anweisungen (2012) enthalten ist: https://www.opengl.org/discussion_boards/showthread.php/177762-Performance-alternative-for-if- ( -)

vielleicht ist die Hardware oder der Shader-Compiler jetzt besser und sie beheben irgendwie dieses (möglicherweise nicht vorhandene) Leistungsproblem.

BEARBEITEN:

In diesem Fall ist enable eine einheitliche Variable und wird immer auf 0 gesetzt:

if(enable == 1) //never happens
{
    output = vec4(0,0,0,0);
}
else  //always happens
{
    output = calcPhong(normal, lightDir);
}

Ich denke, hier haben wir einen Zweig im Shader, der den Shader verlangsamt. Ist das korrekt?

Ist es sinnvoller, zwei verschiedene Shader wie den einen für den anderen und den anderen für den if-Teil zu erstellen?

47
Thomas

Worum geht es bei Shadern, die möglicherweise sogar Leistungsprobleme bei if -Anweisungen verursachen? Es hat damit zu tun, wie Shader ausgeführt werden und woher GPUs ihre massive Rechenleistung beziehen.

Separate Shader-Aufrufe werden normalerweise parallel ausgeführt und führen dieselben Anweisungen zur selben Zeit aus. Sie führen sie einfach auf verschiedenen Mengen von Eingabewerten aus. Sie teilen sich Uniformen, haben aber unterschiedliche interne Register. Ein Begriff für eine Gruppe von Shadern, die alle dieselbe Folge von Operationen ausführen, ist "Wellenfront".

Das potentielle Problem bei jeder Form der bedingten Verzweigung ist, dass es all das vermasseln kann. Dadurch müssen unterschiedliche Aufrufe innerhalb der Wellenfront unterschiedliche Codesequenzen ausführen. Dies ist ein sehr teurer Prozess, bei dem eine neue Wellenfront erstellt, Daten darauf kopiert usw. werden müssen.

Es sei denn ... es tut nicht.

Wenn es sich beispielsweise um eine Bedingung handelt, die von every in der Wellenfront aufgerufen wird, ist keine Laufzeitdivergenz erforderlich. Daher sind die Kosten für if nur die Kosten für die Prüfung einer Bedingung.

Nehmen wir also an, Sie haben eine bedingte Verzweigung und nehmen an, dass alle Aufrufe in der Wellenfront dieselbe Verzweigung haben. Es gibt drei Möglichkeiten für die Art des Ausdrucks in dieser Bedingung:

  • Statisch zur Kompilierungszeit. Der Bedingungsausdruck basiert vollständig auf Konstanten zur Kompilierungszeit. In diesem Fall wissen Sie anhand des Codes, welche Zweige belegt werden. Nahezu jeder Compiler behandelt dies als Teil der grundlegenden Optimierung.
  • Statisch gleichmäßige Verzweigung Die Bedingung basiert auf Ausdrücken, die Dinge betreffen, von denen bekannt ist, dass sie zur Kompilierungszeit konstant sind (insbesondere Konstanten und uniform Werte). Aber der Wert des Ausdrucks ist zur Kompilierungszeit nicht bekannt. Der Compiler kann also statisch sicher sein, dass Wellenfronten niemals von diesem if unterbrochen werden, aber der Compiler kann nicht wissen, welcher Zweig genommen wird.
  • Dynamische Verzweigung. Der bedingte Ausdruck enthält andere Begriffe als Konstanten und Uniformen. Hier kann ein Compiler nicht a priori sagen, ob eine Wellenfront aufgebrochen wird oder nicht. Ob dies erforderlich ist, hängt von der Laufzeitauswertung des Bedingungsausdrucks ab.

Unterschiedliche Hardware kann unterschiedliche Verzweigungstypen ohne Divergenz verarbeiten.

Auch wenn eine Bedingung von verschiedenen Wellenfronten übernommen wird, könnte der Compiler den Code so umstrukturieren, dass keine tatsächliche Verzweigung erforderlich ist. Sie haben ein gutes Beispiel angegeben: output = input*enable + input2*(1-enable); ist funktional äquivalent zur if -Anweisung. Ein Compiler könnte erkennen, dass ein if verwendet wird, um eine Variable zu setzen, und somit beide Seiten ausführen. Dies wird häufig in Fällen von dynamischen Bedingungen durchgeführt, in denen die Körper der Zweige klein sind.

So gut wie jede Hardware kann var = bool ? val1 : val2 Ohne Abweichungen verarbeiten. Dies war bereits 2002 möglich.

Da dies sehr hardwareabhängig ist, ... hängt es von der Hardware ab. Es gibt jedoch bestimmte Epochen von Hardware, die betrachtet werden können:

Desktop, Pre-D3D10

Dort ist es ein bisschen der wilde Westen. Der NVIDIA-Compiler für solche Hardware war dafür berüchtigt, solche Bedingungen zu erkennen und tatsächlich Ihren Shader neu zu kompilieren wann immer Sie Uniformen wechselten, die solche Bedingungen betrafen.

Im Allgemeinen stammen in dieser Ära etwa 80% der Anweisungen, die "nie if verwenden". Aber auch hier ist es nicht unbedingt wahr.

Sie können eine Optimierung der statischen Verzweigung erwarten. Sie können hoffen davon ausgehen, dass eine statisch gleichmäßige Verzweigung keine zusätzliche Verlangsamung verursacht (obwohl die Tatsache, dass NVIDIA davon ausgegangen ist, dass die Neukompilierung schneller ist als die Ausführung, es zumindest für ihre Hardware unwahrscheinlich macht). Eine dynamische Verzweigung kostet Sie jedoch etwas, auch wenn alle Aufrufe dieselbe Verzweigung haben.

Compiler dieser Zeit tun ihr Bestes, um Shader so zu optimieren, dass einfache Bedingungen einfach ausgeführt werden können. Zum Beispiel ist Ihre output = input*enable + input2*(1-enable); etwas, das ein anständiger Compiler aus Ihrer entsprechenden if -Anweisung generieren könnte.

Desktop, Post-D3D10

Hardware dieser Ära ist im Allgemeinen in der Lage, statisch einheitliche Verzweigungsanweisungen mit geringer Verlangsamung zu verarbeiten. Bei der dynamischen Verzweigung kann es zu einer Verlangsamung kommen oder auch nicht.

Desktop, D3D11 +

Hardware dieser Ära wird so gut wie garantiert mit dynamisch einheitlichen Bedingungen mit geringen Leistungsproblemen umgehen können. In der Tat muss es nicht einmal dynamisch einheitlich sein; Solange alle Aufrufe innerhalb derselben Wellenfront denselben Pfad nehmen, werden Sie keinen signifikanten Leistungsverlust feststellen.

Beachten Sie, dass dies wahrscheinlich auch bei Hardware aus der vorherigen Epoche möglich ist. Aber dies ist der Punkt, an dem es fast sicher ist, dass es wahr ist.

Mobile, ES 2.0

Willkommen zurück im wilden Westen. Im Gegensatz zum Pre-D3D10-Desktop ist dies hauptsächlich auf die enorme Varianz der ES 2.0-Hardware zurückzuführen. Es gibt so viele Dinge, die mit ES 2.0 umgehen können, und alle funktionieren sehr unterschiedlich.

Die statische Verzweigung wird wahrscheinlich optimiert. Ob Sie durch statisch gleichmäßige Verzweigung eine gute Leistung erzielen, hängt jedoch stark von der Hardware ab.

Mobile, ES 3.0+

Hardware ist hier eher ausgereifter und leistungsfähiger als ES 2.0. Daher können Sie davon ausgehen, dass statisch einheitliche Verzweigungen relativ gut ausgeführt werden. Und manche Hardware kann wahrscheinlich dynamische Zweige so verarbeiten wie moderne Desktop-Hardware.

99
Nicol Bolas

Es hängt stark von der Hardware und dem Zustand ab.

Wenn Ihr Zustand einheitlich ist: Machen Sie sich keine Sorgen, lassen Sie den Compiler damit umgehen. Wenn Ihre Bedingung etwas Dynamisches ist (wie ein Wert, der aus einem Attribut berechnet oder aus einer Textur oder etwas anderem abgerufen wird), ist sie komplizierter.

In letzterem Fall müssen Sie Tests und Benchmarks durchführen, da dies von der Komplexität des Codes in den einzelnen Zweigen und der Konsistenz der Zweigentscheidung abhängt.

Wenn beispielsweise einer der Zweige zu 99% aus dem Fall entfernt wird und das Fragment verwirft, möchten Sie höchstwahrscheinlich die Bedingung beibehalten. Aber OTOH in Ihrem einfachen Beispiel oben: Wenn enable eine dynamische Bedingung ist, ist die arithmetische Auswahl möglicherweise besser.

Wenn Sie keine eindeutige Lösung wie die oben beschriebene haben oder wenn Sie nicht für eine festgelegte bekannte Architektur optimieren, ist es wahrscheinlich besser, wenn der Compiler dies für Sie herausfindet.

6
246tNt