Betragsfunktion ohne branching
Verfasst: 05.10.2024, 15:00
Weils jetzt wieder mal aktuell wurde und ich mir die Sache eigentlich schon länger anschauen wollte, habe ich mir jetzt die Zeit einfach genommen und die Quelle studiert, die Krishty damals vor Jahren verlinkt hatte (danke nochmals an der Stelle).
Dabei wurde das Thema ja eher durch Zufall gestreift - im Zusammenhang mit meinen Fragen zu Softwarepatenten.
Ursprünglicher Thread viewtopic.php?p=66488
Es geht darum, den Betrag einer Ganzzahl ohne branching zu ermitteln.
Krishty, du hattest in der Diskussion eine Technik dazu erwähnt:
Ich vermute allerdings, dass du sie verwechselt hattest (ein anderer Trick vielleicht?).
Gemeint hattest du wohl Folgendes (laut deiner verlinkten Quelle https://graphics.stanford.edu/~seander/ ... IntegerAbs ):
Patented variation:
(Der Patentversuch durch Sun Microsystems dürfte ungültig sein - siehe Link weiter oben)
Die ganze Idee setzt ja voraus, dass beim right shift auf signed ein arithmetischer und kein logischer Shift stattfindet - heisst, es müssen Bits entsprechend dem "Vorzeichenbit" nachgeschoben werden.
Ist v negativ, dann hat mask den Wert -1 (alle Bits sind 1).
Ist v positiv, dann hat mask den Wert 0.
Dieser Mechanismus ersetzt quasi das branching.
Doch ob bei right shift auf signed arithmetisch oder logisch geshiftet wird, ist ja bekanntlich Implementation-Defined Behavior - also abhängig vom Compiler (was die Quelle ja auch erwähnt).
In der Praxis dürfte dadurch die "Verwendbarkeit" der Technik ziemlich eingeschränkt sein (oder wie seht ihr das?).
Ich habe dann das Ganze anhand von 4-Bit Beispielen mal durchgerechnet, mit Augenmerk auf die Grenzfälle (4-Bit oder 32-Bit - das Konzept und die möglichen Probleme bleiben ja die selben).
1. Version
Im Falle von v = INT_MIN findet ein overflow bei der Addition mit mask statt. overflow, weil typ signed - und kein wrap-around wie bei unsigned. Folglich ist es ab hier UB.
4-Bit Beispiel
Erster Schritt: Addtion mit mask
Zweiter Schritt: XOR mit mask
Das Ergebnis wäre also "eigentlich" richtig, denn beim impliziten cast nach unsigned (r wurde als unsigned definiert), ist das Resultat 8.
Das Bitmuster bleibt (bei gleicher Bitbreite) von signed zu unsigned (und umgekehrt) ja stets das selbe.
Aus -8 wird also 8. Doch weil das Ganze durch die Addition bereits UB war, ist das Kind in den Brunnen gefallen.
In der Quelle wird das soweit auch erwähnt - doch weiter auch Folgendes:
2. Version ("Patentversuch")
Tatsächlich hat diese Version genau die selbe Problematik. Es tritt ein overflow auf (weil signed), was UB ist.
Beide Versionen sind also gleichermassen UB.
4-Bit Beispiel
Erster Schritt: XOR mit mask
Zweiter Schritt: Subtraktion mit mask
Auch hier wäre allerdings das Ergebnis rein rechnerisch richtig. Aus -8 wird 8.
Ich glaube allerdings für beide Versionen eine Lösung gefunden zu haben, wodurch diese definiertes Verhalten aufweisen.
aus:
wird:
Das funktioniert, weil der implizite cast nach unsigned int erst nach dem bitshifting ausgeführt wird. Für den Ausdruck rechts ändert sich also nichts. Fand vorhin ein arithmetischer Shift statt, findet dieser noch immer statt.
Weiter passiert dann Folgendes:
Variante 1.
v ist signed, mask jetzt allerdings unsigned. Dadurch wird v garantiert zu unsigned promoted (Vgl. C Standard "übliche arithmetische Typumwandlungs-Hierarchie. unsigned int "dominiert" signed int).
Dadurch, das v nun zu unsigned int promoted wurde, tritt jetzt kein overflow, sondern ein wrap-around auf - was bekanntlich wohl definiert ist.
Variante 2.
Auch bei Bitwise Operations werden die üblichen arithmetischen Typumwandlungen vorgenommen. (signed int) ^ (unsigned int) - v wird zu unsigned int promoted.
Mit dieser Lösung müssten sich jetzt beide Varianten ebenbürtig sein.
Rechnerisch hat sich nichts geändert, doch das Ganze resultiert jetzt maximal in einem wrap-around und nicht in einem overflow.
Ein expliziter cast ist durch die Regeln der integer promotion nicht nötig.
Vielleicht ist sogar die Variante 1. einen kleinen Tick eleganter, da eine Addition und keine Subtraktion stattfindet.
Ausserdem käme Variante 1. aufgrund der operator precedence auch ohne Klammern aus.
Die Problematik, dass das Ganze aufgrund des right shifts auf signed Implemetation-Defined ist, wird man aber leider nicht los.
Ich hoffe meine Überlegungen stimmen und ich habe nichts übersehen!
Vielleicht sind euch ja noch weitere Details aufgefallen?
Dabei wurde das Thema ja eher durch Zufall gestreift - im Zusammenhang mit meinen Fragen zu Softwarepatenten.
Ursprünglicher Thread viewtopic.php?p=66488
Es geht darum, den Betrag einer Ganzzahl ohne branching zu ermitteln.
Krishty, du hattest in der Diskussion eine Technik dazu erwähnt:
Code: Alles auswählen
(x ^ 0x80000000) - 0x80000000
Gemeint hattest du wohl Folgendes (laut deiner verlinkten Quelle https://graphics.stanford.edu/~seander/ ... IntegerAbs ):
Code: Alles auswählen
int v; // we want to find the absolute value of v
unsigned int r; // the result goes here
int const mask = v >> sizeof(int) * CHAR_BIT - 1;
r = (v + mask) ^ mask;
(Der Patentversuch durch Sun Microsystems dürfte ungültig sein - siehe Link weiter oben)
Code: Alles auswählen
r = (v ^ mask) - mask;
Ist v negativ, dann hat mask den Wert -1 (alle Bits sind 1).
Ist v positiv, dann hat mask den Wert 0.
Dieser Mechanismus ersetzt quasi das branching.
Doch ob bei right shift auf signed arithmetisch oder logisch geshiftet wird, ist ja bekanntlich Implementation-Defined Behavior - also abhängig vom Compiler (was die Quelle ja auch erwähnt).
In der Praxis dürfte dadurch die "Verwendbarkeit" der Technik ziemlich eingeschränkt sein (oder wie seht ihr das?).
Ich habe dann das Ganze anhand von 4-Bit Beispielen mal durchgerechnet, mit Augenmerk auf die Grenzfälle (4-Bit oder 32-Bit - das Konzept und die möglichen Probleme bleiben ja die selben).
1. Version
Code: Alles auswählen
r = (v + mask) ^ mask;
4-Bit Beispiel
Erster Schritt: Addtion mit mask
Code: Alles auswählen
1000 // 1000 ≙ -8
+ 1111 // 1111 ≙ -1 overflow: (-8) + (-1) = -9
-----
1 0111 // overflow
Code: Alles auswählen
0111 // 1000 ≙ 7
^ 1111 // 1111 ≙ -1
-----
1000 // -8 ≙ 8 (unsigned)
Das Bitmuster bleibt (bei gleicher Bitbreite) von signed zu unsigned (und umgekehrt) ja stets das selbe.
Aus -8 wird also 8. Doch weil das Ganze durch die Addition bereits UB war, ist das Kind in den Brunnen gefallen.
In der Quelle wird das soweit auch erwähnt - doch weiter auch Folgendes:
Diese Aussage kann ich in dieser Form nicht nachvollziehen.Hai Jin complained that the result was signed, so when computing the abs of the most negative value, it was still negative.
2. Version ("Patentversuch")
Code: Alles auswählen
r = (v ^ mask) - mask;
Beide Versionen sind also gleichermassen UB.
4-Bit Beispiel
Erster Schritt: XOR mit mask
Code: Alles auswählen
1000 // 1000 ≙ -8
^ 1111 // 1111 ≙ -1
-----
0111 // 0111 ≙ 7
Code: Alles auswählen
0111 // 1000 ≙ 7
- 1111 // 1111 ≙ -1 overflow: 7 - (-1) = 7 + 1 = 8
-----
1000 // -8 ≙ 8 (unsigned)
Ich glaube allerdings für beide Versionen eine Lösung gefunden zu haben, wodurch diese definiertes Verhalten aufweisen.
aus:
Code: Alles auswählen
int mask = v >> sizeof(int) * CHAR_BIT - 1;
Code: Alles auswählen
unsigned int mask = v >> sizeof(int) * CHAR_BIT - 1;
Weiter passiert dann Folgendes:
Variante 1.
Code: Alles auswählen
r = (v + mask) ^ mask;
Dadurch, das v nun zu unsigned int promoted wurde, tritt jetzt kein overflow, sondern ein wrap-around auf - was bekanntlich wohl definiert ist.
Variante 2.
Code: Alles auswählen
r = (v ^ mask) - mask;
Mit dieser Lösung müssten sich jetzt beide Varianten ebenbürtig sein.
Rechnerisch hat sich nichts geändert, doch das Ganze resultiert jetzt maximal in einem wrap-around und nicht in einem overflow.
Ein expliziter cast ist durch die Regeln der integer promotion nicht nötig.
Vielleicht ist sogar die Variante 1. einen kleinen Tick eleganter, da eine Addition und keine Subtraktion stattfindet.
Ausserdem käme Variante 1. aufgrund der operator precedence auch ohne Klammern aus.
Code: Alles auswählen
r = v + mask ^ mask;
Ich hoffe meine Überlegungen stimmen und ich habe nichts übersehen!
Vielleicht sind euch ja noch weitere Details aufgefallen?