Casten von Array um bestimmten Wert rauszulesen -> Byteorder tauschen



  • Ich habe einen unsigned char array, der rohe bytedaten enthält, die aus einer datei gelesen werden. Davon will ich in shorts lesen, also 2-byte-Stücke.

    Die ersten 2 Elemente im uc-Array sind "0" und "21". D.h. wenn ich das in ein short speichern will, soll da auch wieder 21 rauskommen. Ich mach das so:

    unsigned char *array[...] = ...;
    
    //.. irgendwo anders
    unsigned short value = *(unsigned short*)array; //sollte die ersten 2 byte als short auslesen
    

    Allerdings produziert der Code nicht die erwartete 21, sondern in value steht dann "5376". Wenn man sich die Zahl binär anschaut (10101000.00000000) sieht die der 21 auch irgendwie ähnlich (00000000.00010101), aber wie man sieht wird hier die Byte-Order vertauscht.

    Das ganze läuft auf einem i7, falls euch die Byte-Order des Prozessors hilft.

    Kann ich die Byte Order des ausgelesenen Wertes jetzt irgendwie schnell umdrehen, oder durch einen Cast den Code dazu bringen die ersten zwei Elemente in einer anderen Byte-Order auszulesen?

    Das hier passiert dabei an einer sehr performanceintensiven Stelle (das wird alles ~ 6 millionen mal gemacht), weshalb es möglichst schnell sein sollte.



  • Übrigens, was mir sehr merkwürdig aufgefallen ist:

    Wenn ich das zweite Byte manuell auslese, dann kommt auch wie erwartet die 21 raus. Wird hier wirklich nur die BYTE-reihenfolge vertauscht, und nicht die bit-reihenfolge? Das Ergebnis im Eingangspost hat ja auch eine vertauschte Bit-Order im Byte, das 21 sein sollte. Wenn ich array[1] manuell lese, kommt aber trotzdem 21 raus und nicht eine zu erwartende (mit bits vertauscht) 84.

    Woran liegt das? Mache ich hier etwas falsch?



  • Die CPU kann Bits nicht einzeln adressieren; die Reihenfolge, in der die Bits gespeichert sind, ist daher Ansichtssache. Die Bytereihenfolge ist von Architektur zu Architektur unterschiedlich; ein handelsüblicher PC (d. h. x86 bzw. x86-64) bzw. seine Instructions interpretieren Zahlen als little-endian, also mit dem niederwertigsten Byte vorn. Ein 4-Byte-Integer (32-Bit-Integer) mit dem Wert 258 wird durch die Byte-Folge "2 1 0 0" dargestellt, ein 16-Bit-Integer mit gleichem Wert durch "2 1". Das ist genau umgedreht von der Schreibweise, die man von Papier gewohnt ist.

    Andere Prozessorarchitekturen benutzen big-endian (also Papierschreibweise), und historisch gab es eine Reihe von Middle-Endian-Varianten, die das quer durcheinander würfeln. Siehe auch https://de.wikipedia.org/wiki/Byte-Reihenfolge

    Die Lösung deines Problems dürfte ntohs sein, siehe http://pubs.opengroup.org/onlinepubs/9699919799/functions/ntohl.html (POSIX) bzw. http://msdn.microsoft.com/en-us/library/windows/desktop/ms740075.aspx (Windows).



  • deine Datei hat eine BigEndian Byteordner, während dein Betriebssystem LittleEndian benutzt

    greetz KN4CK3R



  • Schau dir die Zahlen mal in Hex an:
    __2110 = 001516
    537610 = 150016
    Da kannst du das besser sehen.



  • Was hälst du denn hiervon?

    short to_short(unsigned char* b) 
    {
    	short i = 0;
    	i |= b[0] & 0xFF;
    	i <<= 8;
    	i |= b[1] & 0xFF;
    	return i;
    }
    int main()
    {
    	unsigned char bytes[] = "\0\1\2\3";
    	unsigned char* b = bytes;
    
    	for(unsigned i=0; i<sizeof(bytes)/2; ++i)
    	{
    		cout << to_short(b) << '\n';
    		b += 2;
    	}
    }
    


  • caster_caster schrieb:

    Kann ich die Byte Order des ausgelesenen Wertes jetzt irgendwie schnell umdrehen, oder durch einen Cast den Code dazu bringen die ersten zwei Elemente in einer anderen Byte-Order auszulesen?

    Das hier passiert dabei an einer sehr performanceintensiven Stelle (das wird alles ~ 6 millionen mal gemacht), weshalb es möglichst schnell sein sollte.

    Es gibt eine Grundregel beim Lesen, die lautet: Lese das, was Du haben willst.
    Wenn Du also short willst, so lese doch short s, das unsigned char array ist schlicht überflüssig. Lesen und Schreiben macht man in C++ mit Streams, und da der std::istream nur für das Lesen von Texten gemacht ist, schreibe man sich selbst einen Stream, der binär liest. Das ist gar nicht so schwer, wie es vielleicht im ersten Moment scheint - wie bei so vielen Dingen, man muss halt wissen wie's geht.

    Solange es bei den short s bleibt mit Wechsel des Endians, reicht folgendes aus:

    #include <algorithm> // std::reverse, std::swap
    #include <iostream>
    #include <fstream>
    #include <streambuf>
    #include <cassert>
    
    class ibinstream : public std::basic_ios< char >
    {
        typedef std::basic_ios< char > base_type;
    public:
        explicit ibinstream( std::streambuf* sb = 0 )
            : base_type( sb )
        {}
    
        ibinstream& operator>>( short& s )
        {
            if( good() )
            {
                std::ios_base::iostate state = std::ios_base::goodbit; // state = good;
                try
                {
                    if( rdbuf()->sgetn( reinterpret_cast< char* >( &s ), std::streamsize( sizeof(s) ) ) == std::streamsize( sizeof(s) ) )
                    {
                        // ok - gewünschte Bytes sind gelesen
                        assert( sizeof(s) == 2 ); // sonst: reverse( reinterpret_cast< char* >( &s ), reinterpret_cast< char* >( &s ) + sizeof( s ) );
                        std::swap( *reinterpret_cast< char* >( &s ), *(reinterpret_cast< char* >( &s )+1) ); // Endian wechseln
                    }
                    else
                        state |= std::ios_base::eofbit | std::ios_base::failbit;
                }
                catch( ... )
                {
                    state |= std::ios_base::badbit;
                    if( exceptions() & std::ios_base::badbit )
                        throw;
                }
                setstate( state );
            }
            return *this;
        }
    };
    
    int main()
    {
        using namespace std;
        ifstream file("egal.bin", ios_base::binary ); // mode == binary!
        if( !file.is_open() )
        {
            cerr << "Fehler beim Oeffnen" << endl;
            return -2;
        }
    
        ibinstream in( file.rdbuf() ); // ibinstream über den File-Buffer 'stülpen'
        int cnt = 0;
        for( short s; in >> s; ) // lesen bis EOF
            ++cnt; // hier kann das short 's' verarbeitet werden
        cout << cnt << "short gelesen" << endl;
    
        return 0;
    }
    

    Die Geschwindigkeit liegt bei mir knapp unter 100MByte/s (inklusive Einlesen von Platte) - die 6MByte sollten also in weniger als 0,1s erledigt sein.

    Mehr Beispiel für ibinstream gibt es hier & hier.

    out schrieb:

    Was hälst du denn hiervon?

    short to_short(unsigned char* b) 
    {
        ...
    

    Mit Verlaub - gar nichts. Ich kenne diesen Code .. klein und harmlos mit Dateien von ein paar Byte mag das ok sein. Hat man Dateien von mehreren 10 oder 100kByte und verteilen sich die Aufrufe von to_short über die Applikation, bekommt man große Probleme. Nirgends ist mehr klar, ob das to_short bereits aufgerufen wurde oder noch nicht.
    Wenn bei oben erwähnter Applikation nur all das Geld, was für die Pflege und Bugfixes von 'to_short' und 'to_dword' und .. aufgewendet wurde, auf meinen Konto wäre, käme leicht ein Jahresgehalt zusammen!
    Für professionellen Code ist das ein NO-GO!

    :xmas2: Werner



  • Werner Salomon schrieb:

    Hat man Dateien von mehreren 10 oder 100kByte und verteilen sich die Aufrufe von to_short über die Applikation, bekommt man große Probleme. Nirgends ist mehr klar, ob das to_short bereits aufgerufen wurde oder noch nicht.

    An der miesen Aufteilung des Programms ist also to_short schuld?
    Ein short ist ein short . Wie soll da nicht klar sein, ob to_short schon erfolgt ist?

    Werner Salomon schrieb:

    Für professionellen Code ist das ein NO-GO!

    Ein NO-GO ist implementationsabhängiges Verhalten durch reinterpret_cast -Gefrickel. Es hat schon seinen Grund, dass so etwas fast immer als unangebracht gilt.

    Für all die Profis was zu lesen: The byte order fallacy

    to_short macht es im Prinzip schon richtig.

    //vernünftiger Name, const, das unnötige unsigned weg
    short read_big_endian_short(const char *b)
    {
        short i = 0;
        i |= b[0];
        i <<= 8;
        i |= b[1];
        return i;
    }
    
    //allgemeiner und mit Fehlerbehandlung:
    template <class T>
    T read_big_endian_int(const char * &b, const char *end)
    {
    	T result = 0;
    	const char * const end_of_int = (b + sizeof(result));
    	if (end_of_int > end)
    	{
    		throw std::runtime_error("Unexpected end of read buffer");
    	}
    	for (; b != end_of_int; ++b)
    	{
    		result <<= 8;
    		result |= *b;
    	}
        return result;
    }
    
    #include <cstdint>
    
    //wird natürlich mit Typen bestimmter Größe verwendet
    read_big_endian_int<std::uint32_t>(pos, end);
    

    caster_caster schrieb:

    Das ganze läuft auf einem i7
    [...]
    Das hier passiert dabei an einer sehr performanceintensiven Stelle (das wird alles ~ 6 millionen mal gemacht), weshalb es möglichst schnell sein sollte.

    Ähh, 6 Millionen von diesen billigen Operationen macht so ein Prozessor im Schlaf.
    Deutlich teurer als das reine Zusammensetzen der short s ist bereits das Holen der Daten aus dem RAM in den Cache.
    Nochmal viel länger dauert das Lesen der Datei von einem Festspeicher.
    Der Teil, auf den du effektiv Einfluss hast, ist unbedeutend für die Geschwindigkeit.
    Viel wichtiger ist an der Stelle, dass das Programm korrekt arbeitet. Das ist mit einer portablen, übersichtlichen Umwandlung (siehe read_big_endian_int ) viel einfacher als mit Zeiger-Gefrickel.



  • TyRoXx schrieb:

    //vernünftiger Name, const, das unnötige unsigned weg
    short read_big_endian_short(const char *b)
    

    Unnötiges unsigned 😕 char nimmst du, wenn du mit alphanumerischen Zeichen arbeitest... und das ist doch hier gar nicht der Fall.
    Aber ich werde nun zukünftig eh Werners Rat befolgen. 🙂



  • @TyRoxx. Ich habe mich in der Zahl geirrt. Es sind eher 144 Millionen mal, die dieses Teil pro Sekunde durchlaufen wird. Wobei auch bei 6 Millionen mal "im Schlaf" wohl nicht genug wäre. Im Schlaf heisst dann üblicherweise auch mindestens ne halbe ms (wenns nicht sowieso viel mehr sind) und so viel Zeit zu verschwenden nur weil die Daten ungünstig im Speicher liegen tut mir weh.

    Ich mach das nun übrigens einfach mit inline Assembler:

    __asm
    {
    mov eax,val
    bswap eax
    mov val,eax
    }

    tada.

    Damit ihr nicht gleich mit der Premature-Optimization Keule schwingt: Ich hab hier ein Codestück, das von ~250ms Laufzeit auf ~120ms Laufzeit zu drücken ist. Algorithmisch ist in dem Codestück nichts mehr zu machen, weshalb ich jetzt Code-mäßig einiges rausholen muss. Bis jetzt läufts noch ganz gut, die (neu implementierte) Funktion ist geschätzt 50% fertig und läuft in 40-60ms durch (die Zeitmessung mit high-performance-countern schwankt hier ziemlich).


Anmelden zum Antworten