2013-02-19 9 views
8

Sto inventando un formato di file per la mia applicazione, e ovviamente mi piacerebbe che funzionasse su entrambi i sistemi big-endian e little-endian. Ho già trovato soluzioni di lavoro per la gestione di tipi interi utilizzando htonl e ntohl, ma sono un po 'bloccato quando si cerca di fare lo stesso con i valori float e double.Come gestisco le differenze di ordine dei byte durante la lettura/scrittura di tipi in virgola mobile in C?

Data la natura di come funzionano le rappresentazioni in virgola mobile, suppongo che le funzioni di ordine byte standard non funzionino su questi valori. Allo stesso modo, non sono nemmeno del tutto sicuro che l'endianità nel senso tradizionale sia ciò che governa l'ordine dei byte di questi tipi.

Tutto ciò di cui ho bisogno è la coerenza. Un modo per scrivere un double e assicurarmi di ottenere lo stesso valore quando lo leggo di nuovo. Come posso farlo in C?

+2

Salvarlo come testo non è un'opzione? – qPCR4vir

+0

@ qPCR4vir Questo potrebbe uccidere un sacco di prestazioni. – fuz

+0

Librerie come HDF5 (http://www.hdfgroup.org/HDF5/) si prendono cura di tutto ciò per te. Sospetto che HDF5 potrebbe essere un po 'pesante per le tue esigenze. –

risposta

11

Un'altra opzione potrebbe essere quella di utilizzare double frexp(double value, int *exp); da <math.h> (C99) per abbattere il valore in virgola mobile in una frazione normalizzata (nell'intervallo [0,5, 1)) e una potenza integrale 2. È quindi possibile moltiplicare il frazione per FLT_RADIXDBL_MANT_DIG per ottenere un numero intero nell'intervallo [FLT_RADIXDBL_MANT_DIG/2, FLT_RADIXDBL_MANT_DIG). Quindi si salvano entrambi i numeri interi big o little-endian, a seconda di quale si sceglie nel formato.

Quando si carica un numero salvato, si esegue l'operazione inversa e si utilizza double ldexp(double x, int exp); per moltiplicare la frazione ricostruita in base alla potenza di 2.

Questo funzionerà meglio quando FLT_RADIX = 2 (praticamente tutti i sistemi, suppongo?) E DBL_MANT_DIG < = 64.

Prestare attenzione per evitare traboccamenti.

Codice di esempio per 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; 
} 

uscita (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

Grande, grazie. Immagino che la mia unica preoccupazione rimasta sarebbe traboccare. Come consiglieresti di gestirli? Inoltre, questo metodo è esatto? –

+0

Se i valori a virgola mobile sono di base 2, questo deve essere esatto. Gli unici overflow in cui puoi incorrere sono quelli in cui il formato della CPU ha una gamma più ampia di esponente rispetto al formato o viceversa. Potrebbe essere necessario controllarlo e utilizzare valori speciali per l'infinito o il valore massimo se l'infinito non è supportato. Potresti avere un problema simile al numero di cifre/bit nella mantissa. Sarà necessario troncarlo o arrotondarlo se non si adatta alla CPU o allo slot del file. –

+0

È possibile ottimizzare il codice per IEEE-754 in modo che se la CPU supporta IEEE-754, non si fa nulla di speciale, nessun controllo. –

2

A seconda dell'applicazione potrebbe essere una buona idea utilizzare un formato di dati di testo semplice (una possibilità è XML). Se non vuoi sprecare spazio su disco, puoi comprimerlo.

+0

'% a' potrebbe essere una scelta migliore di'% f'/'% e' /'% g' quando si scrivono valori a virgola mobile come testo. Non è leggibile, ma dovrebbe evitare il troncamento delle cifre decimali o averne troppe. –

3

I valori in virgola mobile utilizzano lo stesso ordine di byte dei valori interi imho. Utilizzare un unione di sovrapporli con la rispettiva controparte integrale e utilizzare le funzioni comuni hton:

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

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

C'è stata una piattaforma in cui ints e float avevano endianness differenti. Ma potremmo non interessarci più a quelli. –

0

XML è probabilmente il modo più portatile per farlo.

Tuttavia, sembra che la maggior parte del parser sia già stata creata, ma che siano bloccati sul float/doppio problema. Suggerirei di scriverlo come una stringa (a qualsiasi livello di precisione desideri) e poi rileggerlo.

A meno che tutte le piattaforme di destinazione non utilizzino i float IEEE-754 (e raddoppia), nessun trucchetto di scambio di byte funzionerà per tu.

+0

Aspetta ... ci sono piattaforme al giorno d'oggi che non usano i float IEEE 754? – fuz

+0

Non credo ci sia alcuna garanzia circa l'ordine dei bit in un IEEE-754 float/double nella RAM. Potrebbe essere qualsiasi cosa e non si dovrebbe manipolare il suo contenuto direttamente. –

+0

Questo è un post interessante su come garantire che l'implementazione della piattaforma di doppio conforme a IEEE-754: http://stackoverflow.com/a/753018/1384030 –

0

Se si garantisce che le implementazioni gestiscano sempre rappresentazioni in virgola mobile serializzate in un formato specificato, allora si andrà bene (IEEE 754 è comune).

Sì, le architetture possono ordinare numeri in virgola mobile in modo diverso (ad esempio in big o little endian). Pertanto, vorrete in qualche modo specificare l'endianness. Questo potrebbe essere nella specifica o nella variabile del formato e registrato nei dati del file.

L'ultimo grave errore è che l'allineamento per i builtin può variare. Il modo in cui il tuo hardware/processore gestisce i dati disallineati è definito dall'implementazione. Quindi potrebbe essere necessario scambiare i dati/i byte, quindi spostarli nella destinazione float/double.

0

una libreria come HDF5 o addirittura NetCDF è probabilmente uno dei pesi massimi po 'per questo, come High Performance Mark ha detto, a meno che non anche bisogno delle altre funzionalità disponibili in quelle librerie.

Un'alternativa più leggera che riguarda solo la serializzazione sarebbe ad es. XDR (vedere anche wikipedia description). Molti sistemi operativi forniscono le routine XDR immediatamente, se questo non è sufficiente esistono anche librerie XDR free-standing.

Problemi correlati