Unions oder doch Bit-Shift und Bit-Operationen



  • @SeppJ sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Es gibt halt Sachen, die funktionieren praktisch immer, [...]

    Dann halte das eben wie Du willst. In einem Forum über Standard-C++ unions zum Type Punning zu empfehlen ist und bleibt ...

    @SeppJ sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    (und größtenteils auch sachlich)

    Liest sich wie ein Vorwurf ich wäre unsachlich geworden. Eigentlich warte ich noch auf eine Entschuldigung Deinerseits.



  • Ab C++20 gibt es dafür dann std::bit_cast<To, From>.



  • Ach @Swordfish, du bist auch nicht immer der höflichste. (Und bevor mich jmd. darauf hinweist, ich auch nicht, weiss ich eh.)



  • @hustbaer Ich bin nur zu Neulingen unhöflich *scnr* *duck 'n run*



  • Ich fasse also mal zusammen.
    Mein Union Konstrukt ist nicht im Standard verankert, funktioniert aber trotzdem. Wenn sehr wahrscheinlich auch auf wackeligem Boden.

    @SeppJ sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Exakt. Die Garantie lautet salopp gesagt: "Du darfst das machen, aber zeig da bloß nicht mit 'nem Pointer drauf!"<

    dann dürfte aber dieses nicht funktionieren?

    union {
    uint16_t HL; // 16Bit Register-Paar HL
    struct {
    uint8_t L; // 8Bit Register L (lower 8Bit)
    uint8_t H; // 8Bit Register H (upper 8Bit)
    };
    } regHL;

    inc8Bit(regHL.L);

    wenn inc8Bit folgendermaßen mit Referenzparameter deklariert ist:

    void inc8Bit(uint8_t &value);

    denn das entspricht doch einem Pointer auf die Daten mit denen gearbeitet wird.

    Übrigens, das ganze ist ein 8Bit Prozessor, welcher 2 8Bit-Register im Bedarfsfall zu einem 16Bit-Register zusammenfassen kann.

    Sorry wenn die Codeformatierung fehlt. Schreibe aus der Arbeit, und im Browser hier funzt das nicht. 😫


  • Mod

    Anstatt den Paragraph mit eigenen Worten wiederzugeben, verlinke ich mal: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Type-punning
    Und dann doch noch einmal mit eigenen Worten: GCC erlaubt type punning via unions (also Zugriff auf Daten über einen anderen Datentyp, als sie geschrieben wurden), aber wenn man dann wieder über einen Zeiger auf diese Daten zugreift, hat man strict Aliasing verletzt (man greift auf die Daten mittels eines unpassenden Zeigers zu) und es könnte zu Problemen kommen.

    Dein Beispiel ist nicht vollständig genug, um zu sagen, ob strict Aliasing verletzt ist oder nicht. Wenn der letzte Schreibzugriff auf regHL auf das HL Feld ging, dann ist der Zugriff über inc8Bit illegal. Wenn der letzte Zugriff auf das Sub-struct ging, ist das in Ordnung.



  • @Netzschleicher sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    dann dürfte aber dieses nicht funktionieren?

    Es darf alles funktionieren. Wenn der Standard sagt dass es nicht erlaubt ist, dann heisst das nur dass es nicht funktionieren muss. Es kann und darf aber zufällig doch gehen.

    Und ja, genau das von dir gezeigte Pattern ist in C++ ganz viel verboten (und auch in C und auch mit GCC). Weil eben Zugriff über Zeiger bzw. in deinem Fall halt über ne Referenz, was aber keinen Unterschied macht.



  • @hustbaer So ist es eben wenn man anfängt sich in den Fuß zu schießen. Zuerst mit der kleinkalibrigen Pistole ... und ehe man es merkt hat man eine Panzerfaust in der Hand ...



  • @Netzschleicher sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Sorry wenn die Codeformatierung fehlt. Schreibe aus der Arbeit, und im Browser hier funzt das nicht. 😫

    Du brauchst bloß drei Backticks (```) in eine Zeile vor und in eine Zeile nach Deinem Code schreiben.



  • Ich weiss nicht mehr genau welcher Vortrag es war, ich erinnere mich allerdings noch vage daran, dass Chandler Carruth auf die Frage, wie diese Problematik denn am besten zu lösen sei, mit memcpy geantwortet hat, das würde gut optimiert.

    memcpy-Ansatz

    Ich war neugierig und habe mal geschaut, was die Compiler da so generieren: https://godbolt.org/z/FsYGAb

    Für seinen Compiler, Clang, hat Carruth auf jeden Fall nicht zu viel versprochen. Aus

    r2 = set_hi(r2, get_lo(r1));
    r2 = set_lo(r2, get_hi(r1));
    

    wird hier einfach nur

    rol     ax, 8
    

    Eine simple Linksrotation des 16-Bit AX-Registers, bei der die Einsen und Nullen, die links herausgeschoben werden wieder rechts hineingeschoben werden.
    Wie geil ist das denn, was der da aus diesem Funktionsgeschachtel und den memcpy-Aufrufen macht? 😉

    Des weiteren arbeitet der Code, der für die Funktionen erzeugt wird, ausschliesslich auf Registern und kommt ohne Speicherzigriffe aus (Das lea eax, [rdi + rsi] in set_lo ist nur eine Adressberechnung, kein Speicherzugriff!).

    Leider erzeugen GCC und MSVC hier unnötige Speicherzugriffe in den set-Funktionen und somit auch im main-Code. Es scheint, als könnten diese das memcpynicht so gut wegoptimieren. Gut ist jedoch, dass diese bei den get-Funktionen ebenfalls nur auf Registern arbeiten.

    union-Ansatz

    Hier dasselbe mit union: https://godbolt.org/z/F1F96h

    Clang erzeugt hier identischen Code 👍, GCC kommt hierbei jezt ohne die zusätzlichen Speicherzugriffe aus (gut!), berechnet das Ergebnis aber auf anderem Wege, unter Verwendung von ah/al etc., der "hi"/"lo"-8-Bit-Register, welche die x86-Architektur zur Verfügung stellt (Das ist auch gut!). MSVC leidet hierbei leider immer noch unter zu vielen Speicherzugriffen... Schade.

    shift-Ansatz

    ... und nochmal mit Shift und Bitweisen Operationen: https://godbolt.org/z/EmW1aX

    Wieder identischer Code mit Clang - schon beeindruckend wie gut dieser Compiler hier begreift, dass dieser ganze Code äquivalent ist 👍. GCC erzeugt hier den selben Code wie beim union-Ansatz, auch gut! MSVC baut mir hier wieder zu viele Speicherzugriffe ein, auch wenn es auf den ersten Blick ebenfalls nach identischem Code aussieht.

    Fazit

    Zumindest für meinen Testfall hier, scheinen die Shift- und union-Ansätze gleichwertigen Code zu erzeugen. Mit memcpy glänzte hier leider nur Clang.

    Alles in allem ist dennoch Vorsicht angebracht: Mein Test ist nicht sonderlich umfangreich und ist daher nicht geeignet, ein vollständiges Bild zu liefern... ich denke man bekommt aber schon so ein paar Ideen.

    @TGGC Die verscheidenen Tests zeigen auch ein wenig, was ich mit Astraktion meine und warum man nicht "tausende Bitshifts" überall im Code benötigt. Meine "riesige main()-Codebase" musste ich hier gar nicht anfassen, ich habe für jeden Testfall lediglich die Abstraktion angepasst 😉


  • Mod

    So wirklich interessant würde das aber erst bei Code für einen 8-Bit Prozessor. Oder wenn du statt 16 Bit in 2x8 zu zerlegen, du bei dem hier benutzten 64-Bit Zielprozessor 128 Bit in 2x64 zerlegst. Dann ist das mit den Optimierungen nämlich vermutlich nicht mehr so einfach.

    PS: Das ist mir selber aber gerade im Moment zu viel Aufwand, Finnegans Programme darauf umzuschreiben, da es keinen nativen 128-Bit Typen gibt. Aber wenn jemand Zeit und Lust hat, würde mich das Ergebnis sehr interessieren.
    PPS: Oder wenn man -m32/-m16 benutzt und dann das "Register" auf 64 bzw. 32 Bit setzt, sollte das den gleichen Effekt haben, oder?



  • @Finnegan sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    @TGGC Die verscheidenen Tests zeigen auch ein wenig, was ich mit Astraktion meine

    Du hast aber auch nur einen absolutes Minimum an Funktionalität eingebaut. Der Compiler kann Unions aus beliebig vielen structs, die wiederum beliebig viele uints mit einer beliebiger Bitzahl enthalten können. Folgen wir deinem Schema musst du im allgemeinen Fall für jeden der Member zwei Funktionen mit jeweils einem Shift und einem And und den zwei Magical Values programmieren.

    Ich schaue gerade auf eine struct mit 24 Member, das wären 48 Funktionen und 96 Operationen und 96 Magical Values. Noch nicht eingerechnet, wie oft sich diese Sachen im Laufe der Entwicklung geändert hätten, weil man die Member ordnet erweitert oder entfernt. Für eine einzige Struct. Diese Variante ist für mich daher komplett praxisfremd.



  • @SeppJ sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    da es keinen nativen 128-Bit Typen gibt

    GCC, Clang, ICC: __int128



  • @SeppJ sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Aber wenn jemand Zeit und Lust hat, würde mich das Ergebnis sehr interessieren.

    https://godbolt.org/z/O7GROY


  • Mod

    @hustbaer sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    @SeppJ sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    da es keinen nativen 128-Bit Typen gibt

    GCC, Clang, ICC: __int128

    Ausgezeichnet.

    Shift-Variante:
    https://godbolt.org/z/w7jKTb

    Union-Variante:
    https://godbolt.org/z/EoCE41

    Gleiche Beobachtung wie von Finnegan bei 8-Bit gemacht: Die Shift-Variante ist genau so gut wie die Union-Variante, d.h. es wird alles maximal optimiert zu zwei simplen mov. Beim GCC ist die memcpy-Variante etwas schlechter, CLang kann auch diese optimieren. Beeindruckend. Insbesondere da man die Bitmaske für das High-Word gar nicht mehr als Literal hinschreiben kann, aber selbst mit der Zwischenrechnung wurde alles perfekt optimiert.

    Swordfishs Vorschalg kann man natürlich nicht mehr zum Vergleich ranziehen, weil seine überlegene Variante nicht mehr für > 8 Bit funktioniert.



  • @hustbaer sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    https://godbolt.org/z/O7GROY

    Das geht nun wirklich nicht besser, ohne die Aufrufkonvention zu verletzen. Nice 😉

    Warum hingegen GCC die Parameter-Register erst auf den Stack schreibt, nur um sie dann direkt wieder in die Rückgabe-Register einzulesen verstehe ich wirklich nicht: https://godbolt.org/z/vpyYQY
    Da sieht man mal wieder dass Optimierung oft derart ausgeklügelt ist, dass sie bei so simplen Dingen versagt. Etwas superkomplexes hätte er vielleicht brilliant gelöst 😉

    @SeppJ sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Shift-Variante:
    https://godbolt.org/z/w7jKTb

    Union-Variante:
    https://godbolt.org/z/EoCE41

    Interessanterweise bekommt der GCC hierbei auch das simple mov hin. Nur bei memcpy verschluckt er sich, was mich wieder in der vermutung bestärkt, dass die Empfehlung von Chandler Carruth doch sehr "clang-spezifisch" war 😉

    @TGGC sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Ich schaue gerade auf eine struct mit 24 Member, das wären 48 Funktionen und 96 Operationen und 96 Magical Values.

    Das sieht mir schon nach einem massiveren Einsatz von Type Punning via union aus. Vielleicht ist das ja gut handhabbar ein deinem Code, aber wenn ich das so lese, wäre mir dabei etwas unwohl, dass ich nicht irgendwo einen Union-Zugriff einbaue, der trotzdem vor die Wand läuft.
    Was passiert da? Irgendein Zusammenbau von Datenpaketen oder Speicherblöcken für eine Hardware?



  • In dem Fall wird eine Anzahl int Werte speichereffizient abgespeichert und später aus dem Speicher gelesen, verglichen etc.

    Ich hab auch noch ein anderes Real World Beispiel, nur Variablennamen angepasst, sonst rauskopiert.

    /*!
        0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
        ^  ^  ^                                ^  ^                                                   ^  ^                                                                                                 ^
        |    | |_____________     ___________|  |_____________________   __________________________|  |____________________________________   ________________________________________________________|
        |  |                 12 Bit                                    16 Bit                                                                32 Bit
        |  |                   |                                        |                                                                    |
        |  |                 Wert A                                   Wert B                                                            Wert C
        |    Flag B
        Flag A
    */
    	union
    	{
    		struct
    		{
    			uint32		WertC : 32;
    			AndereID	WertB;
    			uint16		Unused : 2;
    			uint16		WertA : 12;
    			uint16		FlagB : 1;
    			uint16		FlagA : 1;
    		};
    
    		uint64			Data;
    	};
    
    

    Hier wird das ganze sogar verschachtelt benutzt. AndereID ist wieder ein solcher Datentyp. Data wird zum schnellen vergleichen benutzt, zum Einordnen in Hashmaps, zum sortieren um Reihenfolgeprobleme zu lösen. Eins der Flags ist z.B. nötig um zu sagen, welchen ID nur auf dem Rechner gültig ist und welche global im Netzwerk gültig sind. Andere Teile damit Threads oder gar verschiedenen User an verschiedenen PCs unabhängig IDs generieren können, die nicht kollidieren. Früher hatten wir nur Data und damit rumgeshiftet, seit dem die Komplexität höher wurde und wir von 32 auf 54 gingen, haben wir das gelassen.

    Der Code ist übersichtlicher und genauer als der Kommentar, zumindest für mich. Zur Compilezeit sind einige Basics durch static asserts abgesichert, die vor den größten Dummheiten schützen sollen, aber nicht wirklich relevant sind aus meiner Sicht. Probleme gabs noch nie, würden aber sofort auffallen, da quasi alles darauf aufbaut. In der täglichen Arbeit kann ich mich darauf verlassen, das der Code simpel aussieht, lesbar ist und verlässlich und effizient tut, was wir wollen.

    Und zu Chandler Carruth bei einem verwandten Thema: https://www.youtube.com/watch?v=vElZc6zSIXM&t=3235
    "Its terrifying [...] But use them, its less terrifying than writing math yourself and hope to get it right"



  • Seh ich jetzt erst:

    @Th69 sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Ab C++20 gibt es dafür dann std::bit_cast<To, From>.

    Wie hilft das bei vorliegendem Problem?



  • @Swordfish Naja mit bit_cast kannst du memcpy ohne memcpy machen. D.h. du kannst damit z.B. nen uint64_t in einen gleich grossen (trivialen) struct konvertieren. Siehe https://en.cppreference.com/w/cpp/numeric/bit_cast

    Der Vorteil dabei ist: das kann zwar über memcpy implementiert sein, es muss aber nicht. Damit können die Jungs die die Standard-Library schreibseln es so schreibseln dass der Compiler versteht dass hier nichts "in den Speicher gezwungen" werden muss sondern bloss bytes "uminterpretiert" werden.

    EDIT: den/dem Fehler korrigiert 🤦♂ . Naja, ich sag mal die Hitze is schuld 🙂



  • @hustbaer sagte in Unions oder doch Bit-Shift und Bit-Operationen:

    Naja mit bit_cast kannst du memcpy ohne memcpy machen. D.h. du kannst damit z.B. nen uint64_t in einen gleich grossen (trivialen) struct konvertieren.

    Ach so, ja. Ich dachte ständig an einen kleineren Typ für High and Low. Mit einer struct wird da schon ein Schuh draus.


Log in to reply