Unions oder doch Bit-Shift und Bit-Operationen



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

    Sorry, aber was soll daran bitte übermäßig fehleranfällig sein?
    Ich redete nirgends vom übermässig. Aber fehleranfällig ist jeder Code, also werden zwangsweise auch hier Fehler auftreten. Mir fällt jetzt direkt kein Beispiel ein, aber ich bin sicher wenn du dir ein paar Sekunden Zeit nimmst, findest du selber was...

    Mir ist völlig klar, das den Compilerherstellern erlaubt ist hier irgendwas einzubauen und nachdem ich Update x einspiele passiert auf einmal nicht mehr was ich erwarte. Bevorzuge ich trotzdem gegenüber dem manuelle Geshifte.



  • @TGGC Du darfst das gerne bevorzugen. Bloss bitte empfehle es niemand anderem! Es gibt schon genug Leute die von sich aus kaputtes C++ schreiben, da muss man nicht noch nachhelfen.



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

    Mir ist völlig klar, das den Compilerherstellern erlaubt ist hier irgendwas einzubauen und nachdem ich Update x einspiele passiert auf einmal nicht mehr was ich erwarte. Bevorzuge ich trotzdem gegenüber dem manuelle Geshifte.

    "Manuell" wäre auch nicht die beste Lösung. Aber gerade in C++ sind doch schöne leichtgewichtige Abstraktionen möglich, die beim Kompilieren in genau die Instruktionen zerfallen, die man gerne haben möchte.

    Ich bin sicher, dass man hierfür eine schicke Register-Klasse bauen könnte, die sich mindestens so schön verwenden lässt wie das Union-Konstrukt. Vielleicht mit sowas wie r.hi und r.lo-Proxy-Objekten. Meiner Erfahrung nach generieren Compiler bei sowas sehr schönen schlanken Code, wenn man es sauber implementiert. Auch wenn es auf den ersten Blick etwas umständlich aussieht.

    Den ganzen Code mit nackten Shifts und Bitweisen Operationen zuzupflastern wäre ohnehin schlechter Stil und ganz gewiss fehleranfällig.


  • Mod

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

    @TGGC Du darfst das gerne bevorzugen. Bloss bitte empfehle es niemand anderem! Es gibt schon genug Leute die von sich aus kaputtes C++ schreiben, da muss man nicht noch nachhelfen.

    Doch, man sollte das hier schon empfehlen. GCC garantiert, dass das immer funktionieren wird. Nachdem man diese Garantie in der Dokumentation gründlich gelesen hat, spricht auch nichts dagegen, diese zu benutzen. Dazu sind Compilererweiterungen schließlich da. Hier hätte man nämlich möglicherweise echte Laufzeitvorteile dadurch. Ich bin mir nach der Beschreibung des OP nicht sicher, auf welcher Art von Prozessor mit welchem Befehlssatz der Code ausgeführt werden soll, aber schlimmstenfalls wird der Zugriff zu einem Bitshift werden, und bestenfalls zu etwas, wo man gar nicht rechnen muss.

    Das Geschrei auf der ganzen Welt wäre ziemlich laut, wenn GCC ankündigen würde, diese Garantie abzuschaffen, von daher würde man sicherlich sehr lange im Voraus mitbekommen, wenn das irgendwann einmal passieren sollte (Wird es aber nie, jede Wette!). Für Kompatibilität kann man die genaue Zugriffsart ja auch noch wegabstrahieren, so wie das vorgeschlagen wurde, und auf anderen Systemen die Bitshiftvariante einsetzen.

    Ich denke schon, dass das alles wichtige Punkte sind, die man diskutieren sollte, anstatt pauschal zu sagen "Nach strikter Standardauslegung undefiniert, daher Nein!".



  • @Finnegan
    Wenn man es nicht manuell machen soll, gibts also eine automatisierte Lösung? Diese ist im Compiler eingebaut und die nutze ich, die ist tausendfach getestet. Wer meint, kann das gern neu erfinden und damit seine Zeit verschwenden mit Implementation und Bugsuche. Ich empfehle es niemandem.

    Der Standard erlaubt das sich hier Verhalten beliebig ändert aber niemand will das wirklich, weder User hoch Compilerbauer. Daher hoffe ich wie gesagt, das man eine Formulierung im Standard findet, die zumindest begrenzt definiertes Verhalten beschreibt ohne das Performance darunter leiden muss.



  • Wie gesagt habe ich zu Beginn des Projektes auch mit Bitshift und Binär-Operationen angefangen. Aber ich fand das im weiteren Verlauf mit den Unions praktischer.

        value--;
        regAF.Flag_H = ((value & 0x0F) == 0x0F);    // H is set if borrow from bit 4, reset otherwise
        regAF.Flag_PV = (value == 0x7F);            // P/V is set if m was 80H before operation; reset otherwise
        regAF.Flag_S = (value & 0x80);              // S is set if result is negative; reset otherwise
        regAF.Flag_Z = (value == 0x00);             // Z is set if result is zero; reset otherwise
        regAF.Flag_N = true;
    

    ist doch etwas einfacher zu lesen als,

        value--;
        ((value & 0x0F) == 0x0F) ? (regF |= Flag_H) : (regF &= ~Flag_H);
        (value == 0x7F) ? (regF |= Flag_PV) : (regF &= ~Flag_PV);
        (value & 0x80) ? (regF |= Flag_S) : (regF &= ~Flag_S);
        (value) ? (regF &= ~Flag_Z) : (regF |= Flag_Z);
        regF |= Flag_N;
    

    was mich jedoch interessieren würde ist, welche Version der Compiler besser optimieren kann, also
    welchen Code der Prozessor dann besser ausführen kann.



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

    ist doch etwas einfacher zu lesen als,

    Hm, es zwingt doch ja auch niemand, mit Bit-Operationen herumzufummeln.

    Genausogut könntest du eine Klasse für die Flags erstellen, z.B. so:

    #include <stdint.h>
    
    union UFlags {
       uint8_t F;
       struct {
            bool Flag_C : 1;    // 'Carry' Flag
            bool Flag_N : 1;    // 'Negative' Flag
            bool Flag_PV : 1;   // 'Parity/Overflow' Flag
            bool  : 1;
            bool Flag_H : 1;    // 'Half-Carry' Flag
            bool  : 1;
            bool Flag_Z : 1;    // 'Zero' Flag
            bool Flag_S : 1;    // 'Sign' Flag
        };
    };
    
    class Flags {
    public:
        uint8_t F;
    public:
        bool carry() { return F & 1; }
        void set_carry(bool val) { F &= ~1; F |= val; }
        bool negative() { return F & 2; }
        void set_negative(bool val) { F &= ~2; F |= val << 1; }
        // usw
    };
    
    int main(int argc, char **) {
        Flags f;
        f.F = argc;
        f.set_negative(f.carry() ? 0 : 1);
    
        UFlags uf;
        uf.F = argc;
        uf.Flag_N = uf.Flag_C ? 0 : 1;
    
        // returne wahlweise f.F oder uf.F!
        return f.F;
    }
    

    was mich jedoch interessieren würde ist, welche Version der Compiler besser optimieren kann, also
    welchen Code der Prozessor dann besser ausführen kann.

    Wenn du dir obigen Code im Compiler Explorer mal anschaust, bleibt nicht viel übrig. Wenn du diese Klasse Flags durch deine Union ersetzt (returne uf.F), kommt (bis auf Vertauschung von rax/rdi) sogar derselbe Assembercode raus. (mit x8664 clang-7.0.0 -Wall -O3 getestet)



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

    GCC garantiert, dass das immer funktionieren wird.

    Dann sollte man das wohl eher im Compiler/IDE-Forum diskutieren und nicht in Standart-C++?



  • @SeppJ Das ist halt so ein scheiss Thema wo's IMO nicht wirklich ne gute Lösung dafür gibt. Also schon wenn Performance Wurst ist, da heisst die Lösung einfach memcpy. Easy.

    Ansonsten... doof.

    Was die Garantie von GCC angeht: ja, die wird wohl in den meisten Fällen reichen. Natürlich kann man sich trotz dieser Garantie in den Fuss schiessen, nämlich indem man Zeiger auf die Variablen in der Union in eine Funktion gibt die vom Union nix weiss. (Wobei ich davon ausgehe dass die Garantie von GCC dermassen eingeschränkt formuliert ist dass solche und ähnliche Konstrukte verboten sind.)

    Was aber IMO auch wichtig wäre bei dem Thema: gibt es ähnliche Garantien auch bei Clang, MSVC, Intel, XL C/C++ und was ich sonst noch an (mehr oder weniger) wichtigen Compilern vergessen habe?


    C und C++ bräuchten einfach mehr Library Funktionen für sowas. Also für so einfache Sachen wie auch für diverse Bit-Fummeleien die von den üblichen modernen CPUs unterstützt werden. Ein paar davon werden mehr oder weniger zuverlässig zu einem Befehl optimiert, aber der Grossteil bleibt unnötig komplizierter Code.

    Dies hier festzustellen (=rumzusudern) hilft natürlich niemandem, aber ich konnte es mir nicht verkneifen 🙂


  • Mod

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

    Was die Garantie von GCC angeht: ja, die wird wohl in den meisten Fällen reichen. Natürlich kann man sich trotz dieser Garantie in den Fuss schiessen, nämlich indem man Zeiger auf die Variablen in der Union in eine Funktion gibt die vom Union nix weiss. (Wobei ich davon ausgehe dass die Garantie von GCC dermassen eingeschränkt formuliert ist dass solche und ähnliche Konstrukte verboten sind.)

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



  • Ich verstehe aber immer noch nicht warum man unbedingt eine (unportable) union dafür brauchen würde. Immerhin geht es "nur" um 16-bit register mit einer Handvoll (Tschernobyl-Generation) Flags:

    #include <climits>
    #include <cassert>
    #include <cstdint>
    
    #if CHAR_BIT != 8
    #   error Kiss my a$$!
    #endif
    
    using word = std::uint16_t;
    using byte = char unsigned;  // kein std::uint8_t ... aliasing ...
    
    byte set_bit(byte &dst, unsigned bit, bool value = true)
    {
    	assert(bit < CHAR_BIT);
    	return dst &= ~(1 << bit) | (value << bit);
    }
    
    bool get_bit(byte src, unsigned bit)
    {
    	assert(bit < CHAR_BIT);
    	return (src >> bit) & 1;
    }
    
    class reg16_t
    {
    	word value;
    
    public:
    	byte &high  = reinterpret_cast<byte*>(&value)[1];  // wird wohl immer
    	byte &low   = reinterpret_cast<byte*>(&value)[0];  // gratis sein.
    
    	reg16_t(word value = 0) : value{ value } {}
    	reg16_t& operator=(word value) { this->value = value; return *this; }
    	operator word&() { return value; }
    
    	bool get_carry() const                  { return get_bit(low, 0); }
    	void set_carry(bool value = true)       { set_bit(low, 0, value); }
    
    	bool get_negative() const               { return get_bit(low, 1); }
    	void set_negative(bool value = true)    { set_bit(low, 1, value); }
    
    	bool get_parity() const                 { return get_bit(low, 2); }
    	void set_parity(bool value = true)      { set_bit(low, 2, value); }
    
    	bool get_half_carry() const             { return get_bit(low, 4); }
    	void set_half_carry(bool value = true)  { set_bit(low, 4, value); }
    
    	bool get_zero() const                   { return get_bit(low, 6); }
    	void set_zero(bool value = true)        { set_bit(low, 6, value); }
    
    	bool get_sign() const                   { return get_bit(low, 7); }
    	void set_sign(bool value = true)        { set_bit(low, 7, value); }
    };
    

    Einmal schreiben, einmal testen, happy sein.
    Fire and forget.


  • Mod

    Sich über Unions beschweren, aber dann gröbste strict aliasing Verletzungen begehen. Super.



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

    aber dann gröbste strict aliasing Verletzungen begehen.

    Wo?


  • Mod

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

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

    aber dann gröbste strict aliasing Verletzungen begehen.

    Wo?

    high, low und operator word& sind gleichzeitig aktive Referenzen unter unterschiedlichem Typ auf das gleiche Objekt?

    edit: Oh, super, jetzt wirst du sagen, dass dein byte ein unsigned char ist und das deswegen nicht aliased. Als ob das Absicht gewesen wäre. Naja, noch mal Glück gehabt, aber trotzdem ein sehr schlechtes Vorbild, weil das nur unter diesen genau speziell gewählten (zufällig getroffenen?) Umständen ausnahmsweise erlaubt ist.



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

    edit: Oh, super, jetzt wirst du sagen, dass dein byte ein unsigned char ist und das deswegen nicht aliased. Als ob das Absicht gewesen wäre...

    Naja nachdem da steht

    using byte = char unsigned;  // kein std::uint8_t ... aliasing ...
    

    ...war es vermutlich Absicht 🙂



  • @hustbaer Wenigstens einer der mich versteht *schnief* *Taschentuchholengeh*

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

    Naja, noch mal Glück gehabt

    Glück!?

    (zufällig getroffenen?)

    wtf? Denkst Du ich würfle!?

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

    Als ob das Absicht gewesen wäre.

    *rolleyes* (der smiley wäre nicht aussagekräftig genug gewesen)

    Und ich habe auch eingangs schon gesagt

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

    Immerhin geht es "nur" um 16-bit register

    char unsigned 8 bit, 16 = 2 x 8. Passt wie Faust auf Auge.

    Was soll daran schlechtes Vorbild sein @SeppJ? Das ist doch genau das worum es geht, zu wissen was geht und was nicht geht.



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

    Oh, super, jetzt wirst du sagen, dass dein byte ein unsigned char ist und das deswegen nicht aliased. Als ob das Absicht gewesen wäre. Naja, noch mal Glück gehabt, aber trotzdem ein sehr schlechtes Vorbild, weil das nur unter diesen genau speziell gewählten (zufällig getroffenen?) Umständen ausnahmsweise erlaubt ist.

    Ne, ernsthaft. Du darfst ruhig von Deinem hohen Ross ab und zu runterkommen und brauchst nicht jedem anderem per se unterstellen er wisse nicht was er tue. Es nervt und ist verletzend. Und das obwohl ich das sogar extra noch als Kommentar hingeschrieben habe, Du Horst! (sorry).

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

    dass dein byte ein unsigned char ist und das deswegen nicht aliased.

    Doch, er aliased. Aber er darf auch. Krümelkacken kann ich auch.

    </rant>



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

    @Finnegan
    Wenn man es nicht manuell machen soll, gibts also eine automatisierte Lösung? Diese ist im Compiler eingebaut und die nutze ich, die ist tausendfach getestet. Wer meint, kann das gern neu erfinden und damit seine Zeit verschwenden mit Implementation und Bugsuche. Ich empfehle es niemandem.

    Mein Argument war nicht für oder gegen eine spezielle Lösung, sondern dass dein Einwand mit den "tausenden Bitshifts" nicht zutreffen muss (was ich unter "manuell" verstehe), da man sowas in C++ sehr schön in einer Klasse zentralisieren kann. Das würde ich übrigens auch für die Union-Lösung bevorzugen, sonst hat man statt der "tausenden Bitshifts" nachher tausende, nicht-standardkonforme Union-Zugriffe. Wenn die irgendwann mal nicht mehr so funktionieren sollten, wie erwartet - oder aber auch wenn es mal eine bessere und standardkonforme Lösung dafür geben sollte, lässt sich das so wesentlich besser warten. Ich halte solche Abstraktionen für etwas völlig natürliches und alles andere als "Zeitverschwendung", wenn sie gut durchdacht sind. Die sind für mich ein wichtiges Werkzeug, dem ständigen Sog wachsender Kompexität in Softwareprojekten etwas entgegenzusetzen.

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

    class reg16_t
    {
    	word value;
    
    public:
    	byte &high  = reinterpret_cast<byte*>(&value)[1];  // wird wohl immer
    	byte &low   = reinterpret_cast<byte*>(&value)[0];  // gratis sein.
    

    So ganz "gratis" ist das nicht, da es zwei zusätzliche Daten-Member sind, welche eigentlich nur triviale Infomation enthalten. Ich fände es besser, diese bei Bedarf "on-the-fly" zu berechnen, was dann wohl letztendlich im generierten Code ausschliesslich in Registern bzw. mithilfe von Address-Offsets umgesetzt wird. Das hat den Vorteil, dass ein reg16_t Im Speicher exakt wie ein word aussieht und sogar TriviallyCopyable wäre, so dass beide Typen via memcpy ineinander kopiert werden könnten (die Referenz-Member erfordern eigene Copy/Move-/Assignment-Operatoren, welche die TriviallyCopyable-Eigenschaft verhindern). Auch sind die Referenz-Member nicht unbedingt cache-freundlich, wenn man es mal mit einem größeren reg16_t-Array zu tun hat. Eine "leichtgewichtige Abstraktion" ist das also in dieser Form meines Erachtens nicht 😉

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

    Sich über Unions beschweren, aber dann gröbste strict aliasing Verletzungen begehen. Super.
    ff.

    Vielleicht ist es hilfreicher, den Diskurs etwas ruhiger zu halten. Man macht es sich unnötig schwerer, Leute zu überzeugen, wenn man sie dabei vor den Kopf stößt. Ich bin eigentlich auch jemand, der erstmal die standardkonforme Variante bevorzugt, würde mich aber von Argumenten wie Performance und weitreichender Compiler-Unterstützung durchaus überzeugen lassen. Das Ihr-habt-keine-Ahnung-Signal, dass hier jedoch zwischen den Zeilen transportiert wird, erzeugt allerdings auch bei mir eine gewisse Trotzreaktion. Mir wäre es lieber wir würden hier gemeinsam versuchen, die beste Lösung zu finden, anstatt alle nur möglichst gut aussehen zu wollen.



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

    So ganz "gratis" ist das nicht, da es zwei zusätzliche Daten-Member sind, welche eigentlich nur triviale Infomation enthalten.

    Ich meinte damit, daß Du die im Kompilat nicht finden wirst.

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

    TriviallyCopyable

    Ist ein Argument, wenn mans braucht? Aber das wäre ja auch einfach zu ändern. Referenzen rauswerfen und stattdessen 4 Funktionen schreiben.



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

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

    So ganz "gratis" ist das nicht, da es zwei zusätzliche Daten-Member sind, welche eigentlich nur triviale Infomation enthalten.

    Ich meinte damit, daß Du die im Kompilat nicht finden wirst.

    Ich denke schon. Im Maschinencode sehen die Referenzen wahrscheinlich wie zwei Pointer-Member aus, die auch in den Speicher geschreiben werden, wenn das reg16_t-Objekt nicht so kurzlebig ist, dass sich das alles in ein paar reine Register-Operationen optimieren lässt.

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

    TriviallyCopyable

    Ist ein Argument, wenn mans braucht? Aber das wäre ja auch einfach zu ändern. Referenzen rauswerfen und stattdessen 4 Funktionen schreiben.

    Das ist vielleicht nicht unbedingt notwendig, aber dennoch gerade für das Anwendungsgebiet in einem Emulator eine sehr praktische Eigenschaft. Später den kompletten Zustand eines simulierten Computers einfach so via memcpy kopieren oder auch im Block in eine Datei schreiben zu können, empfinde ich als lohnenswertes Designziel - zumal es hier kein übermäßiger Mehraufwand ist.


Anmelden zum Antworten