Seite 1 von 1

[C++] Bitweise Reloaded - Zuverlässiges Overflow/Underflow?

Verfasst: 11.12.2015, 11:32
von Schrompf
Tach,

ja, ich wieder. Ich weiß, dass Integer-Overflow und -Underflow nach C++-Standard Unspecified Behavior sind. Leider würde es mir bei einigen meiner zentralen Bitschubsereien sehr nützen, wenn ich mich auf Zweierkomplement-Effekte verlassen könnte.

Beispiel:

Code: Alles auswählen

uint64_t MachEinser(size_t numBits) { 
  return (uint64_t( 1ull) << numBits) - uint64_t( 1); 
}
Mit numBits == 4 z.B. ergibt das Bitschieben ein b00010000, das minus eins ergibt dann b00001111 - prima. Mit numBits == 64 sprengt der Bitschieber den Integertyp und ergibt 0, das minus eins ergibt dann das gewünschte 0xffffffffffffffff. Aber nur, wenn man sich auf den Overflow/Underflow verlassen kann. Oben zitierte Funktion ergibt hier auf Ubuntu 12 und dem GCC 4.8 für numBits == 64 eine 0. Und 0 ist falsch.

Die Frage lautet also: gibt es irgendwelche Tricks, mit denen ich dem Compiler beibringen kann, Zweierkomplement-Magie zuverlässig auszuführen? Danke.

Randnotiz: die selbe Frage habe ich wieder im sppro gepostet, der dortige Thread findet sich hier

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 12:21
von FlorianB82
Moin,

keine Lösung, aber Hintergrundinfos: der Integer-Overflow und -Underflow ist hier nicht dein Problem. Der ist nämlich für unsigned Integertypen wohldefiniert. Für signed-Typen ist er dagegen unspecified (immerhin nicht undefined, aber viel hilft uns das nicht).

Dein Problem ist aber das Shifting - das ist nämlich nur für Shift-Werte kleiner der Bitanzahl des Zieldatentyps (in deinem Fall 64) definiert, ansonsten undefined. Siehe zum Beispiel hier.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 12:27
von Krishty
Mein erster Gedanke:

  return ~0ull >> (64 - numBits);

Rechtsschieben ist AFAIK definiert, bei unsigned Nullen aufzufüllen (nur signed ist undefiniert). Auch Über- und Unterläufe sind übrigens für unsigned definiert (müssen Zweierkomplement folgen). Florian war schneller.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 12:32
von Schrompf
Der Link ist spannend. Darin heißt es u.A.
The x86 processor performs a "shiftcount % bits" on the shift value
Hm. Das würde ja bedeuten, dass Visual Studio da bisher extra Code eingebaut hat, um für Shifts größer als Bitbreite das Ergebnis "0" zu produzieren? Und das wiederrum würde bedeuten, dass ich in all meine Bitschubser-Funktionen Zusatz-Bedingungen einbauen müsste, wo ich in diesen Minifunktionen doch ohne Conditionals auskommen wollte. Grmpf. Naja, vielleicht als ?: implementieren, dann wird's halbwegs zuverlässig ein conditional move.

Interessant @Krishty. Danke auch Dir. Rechtsschieben um 64 ist also definiert, linksschieben um 64 aber nicht? Seltsam.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 13:12
von Krishty
Schrompf hat geschrieben:
The x86 processor performs a "shiftcount % bits" on the shift value
Hm. Das würde ja bedeuten, dass Visual Studio da bisher extra Code eingebaut hat, um für Shifts größer als Bitbreite das Ergebnis "0" zu produzieren?
Sofern kein __assume(count < 64); davor steht (oder sich das aus der Berechnung des Parameters ergibt), war das bei meinem letzten Blick darauf der Fall, ja.
Danke auch Dir. Rechtsschieben um 64 ist also definiert, linksschieben um 64 aber nicht? Seltsam.
Nein; dürfte ebenfalls Implementation Defined sein; da hast du recht. Was ist denn der Werteumfang des Parameters? 0–64, 0–63, 1–63, 0–INT_MAX ist ja alles möglich, aber nicht immer sinnvoll; erklär mal, was du brauchst :)

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 13:44
von Schrompf
Gerne. In diesem konkreten Fall brauche ich den Zahlenbereich (0, 64) - war die Notation so? Ich meine: von einschließlich 0 bis einschließlich 64.

Dann gibt es da die Funktion "Finde das höchste gesetzte Bit". Die sieht auf GCC so aus:

Code: Alles auswählen

/// Ermittelt den Index des höchsten gesetzten Bit (von 0 an zählend) aus der gegebenen Zahl, oder gibt SIZE_MAX zurück bei kompletter Leere
inline size_t FindeHoechstesBit(uint64_t z)
{
   static_assert(sizeof(z) == 8, "Hardcoded auf 64bit");
  // absichtlicher Overflow: falls z == 0, ergibt clz(z) 64, demzufolge ergibt die Subtraktion -1
  return size_t( 63) - size_t( __builtin_clzll(z));
}
"clz" steht wohl für "Count Leading Zeros". Die Funktion soll für den kompletten uint64_t-Range funktionieren, natürlich nur unsigned. Das funktioniert so ohne Conditional und alles, solange ich mich darauf verlassen kann, dass 63 - 64 auch SIZE_MAX ergibt.. Aber wenn ich euch richtig verstehe, ist Integer Overflow für unsigned-Typen wohldefiniert, also müsste das klappen.

Dann gibt es da die Funktion "Finde das niedrigste gesetzte Bit". GCC-Version:

Code: Alles auswählen

/// Ermittelt den Index des niedrigsten gesetzten Bit (von 0 an zählend) aus der gegebenen Zahl, oder gibt SIZE_MAX zurück
inline size_t FindeNiedrigstesBit(uint64_t z)
{
  return size_t(__builtin_ffsll(z)) - 1;
}
"ffs" steht wohl für "Find First Set". Ebenso für kompletten uint64_t-Bereich, ebenso nur unsigned. Und auch die Funktion funktioniert so nur, wenn 0 minus 1 auch SIZE_MAX ergibt.

Und zum Schluss noch die "Zähle gesetzte Bits"-Funktion:

Code: Alles auswählen

/// Ermittelt die Anzahl gesetzter Bits in der gegebenen Zahl
inline size_t GetAnzahlEinsen(uint64_t z)
{
  return size_t(__builtin_popcountll(z));
}
Da gibt's nix zu Grübeln dran.

Die VC-Versionen davon sind je nach Define entweder per Intrinsic oder über die alten Bit-Tricks von dieser einen Webseite implementiert. Mit "Intrinsics" meine ich hier allerdings _BitScanReverse() und Konsorten - es gäbe wohl noch "richtige" Intrinsics für diesen Job. Nur das Einsen-Durchzählen ist schon ein __popcount() und hatte mir damals auch prompt einen Strauß Crashes mit der Vor-Steam-Version von Splatter gebracht, weil ich zu arglos SSE4 vorausgesetzt habe. Seitdem gibt es die Bithack-Version für ältere Prozessoren.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 14:00
von Krishty
Schrompf hat geschrieben:Gerne. In diesem konkreten Fall brauche ich den Zahlenbereich (0, 64) - war die Notation so? Ich meine: von einschließlich 0 bis einschließlich 64.
Dann [0, 64]; runde Klammern sind exklusive (kann man sich merken, weil eckige mehr Platz bieten :) )
// absichtlicher Overflow: falls z == 0, ergibt clz(z) 64, demzufolge ergibt die Subtraktion -1
[…]
"clz" steht wohl für "Count Leading Zeros". Die Funktion soll für den kompletten uint64_t-Range funktionieren, natürlich nur unsigned. Das funktioniert so ohne Conditional und alles, solange ich mich darauf verlassen kann, dass 63 - 64 auch SIZE_MAX ergibt.. Aber wenn ich euch richtig verstehe, ist Integer Overflow für unsigned-Typen wohldefiniert, also müsste das klappen.
Ich lese aus der Dokumentation:
Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
Dass da 64 zurückgegeben wird, scheint also Glück zu sein und dürfte irgendwann kaputtgehen. FindeNiedrigstesBit() dürfte hingegen weiter funktionieren.

Sieht für mich danach aus, dass du Verzweigung oder Conditional Move einbauen musst (habe gerade nicht die übliche Zeit, über Bit Twiddling Hacks nachzugrübeln) – oder halt den umgebenden Algorithmus modifizieren musst, damit MachEinser(64) und FindeHoechstesBit(0) nicht vorkommen.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 15:07
von Schrompf
Danke. Den Teil in der Doku hatte ich übersehen. Ich bau es jetzt jeweils mit einem Conditional Move und schaue mir die Bithack-Möglichkeit nochmal an, sobald das Gesamtprojekt weit genug ist, damit ich realistische Datensätze zur Verfügung habe.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 15:34
von Alexander Kornrumpf
Krishty hat geschrieben:
Schrompf hat geschrieben:Gerne. In diesem konkreten Fall brauche ich den Zahlenbereich (0, 64) - war die Notation so? Ich meine: von einschließlich 0 bis einschließlich 64.
Dann [0, 64]; runde Klammern sind exklusive (kann man sich merken, weil eckige mehr Platz bieten :) )
Oder alternativ:

Die Notation (a,b) ist die traditionell verwendete, während ]a,b[ auf Bourbaki zurückgeht[2].
https://de.wikipedia.org/wiki/Intervall ... hematik%29

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 11.12.2015, 22:44
von dot
Schrompf hat geschrieben:ja, ich wieder. Ich weiß, dass Integer-Overflow und -Underflow nach C++-Standard unspecified Behavior sind. Leider würde es mir bei einigen meiner zentralen Bitschubsereien sehr nützen, wenn ich mich auf Zweierkomplement-Effekte verlassen könnte.
Wie schon gesagt wurde, garantiert der Standard Moduloarithmetik für unsigned und Bitshifts sind für unsigned in beide Richtungen wohldefiniert (so lange der zweite Operand positiv ist; ein negativer Shift ist selbstverständlich immer automatisch UB). Eine Garantie für Zweierkomplementdarstellung gibt es dagegen nicht (einzige Ausnahme sind die exact-width integer types std::intN_t, Overflow/Underflow ist bei signed aber immer UB). Für signed ist der Linksshift UB wenn er überläuft, der Rechtsschift von negativen Werten ist implementation-defined (d.h. es gibt ein wohldefiniertes Verhalten und die Implementierung muss sich – im Gegensatz zu unspecified behavior – konsistent verhalten und dokumentieren, was genau passiert).

Zur eigentlichen Frage, wie schon auf der anderen Seite:

Code: Alles auswählen

#include <type_traits>

template <typename T>
inline constexpr std::enable_if_t<std::is_unsigned<T>::value, T> gimme(int n)
{
    return ~(~T() << n);
}

auto x = gimme<unsigned>(5);
;)

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 00:33
von Krishty
Habe ich wieder einen Brainfart, oder ist gimme<unsigned>(99999) genau so undefined wie das, womit Schrompf begonnen hat?

Zentrales Problem ist ja, dass der Shift Count >= Anzahl Bits im Datentyp sein kann … nicht der Underflow, von dem Schrompf fälschlicherweise dachte, dass er schuld wäre.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 00:48
von Schrompf
Naja, es muss ja nicht gleich 99999 sein. Coole Idee, klappt zumindest für [0, bitcount]. Ist aber leider auch wieder Unspecified Behaviour, auch wenn es in der Praxis wohl für x86-Bitshift funktionieren sollte. Kann aber gut sein, dass CLANG das zum Anlass nimmt, komplette Codepfade rauszuoptimieren, wie es das gelegentlich mal tut.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 01:09
von Krishty
Nein, leider ist es nicht unspecified sondern undefined.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 01:20
von dot
Krishty hat geschrieben:Habe ich wieder einen Brainfart, oder ist gimme<unsigned>(99999) genau so undefined wie das, womit Schrompf begonnen hat?

Zentrales Problem ist ja, dass der Shift Count >= Anzahl Bits im Datentyp sein kann … nicht der Underflow, von dem Schrompf fälschlicherweise dachte, dass er schuld wäre.
Ja, da hast du natürlich recht, das ist mir irgendwie entgangen...

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 01:30
von Krishty
Bei der ganzen Pedanterie: Unter Visual C++ würde ich’s auch wirklich so machen, wie du vorgeschlagen hast – funktioniert perfekt mit dem x86-Befehl; Visual C++ optimiert kein Undefined Behaviour weg; der Standard ist mir eigentlich schnurze. Aber GCC und Clang sind dafür berüchtigt, in solchen Fällen richtig was kaputt zu machen.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 04:35
von dot
Hab noch etwas drüber nachgedacht; ich glaub, dass wir evtl. besser beraten sind, wenn wir das eigentliche Problem etwas weiter oben anfassen. Daher frag ich einfach mal: Wofür genau brauchst du diese Funktion und wieso genau der Wertebereich [0, 64]?

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 12:39
von Schrompf
Mit der Funktion erstelle ich Bitmasken und sowas, um auf Gruppen von Voxeln gleichzeitig zu arbeiten. Nach mäßig intensivem Profilen habe ich damals gelernt, dass die vielen If() eine Bremse darstellen, weswegen ich jetzt kleinere Operationen auf Bitschubsereien umgebaut habe. 8 bytegroße Voxel passen in einen uint64_t und können z.B. mit ein bisschen xor-Magie gleichzeitig ersetzt, durchgezählt oder sonstwas werden. Andere Funktionen wie z.B. die Mesh-Erstellung konstruieren 1bit-pro-Voxel-Präsenztabellen, ob der jeweilige (lodgestufte) Voxel präsent genug ist, um gerendert zu werden. Da kann man den mühsamen "Ist der Voxel rundrum verdeckt"-Test durch eine Serie AND/OR ersetzen und abartig viele Branches einsparen.

Ein uint64_t hat halt 64bits. Und wenn ich nach dynamischen Parametern Bitmasken darin erstelle, kann ich halt auch "0 Bits" und "alle Bits" nicht ausschließen. Wobei "0 Bits" im Kontext betrachtet wahrscheinlich schon vorher ausgefiltert werden. 64Bits allerdings nicht.

Hm. Wahrscheinlich wäre das ein Fall für ein assert()/assume() und ich könnte mir das cmov darin wirklich ersparen. Aber aktuell geht es mir primär darum, verlässliches Verhalten herzustellen, nicht das letzte Quentchen Performance rauszuholen. Mein Hadern mit dem cmov ist nur Premature Optimization :-)

[edit] Und ich muss ehrlich zugeben: ich habe schon wieder unerträglich viel gelernt. Danke euch allen für Eure Zeit und Geduld! Auch wenn ich mich dabei ein bisschen wie ein Dinosaurier fühle, der kurz vor dem Meteoriteneinschlag noch schnell lernt, mit links das Messer zu führen.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 19:13
von dot
Schrompf hat geschrieben:Mit der Funktion erstelle ich Bitmasken und sowas, um auf Gruppen von Voxeln gleichzeitig zu arbeiten. Nach mäßig intensivem Profilen habe ich damals gelernt, dass die vielen If() eine Bremse darstellen, weswegen ich jetzt kleinere Operationen auf Bitschubsereien umgebaut habe. 8 bytegroße Voxel passen in einen uint64_t und können z.B. mit ein bisschen xor-Magie gleichzeitig ersetzt, durchgezählt oder sonstwas werden. Andere Funktionen wie z.B. die Mesh-Erstellung konstruieren 1bit-pro-Voxel-Präsenztabellen, ob der jeweilige (lodgestufte) Voxel präsent genug ist, um gerendert zu werden. Da kann man den mühsamen "Ist der Voxel rundrum verdeckt"-Test durch eine Serie AND/OR ersetzen und abartig viele Branches einsparen.
Ok, d.h. du willst effektiv Bitmasken bauen, in denen die Bits m bis n auf 1 sind und der Rest 0? Aus deiner Beschreibung würde ich jetzt mal vermuten, dass du einige Dinge bereits zur Compilezeit kennst und nicht erst zur Laufzeit!? Beispielsweise wie viele Bits deine Bitmaske auf 1 haben soll!?
Schrompf hat geschrieben:[edit] Und ich muss ehrlich zugeben: ich habe schon wieder unerträglich viel gelernt. Danke euch allen für Eure Zeit und Geduld! Auch wenn ich mich dabei ein bisschen wie ein Dinosaurier fühle, der kurz vor dem Meteoriteneinschlag noch schnell lernt, mit links das Messer zu führen.
lol

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 20:45
von Schrompf
dot hat geschrieben:Ok, d.h. du willst effektiv Bitmasken bauen, in denen die Bits m bis n auf 1 sind und der Rest 0? Aus deiner Beschreibung würde ich jetzt mal vermuten, dass du einige Dinge bereits zur Compilezeit kennst und nicht erst zur Laufzeit!? Beispielsweise wie viele Bits deine Bitmaske auf 1 haben soll!?
Manchmal kenne ich die Parameter zur Bauzeit, aber meistens entstehen die dynamisch aus irgendwelchen Kompressionsmethoden. Ich verlasse mich da auf den Compiler, dass der bei so winzigen Funktionen erkennt, wenn das Ergebnis zur Compile Time feststeht.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 21:09
von Spiele Programmierer
Steht teilweise zur Compilezeit fest? -> constexpr-Variable.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 22:40
von Krishty
Und wenn man constexpr nutzt, wird undefiniertes Verhalten plötzlich definiert? Zumal sie dann ja noch stärker eingeschränkt sind, was Sonderfälle angeht ...

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 12.12.2015, 23:08
von Spiele Programmierer
Ne natürlich nicht.
Dann kann man sicher aber keine Sorgen wegen einem if in der Methode mehr machen.
Außerdem generell eine gute Idee um zwecks Performance sicher zu gehen, dass der Compiler die Methode auswertet.

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 13.12.2015, 01:49
von dot
Naja, constexpr an sich stellt nur sicher, dass etwas zur Compiletime ausgewertet werden kann, constexpr allein ist noch keine Garantie dafür, dass es auch wirklich zur Compiletime ausgewertet wird; der einzige Weg, um das zu forcieren, ist, das constexpr Ding in einem Kontext zu verwenden, der zur Compiletime ausgewertet werden muss... ;)

Abgesehen davon, mal folgender Gedankengang: Wir können jede Zahl \($n$\) auch schreiben als \($n = \frac{n}{2} + \frac{n}{2} + n \bmod 2$\); mit anderen Worten:

Code: Alles auswählen

#include <type_traits>
#include <cstdint>

template <typename T>
constexpr std::enable_if_t<std::is_unsigned<T>::value, T> gimme(unsigned int n)
{
  return ~(~T() << (n / 2) << (n / 2) << (n % 2));
}

auto x_0 = gimme<std::uint64_t>(0);
auto x_5 = gimme<std::uint64_t>(5);
auto x_64 = gimme<std::uint64_t>(64);
sollte für \($n \in [0, 2N-1]$\) standardkonform sein, da kein einzelner Shift um die Anzahl \($N$\) der Bits in T passieren kann...

Ich würde dennoch irgendwie versuchen, irgendwas am Ganzen Ding zu ändern, um den Wertebreich von \($n$\) auf \($[0, 63]$\) runterzubekommen...

PS: LaTeX ist wohl broken?

Re: [C++] Bitweise Reloaded - Zuverlässiges Overflow/Underfl

Verfasst: 13.12.2015, 13:01
von Schrompf
Mehrstufige Shifts... ebenso schlichte wie gute Idee. Aber wie Du schon schriebst, guck ich eher mal, dass ich den Wertebereich einschränken kann. Den Fall 64 kann ich kaum ausschließen, aber wahrscheinlich durchaus die 0. Und für den Fall (0, 64] hatten wir am Anfang dann ja schon eine Implementation.