uni8_t Vector in eine einzige unit32_t variable schreiben
-
@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 zureinterpret_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, dassstd::vector<char>&
eher mit einemchar**
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 einchar*
entgegengenommen undvector.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
-
@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.
-
@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 Wert0x01020304
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
-
@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
-
@SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Der eigentliche Punkt in der weiteren Diskussion war allerdings auch, dass Shift&Add exakt den selben Code erzeugt wie
reinterpret_cast
als Argument, warum man das für die Performance sehr wahrscheinlich nicht benötigt. Stell das Shift&Add auf Little Endian um und jede Wette, das dann dasbswap
wegfällt, womit der generierte Code dann identisch ist.Unter dem Aspekt hat man dann zwar mehr C++ Code (auch ein Argument), aber bei der Performance nichts verloren und gleichzeitig bessere Portabilität gewonnen.
-
@Finnegan sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Unter dem Aspekt hat man dann zwar mehr C++ Code (auch ein Argument), aber bei der Performance nichts verloren und gleichzeitig bessere Portabilität gewonnen.
Ähh, nein. Denn dann würde ja auf einem anderen System wieder der ineffiziente Code erzeugt. Wohingegen der reinterpret_cast garantiert den effizientesten Code für dummes Datenverschieben erzeugt (nämlich gar keinen).
-
@SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
@Finnegan sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Unter dem Aspekt hat man dann zwar mehr C++ Code (auch ein Argument), aber bei der Performance nichts verloren und gleichzeitig bessere Portabilität gewonnen.
Ähh, nein. Denn dann würde ja auf einem anderen System wieder der ineffiziente Code erzeugt. Wohingegen der reinterpret_cast garantiert den effizientesten Code für dummes Datenverschieben erzeugt (nämlich gar keinen).
Ja, ich denke ich verstehe deine Perspektive gerade. Du möchtest die Daten in der Reihenfolge in den Speicher schreiben, den die Architektur vorgibt und gehst davon aus, dass sie vom selben System weiterverarbeitet werden. In dem Fall würde ich natürlich auch pauschal mit
reinterpret_cast
odermemcpy
arbeiten. Kein Problem damitMeine Perspektive war allerdings eine systemunabhängige Vorgabe, wie die Speicherreihenfolge auszusehen hat (eben wegen dem "so soll das aussehen" im OP). Z.B. weil man ein Netzwerkpaket bauen will, dass von einem System mit einer anderen Endianess weiterverarbeitet werden könnte, oder weil man den Speicherbereich in eine Datei schreiben will, die zwischen Systemen geteilt werden kann. In dem Fall macht das Shift&Add sehr wohl Sinn, da man auf dem anderen System dann eben nicht auf den ineffizienten Code verzichten kann, wenn es korrekt sein soll.
Ich denke wir haben hier beide recht, nur eben andere Vorstellungen davon, was die tatsächlichen Anforderungen sind
-
@SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Und dafür halt 50% mehr Instruktionen. Ich sehe echt nicht, wieso sich jeder so gegen reinterpret_cast wehrt.
Weil wir hier über ISO C++ diskutieren und nicht über eine spezielle Variante die nur auf einem speziellen System läuft. Der
reinterpret_cast
funktioniert nur als Optimierung in ganz bestimmten Fällen, und zwar nur dann wenn man eine Maschine mit der richtigen Endianess hat, und auf dieser in ihrem nativen Format gearbeitet wird. Für Internetprotokolle ist als Norm aber BigEndian vor Ewigkeiten definiert worden. D.h. wenn man anfängt etwas für die Datenübertragung für eines der offiziellen Internetprotokolle zu konvertieren, kann man bereits keinenreinterpret_cast
auf einem LittleEndian System mehr nutzen, da es die falsche Byteorder hat.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.
Du gehst von falschen Annahmen aus. Das typische Problem von jemanden, der immer nur x86 Hardware genutzt hat. Der OP will hier gerade eine Konversion in BigEndian Format haben!
Etwas älteren C++ Stil, weil ich nicht extra einen neuen Compiler für den PowerMac übersetzen wollte. (Das braucht zuviel Zeit.)
#include <vector> #include <stdint.h> #include <cstdlib> #include <iostream> uint32_t to_uint32_cast(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])); } uint32_t to_uint32_promo(uint8_t* v) { uint32_t t1 = v[0], t2 = v[1], t3 = v[2], t4 = v[3]; return t1 << 24 | t2 << 16 | t3 << 8 | t4; } int main() { uint8_t v[] = {0x01, 0x02, 0x03, 0x4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint32_t r1 = to_uint32_cast(v); uint32_t r2 = to_uint32_promo(v); uint32_t *r3 = reinterpret_cast<uint32_t*>(&v[0]); std::cout << "cast " << std::hex << r1 << "\n"; std::cout << "promo " << std::hex << r2 << "\n"; std::cout << "rcast " << std::hex << *r3 << "\n"; return EXIT_SUCCESS; }
Das ergibt auf dem PowerMac
cast 1020304 promo 1020304 rcast 1020304
auf dem Intel System
cast 1020304 promo 1020304 rcast 4030201
Ich hoffe Du siehst das Problem.
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.
Code muss korrekt sein, erst danach kommt die Performance. Dein höheres Wissen sind Annahmen, die nicht garantiert sind!
-
Du löst irgendwelche völlig anderen Probleme, von denen außer dir gar niemand redet
-
@SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Du löst irgendwelche völlig anderen Probleme, von denen außer dir gar niemand redet
Falsch, Du weißt nicht um welche Plattform es sich handelt, und willst mit
reinterpret_cast
ran. Das ist maximal am Thema vorbei.
-
@john-0 sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Das ist maximal am Thema vorbei.
Ja, an deinem Fantasiethema, von dem niemand sonst redet.
-
@Columbo sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Hat SeppJ nicht gerade aufgezeigt, dass das casten dieser Zeiger garantiert funktionieren muss?
Nein, hat er nicht. Die Norm garantiert, dass man Zeiger auf Felder die man als
uint32_t
anlegt in einen Zeiger von Typuint8_t
casten kann. Die Norm garantiert aber nicht, dass man das auch umgekehrt kann. Da diese Felder nicht so ausgerichtet sein müssen, dass der Zeiger auf einer Modulo 4 Adresse liegt. Oftmals ist das der Fall, und zwar dann, wenn die Feldgröße ein Vielfaches vonsizeof(uint32_t)
ist.malloc
alloziert nach diesem Muster den Speicher, und richtet ihn so aus dass das für größere Typen passt. Das Problem hier an der Stelle ist, dass Du nicht weißt, welche Allokationsstrategie der Containerstd::vector<uint8_t>
nutzt. Er darf hier Speicher so anfordern, dass er mehr Speicher als die vieruint8_t
anfordert. Wenn er eine Größe anfordert, die nicht ein Vielfaches vonsizeof(uint32_t)
ist, kann es knallen. Ja, das ist unwahrscheinlich, aber es ist eben nicht garantiert, dass das auch funktioniert.
-
@john-0 sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Das Problem hier an der Stelle ist, dass Du nicht weißt, welche Allokationsstrategie der Container
std::vector<uint8_t>
nutzt. Er darf hier Speicher so anfordern, dass er mehr Speicher als die vieruint8_t
anfordert. Wenn er eine Größe anfordert, die nicht ein Vielfaches vonsizeof(uint32_t)
ist, kann es knallen. Ja, das ist unwahrscheinlich, aber es ist eben nicht garantiert, dass das auch funktioniert.Alle Allokationen des Default-Allocator basieren letztendlich auf
std::malloc
und haben ein Alignment von mindestensalignof(std::max_align_t)
.Für die Signatur
uint32_t to_uint32(const std::vector<uint8_t> &v)
bindetv
auch nur anstd::vector
-Typen, welchestd::allocator<uint8_t>
verwenden, daher halte ich denreinterpret_cast
bezüglich Alignment in diesem Fall für unproblematisch, solange man nicht anfängt, bei irgendwelchen krummen Vector-Indizes zu casten.Ich denke auch, die Diskussion driftet hier zu weit in Bereiche ab, die derzeit nicht das aktuelle Problem sind, auch wenn all diese Apsekte durchaus irgendwann mal relevant sein könnten. Für mich ist die Frage eher, ob die im OP genannte Byte-Reihenfolge wichtig ist und auf jedem System die selbe sein sollte (Netzwerkpakete, portable Dateiformate), oder ob einfach nur die Bytes in Maschinen-Reihenfolge in den Speicher geschreiben werden sollen. Ersteres ist ein starkes Argument für Shift&Add, bei letzterem bitte gerne auch
reinterpret_cast
. Ich vermute stark letzteres, so wie die Frage gestellt wurde.
-
@SeppJ sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
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.
Finde ich den falschen Zugang. In diesem speziellen Fall müsste es mMn. sogar - fast - vom Standard abgedeckt sein. Also fast wegen der Annahme dass
uint8_t
entwederchar
oderunsigned char
ist - was der Standard AFAIK nicht garantiert. Sobald die Aliasing-Ausnahme aber nicht mehr gilt, ... oops.
https://godbolt.org/z/1YbacbKdq
-
@john-0
vector<T>
alloziert mitstd::allocator
, welches aufoperator new
basiert, was wiederum garantiert fuer alle normalen Objekte ausgerichteten Speicher ergibt. Da das interne Array mindestenssizeof(uint32_t)
bytes haben muss, damit das type punning ueberhaupt Sinn ergibt, ist es folglich auch fuer ein Objekt vom Typuint32_t
ausgerichtet.Wenn man ganz paranoid ist kann man auch
static_assert(alignof(uint32_t) <= __STDCPP_DEFAULT_NEW_ALIGNMENT__)
schreiben.@hustbaer sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Finde ich den falschen Zugang. In diesem speziellen Fall müsste es mMn. sogar - fast - vom Standard abgedeckt sein. Also fast wegen der Annahme dass uint8_t entweder char oder unsigned char ist - was der Standard AFAIK nicht garantiert. Sobald die Aliasing-Ausnahme aber nicht mehr gilt, ... oops.
Dann kopierst Du halt mit
memcpy
? Oder schreibst einenstatic_assert
mit Deiner Bedingung, der auf 0% der existierenden Platformen feuert. Klingt jetzt nicht nach nem deal breaker.
-
@Columbo 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:
Finde ich den falschen Zugang. In diesem speziellen Fall müsste es mMn. sogar - fast - vom Standard abgedeckt sein. Also fast wegen der Annahme dass uint8_t entweder char oder unsigned char ist - was der Standard AFAIK nicht garantiert. Sobald die Aliasing-Ausnahme aber nicht mehr gilt, ... oops.
Dann kopierst Du halt mit
memcpy
?Mir ist klar dass es mit
memcpy
geht. Ist aber ziemlich egal was mitmemcpy
geht, wenn hier speziellreinterpret_cast
diskutiert wurde, oder?Und was
memcpy
vs. Shiften + Addieren angeht: da kommt's halt drauf an was man braucht. Wenn die Byte-Order fix und unabhängig von der Maschine ist, dann ist mMn. Shiften + Addieren angesagt. Wenn die Byte-Order dagegen garantiert die der Maschine ist, dann nimmt manmemcpy
. Nicht nur weil es die jeweils einfachere Variante ist, sondern weil es mMn. auch die jeweils "logischere" Variante ist.Und da der OP die Frage so formuliert hat dass man annehmen muss dass die Byte Order fix und unabhängig von der Maschine ist => Shiften + Addieren.
Oder schreibst einen
static_assert
mit Deiner Bedingung, der auf 0% der existierenden Platformen feuert. Klingt jetzt nicht nach nem deal breaker.Ich wollte nur darauf hinweisen dass das ganze nur auf Grund der speziellen, im Standard verankerten Ausnahme für
char
,unsigned char
undstd::byte
funktioniert. Weil ich glaube dass das viele Leute die solchen Code schreiben nicht wissen. Und wenn man denen dann ohne diese Ausnahme zu erwähnen sagt "reinterpret_cast
ist OK", dann ist das schlecht. Weil sie dann vermutlich auch annehmen würden dassreinterpret_cast
auch OK ist wenn's z.B. um einuint32_t
Array geht wo manuint64_t
rauslesen will.
-
@hustbaer sagte in uni8_t Vector in eine einzige unit32_t variable schreiben:
Ich wollte nur darauf hinweisen dass das ganze nur auf Grund der speziellen, im Standard verankerten Ausnahme für
char
,unsigned char
undstd::byte
funktioniert. Weil ich glaube dass das viele Leute die solchen Code schreiben nicht wissen. Und wenn man denen dann ohne diese Ausnahme zu erwähnen sagt "reinterpret_cast
ist OK", dann ist das schlecht.Ja, das habe ich hier auch verbockt. Irgendwie tendiere ich bei groben Überfliegen intuitiv öfter mal dazu,
std::uint8_t
ebenfalls zu diesem erlauchten Kreis der Strict-Aliasing-Ausnahmen zu zählen. Passiert mir nicht zum ersten Mal, und das nehme ich hiermit zurückstd::memcpy
sollte eigentlich auch ziemlich gut optimiert werden, auch wenn das im C++-Code erstmal relativ schwergewichtig aussieht - selbst für einzelne Bytes. Obwohlich mich vage erinnern kann, dass Clang das im Vergleich zu GCC oder gar MSVC in einigen Fällen besser gemacht hat. Ist aber schon ne Weile her. Auf jeden Fall den generierten Code anschauen, wenn Effizienz wichtig ist (sowieso immer).