2013-02-19 12 views
8

Ich erstelle ein Dateiformat für meine Anwendung, und ich möchte natürlich, dass es sowohl auf Big-Endian- als auch auf Little-Endian-Systemen funktioniert. Ich habe bereits funktionierende Lösungen für die Verwaltung integraler Typen mit htonl und ntohl gefunden, aber ich bin ein bisschen fest, wenn Sie versuchen, das gleiche mit float und double Werte zu tun.Wie handhabe ich Unterschiede in der Byte-Reihenfolge beim Lesen/Schreiben von Fließkommatypen in C?

Angesichts der Art, wie Fließkommadarstellungen arbeiten, würde ich annehmen, dass die Standard-Byte-Reihenfolge-Funktionen nicht mit diesen Werten arbeiten. Ebenso bin ich nicht einmal ganz sicher, ob die Endlichkeit im traditionellen Sinn die Byte-Reihenfolge dieser Typen bestimmt.

Alles, was ich brauche, ist Konsistenz. Eine Möglichkeit, ein double zu schreiben und sicherzustellen, dass ich denselben Wert bekomme, wenn ich es wieder einlese. Wie kann ich das in C machen?

+2

Speichern als Text ist keine Option? – qPCR4vir

+0

@ qPCR4vir Das könnte eine Menge Leistung töten. – fuz

+0

Bibliotheken wie HDF5 (http://www.hdfgroup.org/HDF5/) kümmern sich um alles für Sie. Ich vermute, HDF5 könnte ein bisschen Schwergewicht für Ihre Bedürfnisse sein. –

Antwort

11

Eine andere Möglichkeit könnte sein, von <math.h> (C99) zu verwenden, um den Gleitkommawert in einen normierten Bruch (im Bereich [0,5, 1)] und eine ganzzahlige Potenz von 2 zu zerlegen Fraktion durch FLT_RADIXDBL_MANT_DIG eine ganze Zahl im Bereich [FLT_RADIXDBL_MANT_DIG/2, FLT_RADIXDBL_MANT_DIG) zu erhalten. Dann speichern Sie beide Ganzzahlen big-oder Little-Endian, je nachdem, was Sie in Ihrem Format wählen.

Wenn Sie eine gespeicherte Nummer laden, führen Sie den umgekehrten Vorgang aus und verwenden double ldexp(double x, int exp);, um den rekonstruierten Bruch mit der Potenz 2 zu multiplizieren.

Dies funktioniert am besten, wenn FLT_RADIX = 2 (praktisch alle Systeme, nehme ich an?) Und DBL_MANT_DIG < = 64.

Es muss überläuft zu vermeiden.

Beispielcode für doubles:

#include <limits.h> 
#include <float.h> 
#include <math.h> 
#include <string.h> 
#include <stdio.h> 

#if CHAR_BIT != 8 
#error currently supported only CHAR_BIT = 8 
#endif 

#if FLT_RADIX != 2 
#error currently supported only FLT_RADIX = 2 
#endif 

#ifndef M_PI 
#define M_PI 3.14159265358979324 
#endif 

typedef unsigned char uint8; 

/* 
    10-byte little-endian serialized format for double: 
    - normalized mantissa stored as 64-bit (8-byte) signed integer: 
     negative range: (-2^53, -2^52] 
     zero: 0 
     positive range: [+2^52, +2^53) 
    - 16-bit (2-byte) signed exponent: 
     range: [-0x7FFE, +0x7FFE] 

    Represented value = mantissa * 2^(exponent - 53) 

    Special cases: 
    - +infinity: mantissa = 0x7FFFFFFFFFFFFFFF, exp = 0x7FFF 
    - -infinity: mantissa = 0x8000000000000000, exp = 0x7FFF 
    - NaN:  mantissa = 0x0000000000000000, exp = 0x7FFF 
    - +/-0:  only one zero supported 
*/ 

void Double2Bytes(uint8 buf[10], double x) 
{ 
    double m; 
    long long im; // at least 64 bits 
    int ie; 
    int i; 

    if (isnan(x)) 
    { 
    // NaN 
    memcpy(buf, "\x00\x00\x00\x00\x00\x00\x00\x00" "\xFF\x7F", 10); 
    return; 
    } 
    else if (isinf(x)) 
    { 
    if (signbit(x)) 
     // -inf 
     memcpy(buf, "\x00\x00\x00\x00\x00\x00\x00\x80" "\xFF\x7F", 10); 
    else 
     // +inf 
     memcpy(buf, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F" "\xFF\x7F", 10); 
    return; 
    } 

    // Split double into normalized mantissa (range: (-1, -0.5], 0, [+0.5, +1)) 
    // and base-2 exponent 
    m = frexp(x, &ie); // x = m * 2^ie exactly for FLT_RADIX=2 
        // frexp() can't fail 
    // Extract most significant 53 bits of mantissa as integer 
    m = ldexp(m, 53); // can't overflow because 
        // DBL_MAX_10_EXP >= 37 equivalent to DBL_MAX_2_EXP >= 122 
    im = trunc(m); // exact unless DBL_MANT_DIG > 53 

    // If the exponent is too small or too big, reduce the number to 0 or 
    // +/- infinity 
    if (ie > 0x7FFE) 
    { 
    if (im < 0) 
     // -inf 
     memcpy(buf, "\x00\x00\x00\x00\x00\x00\x00\x80" "\xFF\x7F", 10); 
    else 
     // +inf 
     memcpy(buf, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F" "\xFF\x7F", 10); 
    return; 
    } 
    else if (ie < -0x7FFE) 
    { 
    // 0 
    memcpy(buf, "\x00\x00\x00\x00\x00\x00\x00\x00" "\x00\x00", 10); 
    return; 
    } 

    // Store im as signed 64-bit little-endian integer 
    for (i = 0; i < 8; i++, im >>= 8) 
    buf[i] = (uint8)im; 

    // Store ie as signed 16-bit little-endian integer 
    for (i = 8; i < 10; i++, ie >>= 8) 
    buf[i] = (uint8)ie; 
} 

void Bytes2Double(double* x, const uint8 buf[10]) 
{ 
    unsigned long long uim; // at least 64 bits 
    long long im; // ditto 
    unsigned uie; 
    int ie; 
    double m; 
    int i; 
    int negative = 0; 
    int maxe; 

    if (!memcmp(buf, "\x00\x00\x00\x00\x00\x00\x00\x00" "\xFF\x7F", 10)) 
    { 
#ifdef NAN 
    *x = NAN; 
#else 
    *x = 0; // NaN is not supported, use 0 instead (we could return an error) 
#endif 
    return; 
    } 

    if (!memcmp(buf, "\x00\x00\x00\x00\x00\x00\x00\x80" "\xFF\x7F", 10)) 
    { 
    *x = -INFINITY; 
    return; 
    } 
    else if (!memcmp(buf, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F" "\xFF\x7F", 10)) 
    { 
    *x = INFINITY; 
    return; 
    } 

    // Load im as signed 64-bit little-endian integer 
    uim = 0; 
    for (i = 0; i < 8; i++) 
    { 
    uim >>= 8; 
    uim |= (unsigned long long)buf[i] << (64 - 8); 
    } 
    if (uim <= 0x7FFFFFFFFFFFFFFFLL) 
    im = uim; 
    else 
    im = (long long)(uim - 0x7FFFFFFFFFFFFFFFLL - 1) - 0x7FFFFFFFFFFFFFFFLL - 1; 

    // Obtain the absolute value of the mantissa, make sure it's 
    // normalized and fits into 53 bits, else the input is invalid 
    if (im > 0) 
    { 
    if (im < (1LL << 52) || im >= (1LL << 53)) 
    { 
#ifdef NAN 
     *x = NAN; 
#else 
     *x = 0; // NaN is not supported, use 0 instead (we could return an error) 
#endif 
     return; 
    } 
    } 
    else if (im < 0) 
    { 
    if (im > -(1LL << 52) || im <= -(1LL << 53)) 
    { 
#ifdef NAN 
     *x = NAN; 
#else 
     *x = 0; // NaN is not supported, use 0 instead (we could return an error) 
#endif 
     return; 
    } 
    negative = 1; 
    im = -im; 
    } 

    // Load ie as signed 16-bit little-endian integer 
    uie = 0; 
    for (i = 8; i < 10; i++) 
    { 
    uie >>= 8; 
    uie |= (unsigned)buf[i] << (16 - 8); 
    } 
    if (uie <= 0x7FFF) 
    ie = uie; 
    else 
    ie = (int)(uie - 0x7FFF - 1) - 0x7FFF - 1; 

    // If DBL_MANT_DIG < 53, truncate the mantissa 
    im >>= (53 > DBL_MANT_DIG) ? (53 - DBL_MANT_DIG) : 0; 

    m = im; 
    m = ldexp(m, (53 > DBL_MANT_DIG) ? -DBL_MANT_DIG : -53); // can't overflow 
      // because DBL_MAX_10_EXP >= 37 equivalent to DBL_MAX_2_EXP >= 122 

    // Find out the maximum base-2 exponent and 
    // if ours is greater, return +/- infinity 
    frexp(DBL_MAX, &maxe); 
    if (ie > maxe) 
    m = INFINITY; 
    else 
    m = ldexp(m, ie); // underflow may cause a floating-point exception 

    *x = negative ? -m : m; 
} 

int test(double x, const char* name) 
{ 
    uint8 buf[10], buf2[10]; 
    double x2; 
    int error1, error2; 

    Double2Bytes(buf, x); 
    Bytes2Double(&x2, buf); 
    Double2Bytes(buf2, x2); 

    printf("%+.15E '%s' -> %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X\n", 
     x, 
     name, 
     buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7],buf[8],buf[9]); 

    if ((error1 = memcmp(&x, &x2, sizeof(x))) != 0) 
    puts("Bytes2Double(Double2Bytes(x)) != x"); 

    if ((error2 = memcmp(buf, buf2, sizeof(buf))) != 0) 
    puts("Double2Bytes(Bytes2Double(Double2Bytes(x))) != Double2Bytes(x)"); 

    puts(""); 

    return error1 || error2; 
} 

int testInf(void) 
{ 
    uint8 buf[10]; 
    double x, x2; 
    int error; 

    x = DBL_MAX; 
    Double2Bytes(buf, x); 
    if (!++buf[8]) 
    ++buf[9]; // increment the exponent beyond the maximum 
    Bytes2Double(&x2, buf); 

    printf("%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X -> %+.15E\n", 
     buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7],buf[8],buf[9], 
     x2); 

    if ((error = !isinf(x2)) != 0) 
    puts("Bytes2Double(Double2Bytes(DBL_MAX) * 2) != INF"); 

    puts(""); 

    return error; 
} 

#define VALUE_AND_NAME(V) { V, #V } 

const struct 
{ 
    double value; 
    const char* name; 
} testData[] = 
{ 
#ifdef NAN 
    VALUE_AND_NAME(NAN), 
#endif 
    VALUE_AND_NAME(0.0), 
    VALUE_AND_NAME(+DBL_MIN), 
    VALUE_AND_NAME(-DBL_MIN), 
    VALUE_AND_NAME(+1.0), 
    VALUE_AND_NAME(-1.0), 
    VALUE_AND_NAME(+M_PI), 
    VALUE_AND_NAME(-M_PI), 
    VALUE_AND_NAME(+DBL_MAX), 
    VALUE_AND_NAME(-DBL_MAX), 
    VALUE_AND_NAME(+INFINITY), 
    VALUE_AND_NAME(-INFINITY), 
}; 

int main(void) 
{ 
    unsigned i; 
    int errors = 0; 

    for (i = 0; i < sizeof(testData)/sizeof(testData[0]); i++) 
    errors += test(testData[i].value, testData[i].name); 

    errors += testInf(); 

    // Test subnormal values. A floating-point exception may be raised. 
    errors += test(+DBL_MIN/2, "+DBL_MIN/2"); 
    errors += test(-DBL_MIN/2, "-DBL_MIN/2"); 

    printf("%d error(s)\n", errors); 

    return 0; 
} 

Ausgang (ideone):

+NAN 'NAN' -> 00 00 00 00 00 00 00 00 FF 7F 

+0.000000000000000E+00 '0.0' -> 00 00 00 00 00 00 00 00 00 00 

+2.225073858507201E-308 '+DBL_MIN' -> 00 00 00 00 00 00 10 00 03 FC 

-2.225073858507201E-308 '-DBL_MIN' -> 00 00 00 00 00 00 F0 FF 03 FC 

+1.000000000000000E+00 '+1.0' -> 00 00 00 00 00 00 10 00 01 00 

-1.000000000000000E+00 '-1.0' -> 00 00 00 00 00 00 F0 FF 01 00 

+3.141592653589793E+00 '+M_PI' -> 18 2D 44 54 FB 21 19 00 02 00 

-3.141592653589793E+00 '-M_PI' -> E8 D2 BB AB 04 DE E6 FF 02 00 

+1.797693134862316E+308 '+DBL_MAX' -> FF FF FF FF FF FF 1F 00 00 04 

-1.797693134862316E+308 '-DBL_MAX' -> 01 00 00 00 00 00 E0 FF 00 04 

+INF '+INFINITY' -> FF FF FF FF FF FF FF 7F FF 7F 

-INF '-INFINITY' -> 00 00 00 00 00 00 00 80 FF 7F 

FF FF FF FF FF FF 1F 00 01 04 -> +INF 

+1.112536929253601E-308 '+DBL_MIN/2' -> 00 00 00 00 00 00 10 00 02 FC 

-1.112536929253601E-308 '-DBL_MIN/2' -> 00 00 00 00 00 00 F0 FF 02 FC 

0 error(s) 
+0

Großartig, danke. Ich denke, meine einzige Sorge wäre Überläufe. Wie würdest du ihnen den Umgang empfehlen? Ist diese Methode auch genau? –

+0

Wenn Gleitkommawerte Base-2 sind, muss dies genau sein. Die einzigen Überläufe, auf die Sie stoßen können, sind solche, bei denen entweder das Format der CPU einen größeren Exponentenbereich als das Format hat oder umgekehrt. Möglicherweise müssen Sie dies überprüfen und spezielle Werte für unendlich oder den Maximalwert verwenden, wenn Unendlichkeit nicht unterstützt wird. Möglicherweise haben Sie ein ähnliches Problem mit der Anzahl der Stellen/Bits in der Mantisse. Sie müssen es abschneiden oder runden, wenn es weder in die CPU noch in den Datei-Slot passt. –

+0

Sie können den Code für IEEE-754 so optimieren, dass, wenn die CPU IEEE-754 unterstützt, Sie nichts besonderes tun, keine Prüfungen. –

2

Je nach Anwendung kann es sinnvoll sein, ein einfaches Textdatenformat zu verwenden (eine Möglichkeit ist XML). Wenn Sie keinen Speicherplatz verschwenden möchten, können Sie es komprimieren.

+0

'% a' ist möglicherweise eine bessere Wahl als'% f'/'% e' /'% g' beim Schreiben von Gleitkommawerten als Text. Nicht so gut lesbar, sollte aber das Abschneiden von Dezimalziffern vermeiden oder zu viele davon haben. –

3

Gleitkommawerte verwenden die gleiche Byte-Reihenfolge wie Integralwerte imho. Verwenden Sie eine Vereinigung sich mit dem jeweiligen integralen Gegenstück zu überlappen, und verwenden Sie die gemeinsame Hton Funktionen:

float htonf(float x) { 
    union foo { 
    float f; 
    uint32_t i; 
    } foo = { .f = x }; 

    foo.i = htonl(foo.i); 
    return foo.f; 
} 
+0

Es gab eine Plattform, wo Ints und Floats unterschiedliche Endianness hatten. Aber wir wollen diese nicht mehr unterstützen. –

0

XML ist wahrscheinlich der meist tragbare Weg, es zu tun.

Es scheint jedoch, dass Sie bereits die meisten Parser gebaut haben, aber auf dem float/double-Problem stecken. Ich würde vorschlagen, es als eine Zeichenfolge (zu welcher Präzision, die Sie wünschen) zu schreiben und dann wieder einlesen.

Wenn alle Ihre Zielplattformen IEEE-754 floats (und verdoppelt) verwenden, funktionieren keine Byte-Swapping-Tricks Sie.

+0

Warte ... gibt es heutzutage Plattformen, die keine IEEE 754 Floats verwenden? – fuz

+0

Ich glaube nicht, gibt es irgendeine Garantie über die Reihenfolge der Bits in einem IEEE-754 float/double im RAM. Es könnte alles sein und du solltest seinen Inhalt nicht direkt manipulieren. –

+0

Das ist ein interessanter Beitrag darüber, wie Sie Ihre Plattform der Implementierung der doppelten Konform IEEE-754, um sicherzustellen, dass: http://stackoverflow.com/a/753018/1384030 –

0

Wenn Sie sicherstellen, dass Ihre Implementierungen serialisierte Gleitkommadarstellungen immer in einem bestimmten Format behandeln, ist alles in Ordnung (IEEE 754 ist üblich).

Ja, Architekturen können Fließkommazahlen anders ordnen (z. B. in Big oder Little Endian). Daher möchten Sie die Endianz irgendwie angeben. Dies könnte in der Spezifikation oder Variable des Formats sein und in den Dateidaten aufgezeichnet sein.

Der letzte große Fehler ist, dass die Ausrichtung für Builtins variieren kann. Wie Ihre Hardware/Ihr Prozessor ungültige Daten verarbeitet, ist in der Implementierung definiert. Daher müssen Sie möglicherweise die Daten/Bytes austauschen und dann zum Ziel float/double verschieben.

0

Eine Bibliothek wie HDF5 oder sogar NetCDF ist wahrscheinlich ein bisschen Schwergewicht für diese als High Performance Mark sagte, es sei denn, Sie benötigen auch die anderen Funktionen, die in diesen Bibliotheken verfügbar sind.

Eine leichtere Alternative, die nur mit der Serialisierung beschäftigt wäre z.B. XDR (siehe auch wikipedia description). Viele Betriebssysteme stellen XDR-Routinen bereit, wenn dies nicht ausreicht, gibt es auch frei verfügbare XDR-Bibliotheken.