Verlangsamen bedingte Anweisungen Shader?

73

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

Ist es besser, dies zu verwenden:

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

anstatt dies zu verwenden:

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-Aussagen wirklich schlecht sind 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 sind die Hardware oder der Shadercompiler jetzt besser und sie beheben irgendwie dieses (möglicherweise nicht vorhandene) Leistungsproblem.

BEARBEITEN:

In diesem Fall ist enable beispielsweise 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 einen für den anderen und einen für den if-Teil zu erstellen?

Thomas
quelle
Ich glaube, die if-Anweisung reduziert nur die Berechnung, die der Shader durchführen muss. Das erste Beispiel zwingt den Shader, beide Fälle zu berechnen ... während die if-Anweisung sie reduziert. Und nein, Ihre if-Anweisung wird keine Verarbeitungszeit in Anspruch nehmen ... da sie am Ende nach der Kompilierung als Verzweigungen im Assembler-Code erscheint
Rakshith Ravi
danke für deine schnelle antwort! Ich habe die Frage bearbeitet. Können Sie bitte auch den bearbeiteten Teil beantworten? Danke!!!
Thomas
1
Ich denke nicht, dass du 2 Shader haben solltest. Ich glaube, dass dies zu einer vorzeitigen Optimierung führt. Wie @ 246tNt hervorhob, wird der Compiler damit umgehen, wenn es sich um eine Uniform handelt. Aber abgesehen davon denke ich wirklich nicht, dass Sie sich so viele Sorgen um die Leistung machen sollten. Mach es zuerst. Wenn Sie dann sehen, dass der Shader der Engpass ist, schauen Sie sich den Algorithmus an. Sachen wie diese sollten nicht viel schaden
Rakshith Ravi
Ich habe diese Frage nur gestellt, weil ich wissen möchte, ob ich damit eine bessere Leistung erzielen kann oder nicht. Wenn Sie einen Shader für eine iterierende Berechnung verwenden und ihn 100 Mal ausführen müssen, bevor Sie Ihr Bild rendern, ist jede ms wichtig
Thomas
3
Verwenden Sie immer den Code, der die klarsten Absichten hat, es sei denn, Sie profilieren und stellen empirisch fest, dass dies ein Engpass ist. In diesem Fall die ifAussage. Die Gründe dafür sind zweierlei: Sie erhalten keine verschlungenen Codekonstrukte ohne Beweise dafür, dass sie besser sind, und was noch wichtiger ist, der Compiler erkennt Ihre Absichten eher und optimiert das Problem.
Dan Bechard

Antworten:

143

Was ist mit Shadern, das möglicherweise sogar ifLeistungsprobleme bei Anweisungen verursacht? 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, wobei dieselben Anweisungen gleichzeitig ausgeführt werden. Sie führen sie einfach auf verschiedenen Sätzen von Eingabewerten aus. Sie teilen sich Uniformen, haben aber unterschiedliche interne Register. Ein Begriff für eine Gruppe von Shadern, die alle dieselbe Abfolge von Operationen ausführen, ist "Wellenfront".

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

Es sei denn ... es nicht.

Wenn die Bedingung beispielsweise von jedem Aufruf in der Wellenfront übernommen wird, ist keine Laufzeitdivergenz erforderlich. Als solche sind die Kosten für ifnur die Kosten für die Überprüfung einer Bedingung.

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

  • Kompilierungszeit statisch. Der bedingte Ausdruck basiert vollständig auf Konstanten zur Kompilierungszeit. Als solches wissen Sie anhand des Codes, welche Zweige verwendet werden. So ziemlich jeder Compiler behandelt dies als Teil der grundlegenden Optimierung.
  • Statisch gleichmäßige Verzweigung. Die Bedingung basiert auf Ausdrücken, die Dinge beinhalten, von denen zur Kompilierungszeit bekannt ist, dass sie konstant sind (insbesondere Konstanten und uniformWerte). Der Wert des Ausdrucks ist jedoch zur Kompilierungszeit nicht bekannt. Der Compiler kann also statisch sicher sein, dass Wellenfronten dadurch niemals unterbrochen werden if, aber der Compiler kann nicht wissen, welcher Zweig verwendet 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 aufgelöst wird oder nicht. Ob dies geschehen muss, hängt von der Laufzeitauswertung des Bedingungsausdrucks ab.

Unterschiedliche Hardware kann unterschiedliche Verzweigungstypen ohne Divergenz verarbeiten.

Selbst wenn eine Bedingung von verschiedenen Wellenfronten übernommen wird, kann der Compiler den Code so umstrukturieren, dass keine tatsächliche Verzweigung erforderlich ist . Sie haben ein gutes Beispiel gegeben: output = input*enable + input2*(1-enable);entspricht funktional der ifAussage. Ein Compiler könnte erkennen, dass ein ifzum Setzen einer Variablen verwendet wird, und somit beide Seiten ausführen. Dies geschieht häufig bei dynamischen Bedingungen, bei denen die Körper der Zweige klein sind.

So gut wie die gesamte Hardware kann var = bool ? val1 : val2ohne Abweichungen umgehen . Dies war bereits im Jahr 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 Ihren Shader tatsächlich neu zu kompilieren, wenn Sie Uniformen wechselten, die sich auf solche Bedingungen auswirkten.

Im Allgemeinen stammen in dieser Ära etwa 80% der "nie verwendeten ifAussagen". Aber auch hier ist es nicht unbedingt wahr.

Sie können eine Optimierung der statischen Verzweigung erwarten. Sie können hoffen, dass eine statisch einheitliche Verzweigung keine zusätzliche Verlangsamung verursacht (obwohl die Tatsache, dass NVIDIA dachte, die Neukompilierung wäre schneller als die Ausführung, es zumindest für ihre Hardware unwahrscheinlich macht). Eine dynamische Verzweigung kostet Sie jedoch etwas, selbst wenn alle Aufrufe dieselbe Verzweigung haben.

Compiler dieser Ära geben ihr Bestes, um Shader so zu optimieren, dass einfache Bedingungen einfach ausgeführt werden können. Zum Beispiel output = input*enable + input2*(1-enable);ist dies etwas, das ein anständiger Compiler aus Ihrer entsprechenden ifAnweisung 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 nicht.

Desktop, D3D11 +

Hardware dieser Ära ist so gut wie garantiert in der Lage, dynamisch einheitliche Bedingungen mit geringen Leistungsproblemen zu bewältigen . In der Tat muss es nicht einmal dynamisch einheitlich sein; Solange alle Aufrufe innerhalb derselben Wellenfront denselben Pfad einschlagen, werden Sie keinen signifikanten Leistungsverlust feststellen.

Beachten Sie, dass einige Hardware aus der vorherigen Epoche dies wahrscheinlich auch tun könnte. Aber hier ist es fast sicher, dass es wahr ist.

Mobil, 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 jedoch durch statisch einheitliche Verzweigung eine gute Leistung erzielen, hängt stark von der Hardware ab.

Mobil, ES 3.0+

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

Nicol Bolas
quelle
Man kann davon ausgehen, dass Compiler integrierte bedingte Funktionen wie minund stepso optimieren, dass sie nicht voneinander abweichen, oder?
Tenfour04
3
@ Tenfour04 Ja, sie verwenden entweder dedizierte Anweisungen oder eine bedingte Zuweisungsanweisung, die, wie erwähnt, keine Abweichung verursacht. Als Randnotiz gibt es meiner Meinung nach keinen guten Grund, die Schrittfunktion zu verwenden. Sie können es jederzeit durch die viel intuitivere bedingte Zuweisung ersetzen:step(a, b) * c -> (a >= b) ? c : 0
Quinchilion
Wie wäre es mit WebGL? Da es auf ES 2.0 basiert, funktioniert es ähnlich oder weil die Implementierung z. Unter Windows wird ANGLE verwendet. Ist die Ausführung von Zweigen ausgereifter?
Andrew
2
@ Andrew: " Wie wäre es mit WebGL? " Was ist damit? Wir sprechen über das Verhalten von Hardware , nicht von Software. Die Schnittstelle ist nicht so wichtig; Entscheidend ist, wie die zugrunde liegende Hardware reagiert. Ich erwähne nur API-Versionen, da die höchste auf einigen Hardwarekomponenten unterstützte API-Version eine grobe Schätzung ihrer Funktionen darstellt.
Nicol Bolas
11

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 dies komplizierter.

Für den letzteren Fall müssen Sie so ziemlich testen und bewerten, 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 besteht und das Fragment verwirft, möchten Sie höchstwahrscheinlich die Bedingung beibehalten. Wenn OTOH in Ihrem einfachen Beispiel oben enableeine dynamische Bedingung ist, ist die arithmetische Auswahl möglicherweise besser.

Wenn Sie keinen eindeutigen Fall wie den oben genannten haben oder nicht für eine feste bekannte Architektur optimieren, ist es wahrscheinlich besser, wenn der Compiler dies für Sie herausfindet.

246tNt
quelle