Jump to content
Unity Insider Forum

Bitselect bei Floats - ComputeShader


Mr 3d

Recommended Posts

Hi,

Branching in einem ComputeShader sollte man ja so gut wie möglich vermeiden^

Deshalb setze ich fast ausschließlich auf bitselect (hab ich hier gefunden https://stackoverflow.com/questions/4911400/shader-optimization-is-a-ternary-operator-equivalent-to-branching )

 

Mit uints funktioniert das auch wunderbar:

bool cond = zahlA > zahlB;
uint bitmask = cond * 0xffffffff;
uint select = (optionTrue & bitmask) | (optionFalse & (~bitmask));

 

Aber wenn ich versuche ein float und ein uint bitwise zu vergleichen gibt es einen Fehler.

Obwohl das ja möglich sein müsste, sind ja beides 4byte..

 

Ich könnte natürlich auch die floats mit einem Faktor multiplizieren, zu uints casten und dann die Selektion durchführen.

Aber gibt es da noch eine "schönere" Lösung? :) 

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 7 Stunden schrieb Mr 3d:

Obwohl das ja möglich sein müsste, sind ja beides 4byte..

Schon. Aber bei float haben die jeweiligen bits nun mal ne andere Bedeutung.

Ich weiß gerad gar nicht, ob es Unions in Unity HLSL gibt (ich glaube schon), aber aus der C Welt wird gerne ein kleiner Trick für sowas angewendet:

typedef union 
{
  unsigned int i;
  float f;
 } u;

 u.f = 10.5;

// Member von Unions teilen sich den selben Speicherort.
// u.i greift nun also auf den Speicher von u.f zu.

/*
bitwise Vergleich von u.i und deinem anderem Zeug.
*/

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Zitat

Branching in einem ComputeShader sollte man ja so gut wie möglich vermeiden

Diese Aussage ist mit Vorsicht zu genießen ... Ich denke dies gilt nur unter ganz bestimmten Bedingungen und hängt von der Shadersprache und vom Compiler ab, man kann mit zu viel Optimierung auch den Code verschlechtern.

Ich habe eine kleine Shader-Testsuite erstellt, die beweißt, daß zumindest bei einer kleinen If-Abfrage die Performance im Shadercode sich nicht verschlechtert.

Verwendung:
Diesen Shader (mit zuvor erstelltem Material) auf eine Plane ziehen und die Plane so einstellen, daß sie den ganzen Bildschirm im Playmode einnimmt. Nun die "Instruction repeats" einstellen (z.b. 75000) und die "CPU: main" im "Stats Window" von Unity beobachten.

Über diese Zeilen kann man entweder Methode 1 oder Methode 2 einstellen:

				//result = instructionBlock2();
				result = instructionBlock1();

Bei mir sind beide Methoden in etwa gleich schnell ... Die GPU Performance schwankt extrem, daher sehr schwer zu messen.

Der Testshader:

Shader "Custom/TestPerf" {
		Properties{
			_InstructionRepeat("Instruction repeats", Range(0, 100000)) = 50000
			[HideInInspector]_MainTex("Albedo (RGB)", 2D) = "white" {}
		}
			SubShader{
			Tags{ "RenderType" = "Opaque" }
			LOD 200

			CGPROGRAM

			#pragma surface surf Standard fullforwardshadows
			#pragma target 5.0

			int _InstructionRepeat;
			sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		fixed4 _Color;

		float instructionBlock1() {
			// we solve the following if statement by a bit operation: if (zahlA > zahlB) result = 1.0f; else result = 0.25f;
			// later we will output a color value as result
			float result = 0; 									
			uint zahlA = 50;
			uint zahlB = 255;
			uint cond = zahlA > zahlB; // if TRUE, cond has all bits 1, if FALSE all bits 0
			uint condTrue = 1 & cond; // 1 if TRUE, 0 if FALSE
			uint condFalse = 1 & ~cond; // 0 if TRUE, 1 if FALSE
			
			result = condTrue * 1.0f + condFalse * 0.25f;
			return result;
		}

		float instructionBlock2() {
			float result = 0;
			uint zahlA = 50;
			uint zahlB = 255;
			if (zahlA < zahlB) result = 1.0; else result = 0.25;
			return result;
		}

		void surf(Input IN, inout SurfaceOutputStandard o) {
			
			// Do a simple loop for each pixel in screen space
			// Large surfaces decrease performane, so use same size 
			// of the object in screen size to measure performance
			float result = 0;
			for (uint i = 0; i < _InstructionRepeat; i++) {
				//result = instructionBlock1();
				result = instructionBlock2();
			}
			
			// simply output a grey color
			o.Albedo = float4(result, result, result,1.0);
		}
		ENDCG
		}
			FallBack Off
	}

PS:
Ansonsten war die "Originalmethode" mit floats nicht durchführbar da:

Zitat

Bitwise operators are defined to operate only on int and uint data types. Attempting to use bitwise operators on float, or struct data types will result in an error.

und asuint funktioniert wohl erst ab Shader Model4 und DX 11
https://msdn.microsoft.com/en-us/library/windows/desktop/bb509573(v=vs.85).aspx
Wobei ich aber auch eine Methode für "uint nach float" überhaupt nicht gefunden habe ...

Wäre mal interessant diesen Test auch noch einmal in einem Compute-Shader durchzuführen, wo man eine klare Zuweisung von Threads hat ...
Bei meinen Messungen war es quasi unmöglich zu bestimmen, welche der beiden Methoden schneller war, obwohl beide Methoden so oft wiederholt wurden ...

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

vor 19 Stunden schrieb Life Is Good:

Ich weiß gerad gar nicht, ob es Unions in Unity HLSL gibt (ich glaube schon), aber aus der C Welt wird gerne ein kleiner Trick für sowas angewendet:

Über diesen Trick bin ich auch schon gestolpert.. leider scheint die Unity DirectCompute / hlsl Sprache keine "union" zu kennen.. :/

 

@Zer0Cool

Meines Wissens ist das mit Branching auf der GPU so (hab ich mal irgendwo gelesen ^)..

Der Code wird in Threadgruppen ausgeführt. Nimmt ein (oder mehrere) Threads eine andere Abzweigung als die Anderen, so muss die gesamte Gruppe den Code noch einmal durchlaufen.

Und das so lange bis jeder Thread in der Gruppe einmal an seinem richtigen Ende angekommen ist.

 

In deinem Beispiel nehmen alle Threads die selbe Abzweigung, weshalb es keine Performanceprobleme geben sollte. (was dein Test bestätigen würde)

 

'asuint' bzw. 'asfloat' würde mein Problem lösen.. aber damit würde ich wahrscheinlich die Kompatibilität zu stark einschränken.. ( wäre auch zu schön gewesen :) )

 

Ich glaube ich werde einfach mit uints arbeiten müssen^

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Ich hab das ein wenig anders verstanden.

So wie ich es gelesen habe, kann bei einem "Branch" folgendes passieren:
- der Shader führt sowohl Zweig 1 und auch Zweig 2 aus
- der Shader führt nur Zweig 1 aus
- der Shader führt nur Zweig 2 aus

Der schlechteste Fall ist also, daß der Shader beide Zweige ausführen muss, da die parallelen Recheneinheiten der GPU nur jeweils die gleichen Instruktionen ausführen können.
Meine Erfahrung mit Computeshadern / Shader deutet auch in diese Richtung. Ideal für einen Computeshader ist: "Alles ist gleich", damit meine ich die Threads in den Threadgruppen führen Berechnungen für "ähnliche Inputs" aus.

Im idealen Fall kann aber die GPU das Branching ebenfalls intern optimieren (wie das intern umsetzt ist weiß niemand so genau) und damit führt ein Teil der Recheneinheiten den Zweig 1 aus und andere Einheiten den Zweig 2. Man kann daher nicht Vorhersagen, wie der Shader es intern regelt, daß hängt vom Hersteller, von der Shadergeneration und anderen Faktoren ab.

Allerdings wenn ich einen Zweig in einem Computeshader definiere, dann möchte ich dort auch einen Zweig haben und dieser lässt sich auch nicht "wegoptimieren".
Ein IF-Statement wie ich sie oben verwendet habe, werden durch den Compiler denke ich eh "wegoptimiert", d.h. ich vermute es entsteht überhaupt kein Branch in dem oben beschriebenen Sinne , sondern es wird ein einzelnes Statement intern erzeugt.

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Mal abgesehen, ob es jetzt performanter ist, oder nicht, hab ich glaub ich einen einfachen Weg gefunden diesen Float-Switch ohne Branching umzusetzen..

 

float switchFloats(float optionTrue, float optionFalse, bool condition){
	return lerp(optionFalse, optionTrue, (uint)condition);
}

bzw.

return optionFalse * (1 - (uint)condition) + optionTrue * (uint)condition;

 

Fast schon zu einfach.. oder übersehe ich grad was? ^

Link zu diesem Kommentar
Auf anderen Seiten teilen

Stimmt^ das hast du ja oben schon so geschrieben.. hätte ich mal genauer hinschaun sollen :D 

 

Ich weiß jetzt nicht, ob das ein Unterschied zwischen 'normalen' Shadern und ComputeShadern ist, aber aus meiner Erfahrung ist bei einem 'wahren'-Bool nur das letzte Bit gleich 1. (in einem ComputeShader)

vor 19 Minuten schrieb Zer0Cool:

(uint)~condition

würde dann zu 11111111 11111111 11111111 11111110 werden..

 

Habs grad mal getestet und scheint, zumindest im ComputeShader, nicht zu funktionieren.. 

 

(~condition & 1) 

klappt.. aber sind die beiden bitwise-Operatoren schneller als eine Subtraktion?

(edit: das '& 1' hast du ja auch oben geschrieben)

Link zu diesem Kommentar
Auf anderen Seiten teilen

Mhh ok dann

(~condition & 1) 
Zitat

klappt.. aber sind die beiden bitwise-Operatoren schneller als eine Subtraktion?

Schwer zu sagen, bei einer Bitoperation hätte ich gesagt ja, bei 2 bin ich eher auch skeptisch.... Ich denke auch der &-Operator ist teurer als das einfache logical not.

Da kann man mal ein C#-Pogramm schreiben und es testen, die Operationen auf GPU und CPU sollten hier zumindest ähnlich und damit vergleichbar sein, daß auf der GPU zu testen kann man denke ich vergessen... Ich könnte schon keinen Unterschied zwischen Methoden mit mehreren Operationen herausfinden, allerdings hatte ich hier auch einen normalen Shader genommen. Vielleicht kann man bei einem reinen Computeshader die Zeit besser messen, allerdings denk ich muss man da schon riesige Schleifen einbauen, damit die GPU ins Schwitzen kommt ...

Link zu diesem Kommentar
Auf anderen Seiten teilen

  • 2 months later...

Kleiner Nachtrag dazu:

Am 19.3.2018 um 03:58 schrieb Zer0Cool:

man kann mit zu viel Optimierung auch den Code verschlechtern.

Tatsache^^

Ich habe eben mal meinen "performanten" Zahlen-Switch-Code mit einfachen If-Abfragen ersetzt und mir den kompilierten Code angeschaut.

Der Compiler hat die Branches einfach wegoptimiert und der Code läuft jetzt knapp 20% schneller!

 

Also Notiz an mich selbst: einfach den Compiler die Arbeit machen lassen :D 

 

Link zu diesem Kommentar
Auf anderen Seiten teilen

Archiviert

Dieses Thema ist jetzt archiviert und für weitere Antworten gesperrt.

×
×
  • Neu erstellen...