uni8_t Vector in eine einzige unit32_t variable schreiben



  • Wie kann ich die ersten vier Elemente eines uni8_t Vector in eine einzige unit32_t variable schreiben ?

    mein vector hat bspw folgende Elemente: {0x01, 0x02, 0x03, 0x4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

    Die ersten vier argumente möchte ich nun meine uint32_t variable schreiben.
    das ergebnis soll dann quasi sein, dass die Variable den wert 0x01020304 enthält

    Vielen Dank schon einmal für die Hilfe


  • Mod

    @agrw sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    das ergebnis soll dann quasi sein, dass die Variable den wert 0x01020304 enthält

    Ist es wichtig dass es genau dieser Wert ist, oder denkst du nur, dass dies das selbstverständliche Ergebnis wäre? Es gibt nämlich zwei Möglichkeiten:

    Du kannst entweder direkt das Bitmuster übertragen (sehr einfach und effizient mittels reinterpret_cast). Das enthält dann alle Informationen aus dem Vector und ist auch reversibel, aber es wird halt in den seltensten Fällen tatsächlich das Bitmuster von 0x01020304 sein (ein wahrscheinlicher Wert wäre zum Beispiel 0x03040102, ist aber systemabhängig).

    Oder es geht dir tatsächlich um den genannten Wert. In dem Fall musst du rechnen. Das ist zwar auch nicht schwer, aber braucht halt ein bisschen mehr Programmierung und Gehirnschmalz.. Du hast hier vier Ziffern in einem Ziffernsystem, aber nicht zur Basis 10, sondern zur Basis 256. Sonst ist alles so wie in der Grundschule, wenn der Mathelehrer fragt, wie viel das ist, wenn man 4 Hunderter, 7 Zehner, und 3 Einer hat (also 4*10^2 + 7*10^1 + 3*10^0 = 473). Wenn man sich etwas mit Zahlensystemen mit Zweierpotenzen als Basis auskennt, kann man sogar etwas tricksen und die Potenzen und Multiplikationen durch viel effizientere Bitverschiebungen umschreiben (bekommt wahrscheinlich der Compileroptimierer auch selber hin)



  • Einfach rechnen:

    #include <vector>
    #include <stdint.h>
    
    uint32_t to_uint32(const std::vector<uint8_t> &v) {
        return (static_cast<uint32_t>(v[0]) << 24)
         + (static_cast<uint32_t>(v[1]) << 16)
         + (static_cast<uint32_t>(v[2]) << 8)
         + (static_cast<uint32_t>(v[3]));
    }
    
    int main() 
    {
        std::vector<uint8_t> v = {0x01, 0x02, 0x03, 0x4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
        auto r = to_uint32(v);
        return r;
    }
    

    Ich finde es mit << 8 etc. einfacher zu lesen als mit * 256 etc. Aus mal einen Shift zu machen, das bekommt selbst der dümmste Compiler hin, behaupte ich einfach mal. gcc 12 optimiert bei obigem Code zwar den vector-new/delete call nicht weg, returnt danach aber einfach 16909060, d.h. bekommt die Berechnung sogar schon zur Compilezeit hin: https://gcc.godbolt.org/z/zvref6WWo

    Wichtiger finde ich den cast auf den unsigned-Ergebnistyp vor dem shiften, weil ansonsten der uint8 in einen (signed) int promoted wird, was wir ja nicht haben wollen. Besonders dann nicht, wenn wir die Funktion mal auf uint64_t umstellen wollen.



  • Ich würde das ganze ohne casts schreiben, da es so klarer formuliert ist.

    #include <vector>
    #include <cstdint>
    #include <iostream>
    #include <cstdlib>
    
    uint32_t to_uint32(const std::vector<uint8_t> &v) {
        uint32_t t1 = v[0];
        uint32_t t2 = v[1];
        uint32_t t3 = v[2];
        uint32_t t4 = v[3];
    
        return ((t1 << 24) | (t2 << 16) | (t3 << 8) | t4);
    }
    
    int main() {
        std::vector<uint8_t> v = {0x01, 0x02, 0x03, 0x4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
        auto r = to_uint32(v);
    
        std::cout << std::hex << r << std::endl;
    
        return EXIT_SUCCESS;
    }
    

    @SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Du kannst entweder direkt das Bitmuster übertragen (sehr einfach und effizient mittels reinterpret_cast).

    Das ist nicht garantiert, da ein Zeiger auf ein Feld mit uint8_t nicht so ausgerichtet sein muss, dass man das in ein Zeiger auf uint32_t casten kann.



  • @john-0 sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Ich würde das ganze ohne casts schreiben, da es so klarer formuliert ist.

    Ok, das geht natürlich auch. Dann sind die casts ja implizit auch in Zeile 7 bis 10 drin. Solange man im Hinterkopf hat, dass irgendwelche Promotionen zu int vorkommen, wenn man mit was kleinerem hantiert, ist alles in Ordnung 🙂



  • @wob sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Ok, das geht natürlich auch. Dann sind die casts ja implizit auch in Zeile 7 bis 10 drin. Solange man im Hinterkopf hat, dass irgendwelche Promotionen zu int vorkommen, wenn man mit was kleinerem hantiert, ist alles in Ordnung 🙂

    Ich halte es immer für besser einen Typen zu promoten, als in zu casten. Casten sollte man wirklich nur dann, wenn es keine Alternative gibt oder wenn man potentiell Daten verliert, d.h. von einem größeren Typen auf einem kleinerem Typen konvertiert. Cast impliziert, dass das etwas schief gehen könnte, und das ist bei einer Promotion von uint8_t auf uint32_t ausgeschlossen.


  • Mod

    @john-0 sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Das ist nicht garantiert, da ein Zeiger auf ein Feld mit uint8_t nicht so ausgerichtet sein muss, dass man das in ein Zeiger auf uint32_t casten kann.

    Ich bin zu faul zu gucken, aber ich bin ziemlich sicher, dass der Anfang eines Arrays so ausgerichtet sein muss, dass er für alle Typen passend ist. Wenn man davor Angst hat (oder bei einem unpassenden Arrayindex anfangen möchte) kann man dann auch memcpy nutzen (das dann auf einem x86 das machen wird, was der reinterpret_cast gemacht hätte). Ich würde nicht zu viel Rücksicht auf solche Überlegungen geben. Wir sind immer noch kluge Programmierer und wissen (hoffentlich), was wir da tun, wenn wir reinterpret_cast schreiben, und dass der Code hinterher auf einer realen Maschine laufen wird, die halbwegs bekannte Eigenschaften hat. Wenn's wirklich nur um das Übertragen eines Vectors an eine andere Speicherstelle geht, hätte ich echt kein Problem mit reinterpret_cast abzukürzen. Wenn der Wert in irgendwelchen speziellen SIMD-Routinen verarbeitet werden soll, würde ich es mir anders überlegen (aber dann hätten wir ja auch sicherlich keinen vector<uint8> sondern einen Vector eines für SIMD gemachten Datentyps).



  • @SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Ich bin zu faul zu gucken, aber ich bin ziemlich sicher, dass der Anfang eines Arrays so ausgerichtet sein muss, dass er für alle Typen passend ist. Wenn man davor Angst hat (oder bei einem unpassenden Arrayindex anfangen möchte) kann man dann auch memcpy nutzen (das dann auf einem x86 das machen wird, was der reinterpret_cast gemacht hätte)

    Man kann sich den ganzen Aufwand sparen, wenn man das sauber über die Typpromotion macht. So ein Code funktioniert auf jeder Plattform. Wenn man hingegen anfängt Zeiger zu casten und die sind nicht durch vier teilbar liefern etliche CPUs einen Bus Error zurück.


  • Mod

    @SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    @john-0 sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Das ist nicht garantiert, da ein Zeiger auf ein Feld mit uint8_t nicht so ausgerichtet sein muss, dass man das in ein Zeiger auf uint32_t casten kann.

    Ich bin zu faul zu gucken, aber ich bin ziemlich sicher, dass der Anfang eines Arrays so ausgerichtet sein muss, dass er für alle Typen passend ist.

    Der interne Speicher von vector<T> wird nicht als Array alloziert, sondern mittels operator new, aber Recht hast Du natuerlich trotzdem, das alignment muss mindestens das fundamental alignment sein, was jeden skalaren Typ mit einschliesst.

    @john-0 sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Wenn man hingegen anfängt Zeiger zu casten und die sind nicht durch vier teilbar liefern etliche CPUs einen Bus Error zurück.

    Hat SeppJ nicht gerade aufgezeigt, dass das casten dieser Zeiger garantiert funktionieren muss?

    @SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Das enthält dann alle Informationen aus dem Vector und ist auch reversibel, aber es wird halt in den seltensten Fällen tatsächlich das Bitmuster von 0x01020304 sein (ein wahrscheinlicher Wert wäre zum Beispiel 0x03040102, ist aber systemabhängig).

    Spielst Du auf Endianness an? Dann wuerde der Wert doch anders aussehen?


  • Mod

    @Columbo sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Das enthält dann alle Informationen aus dem Vector und ist auch reversibel, aber es wird halt in den seltensten Fällen tatsächlich das Bitmuster von 0x01020304 sein (ein wahrscheinlicher Wert wäre zum Beispiel 0x03040102, ist aber systemabhängig).

    Spielst Du auf Endianness an? Dann wuerde der Wert doch anders aussehen?

    Ja. Aber da es nur drauf ankommt, dass der Wert wahrscheinlich nicht 0x01020304 sein wird, habe ich nicht zu hart darüber nachgedacht, was eigentlich rauskommen wird. Nimm es als Beispiel, was auf einer 16-Bit Maschine mit mixed-Endianess herauskommen würde 🙂



  • Also ich kann keinen Mehrwert darin erkennen es mittels reinterpret_cast zu lösen. Mag sein dass das Alignment bei vector garantiert ist oder auch nicht, darüber will ich mir schonmal gar keine Gedanken machen. Und ich will meinen Code auch nicht umschreiben müssen wenn es mal nicht bei 0 sondern bei n losgehen soll. Oder auf ner anderen Endianness laufen soll.

    Die von @wob gezeigte Funktion funktioniert (hah, ein Wortspiel), und ist genau das was ich schreiben würde. Inklusive der Casts, denn ohne die Casts hat man das Problem dass man evtl. in das Vorzeichenbit rein shiftet - und das hat soweit ich weiss kein vom Standard garantiertes Verhalten (UB oder IB - müsste nachsehen).

    Endianness ist dann auch kein Problem mehr. Und wenn man für ein Big-Endian System kompiliert welches unaligned Loads kann, mit z.B. GCC und -O2, dann wird das auch schön zu einem einzigen 32 Bit Load optimiert.


  • Mod

    @hustbaer sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Also ich kann keinen Mehrwert darin erkennen es mittels reinterpret_cast zu lösen.

    Performance? Das eine sind 8+ Rechenoperationen im echten Maschinencode, der Cast ist gar nichts. Deshalb war meine (bislang leider unbeantwortete) Frage an den OP ja auch, ob er wirklich an den Werten interessiert ist oder einfach nur Daten durch die Gegend schaufeln möchte. Für letzteres möchte ich doch sehr hoffen, dass da niemand jemals anfängt herum zu rechnen.



  • Naja, wenn ich mein Beispiel oben nehme, kommt raus (ich hatte es ja verlinkt):

    to_uint32(std::vector<unsigned char, std::allocator<unsigned char> >&):
            mov     rax, QWORD PTR [rdi]
            mov     eax, DWORD PTR [rax]
            bswap   eax
            ret
    

    Mit return *(reinterpret_cast<uint32_t*>(&v[0])); bekomme ich:

    to_uint32_b(std::vector<unsigned char, std::allocator<unsigned char> >&):
            mov     rax, QWORD PTR [rdi]
            mov     eax, DWORD PTR [rax]
            ret
    

    D.h. der einzige Unterschied ist, dass das bswap fehlt. Also eine Korrektur für die Endianness. Es ist also ein anderes Ergebnis.

    Das Performance-Argument zieht doch überhaupt nicht, wenn der Optimizer klug ist. Dafür ist der eine Code auf allen Plattformen gleich, während der reinterpret_case-Code von der Plattform abhängig ist.

    Die Frage ist also nicht "Performance", sondern: "wie will ich die Daten im vector interpretieren".



  • @SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    @hustbaer sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Also ich kann keinen Mehrwert darin erkennen es mittels reinterpret_cast zu lösen.

    Performance? Das eine sind 8+ Rechenoperationen im echten Maschinencode, der Cast ist gar nichts.

    Wenn unter Compiler-Optimierungen dann aber zuverlässig das hier herauskommt:

    @hustbaer sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Und wenn man für ein Big-Endian System kompiliert welches unaligned Loads kann, mit z.B. GCC und -O2, dann wird das auch schön zu einem einzigen 32 Bit Load optimiert.

    Dann gibt es allerdings wirklich keinen Grund für den reinterpret_cast. Im Gegenteil, ich finde es sogar zu bevorzugen, den Compiler entscheiden zu lassen, ob für die aktuelle Ziel-Architektur und Compiler-Flags wie -mbig-endian/-mlittle-endian ein zu reinterpret_cast äquivalenter Code erzeugt werden kann oder nicht. Das ist auf jeden Fall portabler.

    Ich würde also erstmal die Shifts und Additionen als Default implementieren und nur für den Fall, dass der Optimizer bei bestimmten Ziel-Konfigurationen versagt, eine alternative Implemetierung mit reinterpret_cast aktivieren... und natürlich vorher sorgfältig sicherstellen, dass es der Compiler am Ende nicht aus irgeneinem Grund nicht doch besser weiss (UB oder obskure Architektur-Besoderheiten).

    Aber wem sag ich das, ich denke das würdest du in ersthaftem Code ähnlich machen 😉



  • @wob sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    to_uint32_b(std::vector<unsigned char, std::allocator<unsigned char> >&):
            mov     rax, QWORD PTR [rdi]
            mov     eax, DWORD PTR [rax]
            ret
    

    Interessant sind hier die zwei Loads. Mit dem ersten wird die im std::vector gespeicherte Adresse auf den eigentlichen Speicherbereich geladen. Das führt einem mal wieder vor Augen, dass std::vector<char>& eher mit einem char** vergleichbar ist.

    Ich denke, das wird in tatsächlichem Code sicher alles gut ge-inlined, aber to_uint32_b für sich genommen wäre wohl kompakter, wenn ein char* entgegengenommen und vector.data() übergeben würde.

    Edit: Okay, keine sonderlich überraschende Erkenntnis, jetzt wo ichs mir nochmal ansehe. Die zwei Loads haben einfach meine Aufmerksamkeit erregt - ist aber nichts unerwartetes 😉


  • Mod

    @wob sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    D.h. der einzige Unterschied ist, dass das bswap fehlt. Also eine Korrektur für die Endianness. Es ist also ein anderes Ergebnis.

    Und dafür halt 50% mehr Instruktionen. Ich sehe echt nicht, wieso sich jeder so gegen reinterpret_cast wehrt. Genau für solche Fälle ist der da! Bloß weil jedem mal Horrorgeschichten über irgendwelche Hacks in C erzählt wurden, muss man doch keine falsche Angst davor haben einen POD-Typen als einen anderen POD-Typen zu interpretieren. Da braucht man auch nix zu erzählen vonwegen "es ist technisch gesehen undefiniert". Auch da gilt: Gerade deswegen programmieren wir doch in einer maschinennahen Sprache, eben damit wir unser höheres Wissen anwenden können, dass dies hier eine völlig harmlose Operation ist, mit der wir unnöitge Rechnungen einsparen können.



  • @SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    @wob sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    D.h. der einzige Unterschied ist, dass das bswap fehlt. Also eine Korrektur für die Endianness. Es ist also ein anderes Ergebnis.

    Und dafür halt 50% mehr Instruktionen. Ich sehe echt nicht, wieso sich jeder so gegen reinterpret_cast wehrt.

    Da ist keine Instruktion überflüssig. Das bswap wird benötigt, da die vomOP gewünschte Speicher-Reihenfolge Big-Endian ist (höchstwertigstes Byte an niedrigster Speicheradresse), der generierte Code aber für ein Little-Endian-System (x64) ist.

    reinterpret_cast führt hier zu einem inkorrekten Ergebnis. Man könnte auch sagen "mit Shift&Add wär das nicht passiert" 😛

    Und ich hab nix gegen reinterpret_cast, überlasse es aber lieber dem Compiler, wenn der es mindestens genau so gut kann.


  • Mod

    @Finnegan sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Da ist keine Instruktion überflüssig. Das bswap wird benötigt, da die vomOP gewünschte Speicher-Reihenfolge Big-Endian ist (höchstwertigstes Byte an niedrigster Speicheradresse), der generierte Code aber für ein Little-Endian-System (x64) ist.
    reinterpret_cast führt hier zu eine inkorrekten Ergebnis. Man könnte auch sagen "mit Shift&Add wär das nicht passiert"

    Das hängt halt von der nach wie vor unbeantworteten Frage ab, was der Threaderstelelr üerhaupt möchte.



  • @SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Das hängt halt von der nach wie vor unbeantworteten Frage ab, was der Threaderstelelr üerhaupt möchte.

    Er hat geschrieben, dass die uint32_t den Wert 0x01020304 enthalten soll, das ist klar formuliert. Ob das aber das ist, was er wirklich möchte, ist eine andere Frage. Ich würde auch vermuten er arbeitet eher mit x86 und hätte es wohl gern andersrum 😉


  • Mod

    @Finnegan sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:

    Ob das aber das ist, was er wirklich möchte, ist eine andere Frage.

    Deswegen habe ich sie ja auch gestellt 😉


Log in to reply