Vermeiden Sie if-Anweisungen in DirectX 10-Shadern?

14

Ich habe gehört, dass if-Anweisungen in Shadern vermieden werden sollten, da beide Teile der Anweisungen ausgeführt werden und dann das falsche gelöscht wird (was die Leistung beeinträchtigt).

Es ist immer noch ein Problem in DirectX 10? Jemand hat mir gesagt, dass darin nur der richtige Zweig ausgeführt wird.

Zur Illustration habe ich den Code:

float y1 = 5; float y2 = 6; float b1 = 2; float b2 = 3;

if(x>0.5){
    x = 10 * y1 + b1;
}else{
    x = 10 * y2 + b2;
}

Gibt es eine andere Möglichkeit, es schneller zu machen?

Wenn ja, wie geht das?

Beide Zweige sehen ähnlich aus, der einzige Unterschied sind die Werte der "Konstanten" ( y1, y2, b1, b2sind für alle Pixel in Pixel Shader gleich).

PolGraphic
quelle
1
Ehrlich gesagt, das ist eine sehr verfrühte Optimierung. Ändern Sie sie erst, wenn Sie Ihren Code bewertet haben und 100% ig der Shader ein Engpass ist.
Mittwoch,

Antworten:

17

Viele Regeln für die Mikrooptimierung von Shadern sind die gleichen wie für herkömmliche CPUs mit Vektorerweiterungen. Hier einige Hinweise:

  • Es gibt eingebaute Testfunktionen ( test, lerp/mix )
  • Das Hinzufügen von zwei Vektoren hat die gleichen Kosten wie das Hinzufügen von zwei Floats
  • Swizzling ist kostenlos

Es ist wahr, dass Filialen auf moderner Hardware billiger sind als früher, aber es ist immer noch besser, sie zu vermeiden, wenn dies möglich ist. Mit Swizzling- und Testfunktionen können Sie Ihren Shader ohne Tests neu schreiben:

/* y1, y2, b1, b2 */
float4 constants = float4(5, 6, 2, 3);

float2 tmp = 10 * constants.xy + constants.zw;
x = lerp(tmp[1], tmp[0], step(x, 0.5));

Die Verwendung von stepund lerpist eine sehr gebräuchliche Ausdrucksweise für die Auswahl zwischen zwei Werten.

sam hocevar
quelle
6

Im Allgemeinen ist es in Ordnung. Shader werden in Gruppen von Scheitelpunkten oder Pixeln ausgeführt (verschiedene Anbieter haben unterschiedliche Terminologie, daher halte ich mich davon fern). Wenn alle Scheitelpunkte oder Pixel in einer Gruppe denselben Pfad verwenden, sind die Verzweigungskosten vernachlässigbar.

Sie müssen auch dem Shader-Compiler vertrauen. Der von Ihnen geschriebene HLSL-Code sollte nicht als direkte Repräsentation des Bytecodes oder gar der Assembly angesehen werden, in die er kompiliert, und der Compiler kann ihn problemlos in etwas Äquivalentes konvertieren, vermeidet jedoch den Zweig (z. B. ein Lerp) eine bevorzugte Umwandlung). Wenn der Compiler hingegen feststellt, dass das Durchführen einer Verzweigung tatsächlich der schnellere Weg ist, kompiliert er sie bis zu einer Verzweigung. Das Anzeigen der generierten Assembly in PIX oder einem ähnlichen Tool kann hier sehr hilfreich sein.

Schließlich gilt hier immer noch die alte Weisheit: Profilieren Sie sie, stellen Sie fest, ob es sich tatsächlich um ein Leistungsproblem handelt, und packen Sie es dann an, nicht vorher. Unter der Annahme , dass etwas kann ein Leistungsproblem sein und Handeln nach dieser Annahme wird nur ein großes Risiko von größeren Problemen später entstehen.

Maximus Minimus
quelle
4

Zitat aus dem Link / Artikel von Robert Rouhani:

"Bedingungscodes (Prädikation) werden in älteren Architekturen verwendet, um eine echte Verzweigung zu emulieren. Wenn-Dann-Anweisungen, die zu diesen Architekturen kompiliert wurden, müssen sowohl genommene als auch nicht genommene Verzweigungsbefehle für alle Fragmente ausgewertet werden. Die Verzweigungsbedingung wird ausgewertet und ein Bedingungscode wird festgelegt Anweisungen in jedem Teil der Verzweigung müssen den Wert des Bedingungscodes überprüfen, bevor ihre Ergebnisse in Register geschrieben werden. Infolgedessen schreiben nur Anweisungen in genommenen Verzweigungen ihre Ausgabe. In diesen Architekturen kosten alle Verzweigungen so viel wie beide Teile der Verzweigung, zuzüglich der Kosten für die Bewertung der Verzweigungsbedingung. Verzweigung sollte bei solchen Architekturen sparsam eingesetzt werden. NVIDIA GeForce FX Series-GPUs verwenden in ihren Fragmentprozessoren die Emulation von Verzweigungsbedingungen. "

Wie von mh01 vorgeschlagen ("Das Anzeigen der generierten Assembly in PIX oder einem ähnlichen Tool kann hier sehr hilfreich sein."), Sollten Sie ein Compiler-Tool verwenden, um die Ausgabe zu untersuchen. Nach meiner Erfahrung lieferte das Cg-Tool von nVidia (das aufgrund seiner plattformübergreifenden Funktionen immer noch weit verbreitet ist) eine perfekte Darstellung des Verhaltens, das im Abschnitt " GPU-Gems- Bedingungscodes (Prädikation)" erwähnt wird . Unabhängig vom Auslösewert wurden daher beide Zweige fragmentweise ausgewertet, und erst am Ende wurde der richtige Zweig in das Ausgaberegister gestellt. Trotzdem wurde die Rechenzeit verschwendet. Damals dachte ich, dass Verzweigung die Leistung verbessern wird, vor allem, weil alleFragmente in diesem Shader beruhten auf einem einheitlichen Wert, um den richtigen Zweig zu bestimmen - das geschah nicht wie beabsichtigt. Also, eine große Einschränkung hier (z. B. Ubershader vermeiden - möglicherweise die größte Quelle der Höllenverzweigung).

Teodron
quelle
2

Wenn Sie noch keine Leistungsprobleme haben, ist dies in Ordnung. Die Kosten für den Vergleich mit einer Konstanten sind immer noch extrem günstig. Lesen Sie hier mehr über die GPU-Verzweigung: http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html

Unabhängig davon ist hier ein Codeausschnitt, der viel schlechter abschneidet als die if-Anweisung (und weitaus weniger lesbar / wartbar ist), den er jedoch immer noch entfernt:

int fx = floor(x);
int y = (fx * y2) + ((1- fx) * y1);
int b = (fx * b2) + ((1 -fx) * b1);

x = 10 * y + b;

Beachten Sie, dass ich davon ausgehe, dass x auf den Bereich beschränkt ist [0, 1]. Dies funktioniert nicht, wenn x> = 2 oder x <0 ist.

Das Ergebnis ist, x in entweder umzuwandeln 0oder 1das falsche mit 0 und das andere mit 1 zu multiplizieren.

Robert Rouhani
quelle
Da der Originaltest if(x<0.5)der Wert für fxsein soll round(x)oder ist floor(x + 0.5).
Sam Hocevar
1

Es gibt mehrere Befehle, die Bedingungen ohne Verzweigung ausführen können.

vec4 when_eq(vec4 x, vec4 y) {
  return 1.0 - abs(sign(x - y));
}

vec4 when_neq(vec4 x, vec4 y) {
  return abs(sign(x - y));
}

vec4 when_gt(vec4 x, vec4 y) {
  return max(sign(x - y), 0.0);
}

vec4 when_lt(vec4 x, vec4 y) {
  return max(sign(y - x), 0.0);
}

vec4 when_ge(vec4 x, vec4 y) {
  return 1.0 - when_lt(x, y);
}

vec4 when_le(vec4 x, vec4 y) {
  return 1.0 - when_gt(x, y);
}

Plus einige logische Operatoren;

vec4 and(vec4 a, vec4 b) {
  return a * b;
}

vec4 or(vec4 a, vec4 b) {
  return min(a + b, 1.0);
}

vec4 xor(vec4 a, vec4 b) {
  return (a + b) % 2.0;
}

vec4 not(vec4 a) {
  return 1.0 - a;
}

Quelle: http://theorangeduck.com/page/avoiding-shader-conditionals

Alexis Paques
quelle