Domanda

In un programma open source I ha scritto , sto leggendo i dati binari (scritti da un altro programma) da un file e producendo ints, doppi, e altri tipi di dati assortiti. Una delle sfide è che deve eseguito su macchine a 32 e 64 bit di entrambe le endiannesses, il che significa che I finiscono per dover fare un po 'di manipolazione a basso livello. Conosco un (molto) un po 'di punzonatura e aliasing rigoroso e voglio essere sicuro di esserlo fare le cose nel modo giusto.

Fondamentalmente, è facile convertire da un carattere * a un int di varie dimensioni:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    return *(int64_t *) buf;
}

e ho un cast di funzioni di supporto per scambiare ordini di byte secondo necessità, ad esempio come:

int64_t swappedint64_t(const int64_t wrongend)
{
    /* Change the endianness of a 64-bit integer */
    return (((wrongend & 0xff00000000000000LL) >> 56) |
            ((wrongend & 0x00ff000000000000LL) >> 40) |
            ((wrongend & 0x0000ff0000000000LL) >> 24) |
            ((wrongend & 0x000000ff00000000LL) >> 8)  |
            ((wrongend & 0x00000000ff000000LL) << 8)  |
            ((wrongend & 0x0000000000ff0000LL) << 24) |
            ((wrongend & 0x000000000000ff00LL) << 40) |
            ((wrongend & 0x00000000000000ffLL) << 56));
}

In fase di esecuzione, il programma rileva l'endianness della macchina e assegna uno dei precedenti a un puntatore a funzione:

int64_t (*slittleint64_t)(const char *);
if(littleendian) {
    slittleint64_t = snativeint64_t;
} else {
    slittleint64_t = sswappedint64_t;
}

Ora, la parte difficile arriva quando provo a lanciare un carattere * su un doppio. mi piacerebbe piace riutilizzare il codice di scambio endian in questo modo:

union 
{
    double  d;
    int64_t i;
} int64todouble;

int64todouble.i = slittleint64_t(bufoffset);
printf("%lf", int64todouble.d);

Tuttavia, alcuni compilatori potrebbero ottimizzare "int64todouble.i" assegnazione e interrompere il programma. C'è un modo più sicuro per farlo, mentre si considera che questo programma deve rimanere ottimizzato per le prestazioni, e anche che lo farei preferisco non scrivere un insieme parallelo di trasformazioni su cui castare * raddoppiare direttamente? Se il metodo di punizione sindacale è sicuro, dovrei essere riscrivere le mie funzioni come snativeint64_t per usarlo?


Ho finito per usare la risposta di Steve Jessop perché le funzioni di conversione sono state riscritte per usare memcpy, in questo modo:

int64_t snativeint64_t(const char *buf) 
{
    /* Interpret the first 8 bytes of buf as a 64-bit int */
    int64_t output;
    memcpy(&output, buf, 8);
    return output;
}

compilato nello stesso assemblatore esatto del mio codice originale:

snativeint64_t:
        movq    (%rdi), %rax
        ret

Dei due, la versione memcpy esprime più esplicitamente quello che sto cercando di fare e dovrebbe funzionare anche con i compilatori più ingenui.

Adam, anche la tua risposta è stata meravigliosa e ho imparato molto da essa. Grazie per la pubblicazione!

È stato utile?

Soluzione

Dal momento che sembra che tu sappia abbastanza dell'implementazione per essere sicuro che int64_t e double abbiano le stesse dimensioni e abbiano rappresentazioni di archiviazione adeguate, potresti rischiare un memcpy. Quindi non devi nemmeno pensare all'aliasing.

Dato che stai usando un puntatore a funzione per una funzione che potrebbe essere facilmente incorporata se eri disposto a rilasciare più binari, le prestazioni non devono comunque essere un grosso problema, ma potresti voler sapere che alcuni compilatori possono essere piuttosto diabolici ottimizzazione di memcpy: per dimensioni di interi di piccole dimensioni è possibile includere un insieme di carichi e negozi e potresti persino scoprire che le variabili sono completamente ottimizzate e il compilatore esegue il "copia" semplicemente riassegnando gli slot dello stack che sta utilizzando per le variabili, proprio come un'unione.

int64_t i = slittleint64_t(buffoffset);
double d;
memcpy(&d,&i,8); /* might emit no code if you're lucky */
printf("%lf", d);

Esamina il codice risultante o semplicemente profilalo. Probabilmente anche nel peggiore dei casi non sarà lento.

In generale, tuttavia, fare qualcosa di troppo intelligente con lo scambio di byte comporta problemi di portabilità. Esistono ABI con doppio di end-endian, dove ogni parola è little-endian, ma la parola più grande viene prima.

Normalmente potresti considerare di memorizzare i tuoi doppi usando sprintf e sscanf, ma per il tuo progetto i formati di file non sono sotto il tuo controllo. Ma se la tua applicazione sta solo spalando IEEE raddoppia da un file di input in un formato a un file di output in un altro formato (non sono sicuro che lo sia, dal momento che non conosco i formati del database in questione, ma se è così), allora forse tu posso dimenticare il fatto che è un doppio, dal momento che non lo usi comunque per l'aritmetica. Basta trattarlo come un carattere opaco [8], che richiede il byteswapping solo se i formati di file differiscono.

Altri suggerimenti

Consiglio vivamente di leggere Capire Alias ??rigoroso . In particolare, vedere le sezioni etichettate "Casting through a union". Ha un numero di esempi molto buoni. Mentre l'articolo è su un sito Web sul processore Cell e utilizza esempi di assemblaggio PPC, quasi tutto è ugualmente applicabile ad altre architetture, tra cui x86.

Lo standard dice che scrivere su un campo di un sindacato e leggere da esso immediatamente è un comportamento indefinito. Quindi, se segui il libro delle regole, il metodo basato sul sindacato non funzionerà.

Le macro sono generalmente una cattiva idea, ma questa potrebbe essere un'eccezione alla regola. Dovrebbe essere possibile ottenere un comportamento simile a un modello in C usando una serie di macro usando i tipi di input e output come parametri.

Come piccolo suggerimento secondario, ti suggerisco di indagare se puoi scambiare il mascheramento e lo spostamento, nel caso a 64 bit. Poiché l'operazione sta scambiando byte, dovresti essere sempre in grado di cavartela con una maschera di 0xff . Ciò dovrebbe portare a un codice più veloce e più compatto, a meno che il compilatore non sia abbastanza intelligente da capirlo da solo.

In breve, cambiando questo:

(((wrongend & 0xff00000000000000LL) >> 56)

in questo:

((wrongend >> 56) & 0xff)

dovrebbe generare lo stesso risultato.

Modifica:
Rimossi i commenti su come archiviare in modo efficace dati sempre big endian e passare a endianess macchina, poiché l'interrogante non ha menzionato un altro programma scrive i suoi dati (che sono informazioni importanti).

Tuttavia, se i dati devono essere convertiti da qualsiasi endian a big e da big a host endian, ntohs / ntohl / htons / htonl sono i metodi migliori, più eleganti e imbattibili in termini di velocità (in quanto eseguiranno attività in hardware se la CPU lo supporta, non si può battere).


Per quanto riguarda double / float, è sufficiente archiviarli in ints tramite il cast di memoria:

double d = 3.1234;
printf("Double %f\n", d);
int64_t i = *(int64_t *)&d;
// Now i contains the double value as int
double d2 = *(double *)&i;
printf("Double2 %f\n", d2);

Avvolgilo in una funzione

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

double int64ToDouble(int64_t i)
{
    return *(double *)&i;
}

Il richiedente ha fornito questo link:

http: // cocoawithlove .com / 2008/04 / usando puntatori-a-rifusione-in-c-è-bad.html

a riprova del fatto che il casting è negativo ... sfortunatamente non posso essere assolutamente d'accordo con la maggior parte di questa pagina. Citazioni e commenti:

  

Comune come il casting tramite un puntatore   è, in realtà è una cattiva pratica e   codice potenzialmente rischioso. getto   attraverso un puntatore ha il potenziale per   creare bug a causa della punzonatura di tipo.

Non è affatto rischioso e non è neanche una cattiva pratica. Ha solo il potenziale per causare bug se lo fai in modo errato, proprio come la programmazione in C ha il potenziale per causare bug se lo fai in modo errato, così fa qualsiasi programmazione in qualsiasi linguaggio. Con questo argomento devi interrompere del tutto la programmazione.

  

Tipo di punzonatura
Una forma di puntatore   aliasing in cui due puntatori e riferimenti   nella stessa posizione in memoria ma   rappresenta quella posizione come diversa   tipi. Il compilatore tratterà entrambi   & Quot; giochi di parole " come puntatori non correlati. genere   la punizione ha il potenziale per causare   problemi di dipendenza per qualsiasi dato   accessibile tramite entrambi i puntatori.

Questo è vero, ma sfortunatamente totalmente estraneo al mio codice .

Ciò a cui si riferisce è un codice come questo:

int64_t * intPointer;
:
// Init intPointer somehow
:
double * doublePointer = (double *)intPointer;

Ora doublePointer e intPointer puntano entrambi alla stessa posizione di memoria, ma trattano lo stesso tipo. Questa è la situazione che dovresti risolvere con un'unione, qualsiasi altra cosa è piuttosto brutta. Male, non è quello che fa il mio codice!

Il mio codice viene copiato per valore , non per riferimento . Ho lanciato un doppio puntatore a int64 (o viceversa) e deferenza immediatamente . Una volta che le funzioni ritornano, non c'è nessun puntatore a nulla. C'è un int64 e un doppio e questi sono totalmente estranei al parametro di input delle funzioni. Non copio mai alcun puntatore su un puntatore di un tipo diverso (se lo hai visto nel mio esempio di codice, hai letto male il codice C che ho scritto), trasferisco semplicemente il valore a una variabile di tipo diverso (in una propria posizione di memoria) . Quindi la definizione di tipo di punzonatura non si applica affatto, come dice "si riferiscono alla stessa posizione in memoria" e nulla qui si riferisce alla stessa posizione di memoria.

int64_t intValue = 12345;
double doubleValue = int64ToDouble(intValue);
// The statement below will not change the value of doubleValue!
// Both are not pointing to the same memory location, both have their
// own storage space on stack and are totally unreleated.
intValue = 5678;

Il mio codice non è altro che una copia di memoria, scritto in C senza una funzione esterna.

int64_t doubleToInt64(double d)
{
    return *(int64_t *)&d;
}

Potrebbe essere scritto come

int64_t doubleToInt64(double d)
{
    int64_t result;
    memcpy(&result, &d, sizeof(d));
    return result;
}

Non è altro che questo, quindi non c'è alcun tipo di punzonatura nemmeno in vista da nessuna parte. E questa operazione è anche totalmente sicura, tanto sicura quanto un'operazione può essere in C. Un doppio è definito per essere sempre 64 Bit (a differenza di int non varia di dimensioni, è fissato a 64 bit), quindi si adatterà sempre in una variabile di dimensioni int64_t.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top