Question

Dans un open source programme I écrit , je lis des données binaires (écrites par un autre programme) à partir d’un fichier et produisant des données ints, doubles, et d'autres types de données assortis. L’un des défis est qu’il doit fonctionner sur des machines 32 bits et 64 bits des deux endianités, ce qui signifie que je finissent par avoir à faire un peu de bricolage de bas niveau. Je connais un (très) peu de choses sur le type punning et aliasing strict et veulent vous assurer que je suis faire les choses comme il faut.

En gros, il est facile de convertir un caractère * en un entier de tailles différentes:

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

et j’ai un ensemble de fonctions de support pour permuter les ordres d’octets selon les besoins, tels que comme:

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));
}

Au moment de l'exécution, le programme détecte la finalité de la machine et attribue un des éléments ci-dessus à un pointeur de fonction:

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

Maintenant, la partie délicate vient quand j'essaye de lancer un caractère * sur un double. J'aurais tiens à réutiliser le code endian-swapping comme suit:

union 
{
    double  d;
    int64_t i;
} int64todouble;

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

Cependant, certains compilateurs pourraient optimiser le fichier "int64todouble.i". affectation et casser le programme. Y at-il un moyen plus sûr de le faire, tout en considérant que ce programme doit rester optimisé pour la performance, et aussi que je préférez ne pas écrire un ensemble parallèle de transformations vers lesquelles lancer char * doubler directement? Si la méthode syndicale de punition est sûre, devrais-je être réécrire mes fonctions comme snativeint64_t pour l'utiliser?

J'ai fini par utiliser la réponse de Steve Jessop parce que les fonctions de conversion ont été réécrites pour utiliser memcpy, comme suit:

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;
}

compilé dans le même assembleur que mon code d'origine:

snativeint64_t:
        movq    (%rdi), %rax
        ret

Des deux versions, la version de memcpy exprime plus explicitement ce que je cherche à faire et devrait fonctionner même pour les compilateurs les plus naïfs.

Adam, ta réponse était aussi merveilleuse et j’en ai appris beaucoup. Merci d'avoir posté!

Était-ce utile?

La solution

Étant donné que vous semblez en savoir assez sur votre implémentation pour vous assurer que int64_t et double ont la même taille et que les représentations de stockage sont appropriées, vous risquez un risque. Dans ce cas, vous n’aurez même pas à penser au repliement.

Etant donné que vous utilisez un pointeur de fonction pour une fonction qui pourrait facilement être intégrée si vous êtes prêt à publier plusieurs fichiers binaires, les performances ne doivent pas être un gros problème, mais vous voudrez peut-être savoir que certains compilateurs peuvent être assez diaboliques. optimizing memcpy - pour les petites tailles entières, un ensemble de charges et de magasins peut être en ligne, et vous pouvez même trouver que les variables sont optimisées et que le compilateur effectue la "copie". réaffectez simplement les emplacements de pile utilisés pour les variables, comme pour une union.

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

Examinez le code obtenu ou profilez-le simplement. Les chances sont même dans le pire des cas, il ne sera pas lent.

En général, toutefois, tout ce qui est trop intelligent avec l'échange d'octets entraîne des problèmes de portabilité. Il existe des ABI avec des doubles milieu-endian, où chaque mot est petit-endian, mais le gros mot vient en premier.

Normalement, vous pouvez envisager de stocker vos doubles avec sprintf et sscanf, mais pour votre projet, les formats de fichier ne sont pas sous votre contrôle. Mais si votre application est simplement en train de pelleter IEEE double d'un fichier d'entrée dans un format à un fichier de sortie dans un autre format (je ne sais pas si c'est le cas, car je ne connais pas les formats de base de données en question, mais si c'est le cas), alors peut-être peut oublier le fait que c'est un double, puisque vous ne l'utilisez pas pour le calcul de toute façon. Traitez-le simplement comme un caractère opaque [8], ne nécessitant un échange d'octets que si les formats de fichier diffèrent.

Autres conseils

Je vous suggère fortement de lire Comprendre l'alias strict. . Plus précisément, reportez-vous aux sections intitulées "Incarcération d’un syndicat". Il contient de très bons exemples. Bien que l'article se trouve sur un site Web concernant le processeur Cell et utilise des exemples d'assemblys PPC, il est presque également applicable à d'autres architectures, y compris x86.

La norme indique que l’écriture dans un domaine d’une union et sa lecture immédiate constituent un comportement indéfini. Donc, si vous respectez les règles, la méthode basée sur l'union ne fonctionnera pas.

Les macros sont généralement une mauvaise idée, mais cela peut constituer une exception à la règle. Il devrait être possible d'obtenir un comportement semblable à un modèle en C en utilisant un ensemble de macros en utilisant les types d'entrée et de sortie comme paramètres.

En guise de très petite suggestion, je vous suggère de rechercher si vous pouvez échanger le masquage et le décalage, dans le cas du 64 bits. Étant donné que l'opération consiste à permuter des octets, vous devriez pouvoir toujours vous en sortir avec un masque de seulement 0xff . Cela devrait conduire à un code plus rapide et plus compact, à moins que le compilateur soit suffisamment intelligent pour le comprendre lui-même.

En bref, changer ceci:

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

dans ceci:

((wrongend >> 56) & 0xff)

devrait générer le même résultat.

Modifier:
Suppression des commentaires sur la manière de stocker efficacement les données toujours volumineuses et de passer à la machine, car l'interlocuteur n'a pas mentionné qu'un autre programme écrit ses données (ce qui est une information importante). Encore si les données doivent être converties à partir d'un endian ntohs / ntohl / htons / htonl sont les meilleures méthodes, les plus élégantes et les plus rapides en termes de vitesse (car ils effectueront des tâches matérielles si le processeur le permet, vous ne pouvez pas battre cela.)

En ce qui concerne double / float, stockez-les simplement dans les ints en procédant à un transfert de mémoire:

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);

Envelopper dans une fonction

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

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

L’interlocuteur a fourni ce lien:

http: // cocoawithlove .com / 2008/04 / using-pointers-to-refast-in-c-is-bad.html

pour prouver que le casting est mauvais ... Malheureusement, je ne peux que fortement être en désaccord avec la plus grande partie de cette page. Citations et commentaires:

  

Aussi courant que de passer par un pointeur   c’est en fait une mauvaise pratique et   code potentiellement risqué. Moulage   à travers un pointeur a le potentiel de   créer des bugs à cause du type punning.

Ce n’est pas risqué et ce n’est pas non plus une mauvaise pratique. Si vous ne le faites pas correctement, cela ne risque de causer des bugs, tout comme la programmation en C risque de causer des bugs si vous le faites incorrectement, il en va de même pour toute programmation dans n'importe quel langage. Par cet argument, vous devez arrêter complètement de programmer.

  

Type punning
Une forme de pointeur   aliasing où deux pointeurs et se réfèrent   au même endroit en mémoire mais   représenter cet endroit comme différent   les types. Le compilateur traitera les deux   " jeux de mots " comme des pointeurs indépendants. Type   le jeu de mots a le potentiel de causer   problèmes de dépendance pour toutes les données   accessible via les deux pointeurs.

Cela est vrai, mais malheureusement, n'a aucun lien avec mon code .

Ce à quoi il fait référence, c'est un code comme celui-ci:

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

Maintenant, doublePointer et intPointer pointent tous deux vers le même emplacement mémoire, mais en le considérant comme du même type. C'est la situation que vous devriez résoudre avec un syndicat, en effet, tout le reste est très mauvais. Mauvais, ce n’est pas ce que mon code fait!

Mon code est copié selon la valeur , et non par la référence . Je lance un double pointeur sur int64 (ou l'inverse) et le déférence immédiatement . Une fois que les fonctions sont revenues, il n’ya plus de pointeur. Il existe un int64 et un double et ceux-ci sont totalement indépendants du paramètre d'entrée des fonctions. Je ne copie jamais aucun pointeur vers un pointeur d'un type différent (si vous voyez ceci dans mon exemple de code, vous avez fortement mal interprété le code C que j'ai écrit), je transfère simplement la valeur sur une variable de type différent (dans un emplacement de mémoire propre). . La définition du type punning ne s'applique donc pas du tout, car elle dit "se référer au même emplacement en mémoire". et rien ici ne fait référence au même emplacement mémoire.

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;

Mon code n’est rien d’autre qu’une copie en mémoire, mais une écriture en C sans fonction externe.

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

Pourrait être écrit comme

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

Ce n’est rien d’autre que ça, il n’ya donc pas de punaise, même en vue, nulle part. Et cette opération est également totalement sûre, aussi sûre qu’une opération puisse être en C. Un double est défini comme toujours à 64 bits (contrairement à int, sa taille ne varie pas, elle est fixée à 64 bits), elle tient donc toujours dans une variable de taille int64_t.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top