Puning en toute sécurité car * doubler en C

Dans un programme Open Source que j’ai écrit , je lis des données binarys (écrites par un autre programme) à partir d’un fichier et génère des données ints, doubles et autres types de données assorties. L’un des défis est qu’il doit fonctionner sur des machines 32 bits et 64 bits des deux finales, ce qui signifie que je finis par devoir faire un peu de bricolage à bas niveau. Je connais (très) un peu le typage punning et l’aliasing ssortingct et je veux m’assurer que je fais les choses correctement.

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:

 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 affecte l’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. Je voudrais réutiliser le code endian-swapping comme ceci:

 union { double d; int64_t i; } int64todouble; int64todouble.i = slittleint64_t(bufoffset); printf("%lf", int64todouble.d); 

Cependant, certains compilateurs pourraient optimiser l’affectation “int64todouble.i” et interrompre le programme. Existe-t-il un moyen plus sûr de procéder, tout en considérant que ce programme doit restr optimisé pour les performances et que je préférerais ne pas écrire un ensemble de transformations parallèles pour transformer le caractère * en double directement? Si la méthode d’union de punition est sûre, devrais-je 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 ceci:

 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 sur les compilateurs les plus naïfs.

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

É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 de perdre votre mémoire. Dans ce cas, vous n’avez même pas à penser au repliement.

Puisque vous utilisez un pointeur de fonction pour une fonction qui pourrait facilement être intégrée si vous êtes prêt à publier plusieurs fichiers binarys, les performances ne doivent pas être un problème majeur, mais vous voudrez peut-être savoir que certains compilateurs peuvent être assez diaboliques pour optimiser la mémoire – Pour les petites tailles entières, un ensemble de charges et de magasins peut être aligné, et vous pouvez même trouver que les variables sont totalement optimisées et que le compilateur effectue la “copie” en réaffectant simplement les emplacements de stack qu’il utilise 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 juste 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 firebase database 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], nécessitant un échange d’octets uniquement si les formats de fichier diffèrent.

Je vous suggère fortement de lire Understanding Ssortingct Aliasing . Plus précisément, voir les 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 parameters.

En tant que très petite sous-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 toujours pouvoir vous en sortir avec un masque de 0xff seulement. 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 concernant 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 (qui sont des informations importantes).

Néanmoins, si les données doivent être converties de tout type d’endian à grand et de grand à hôte hôte, 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 ne peut pas battre ça).


En ce qui concerne double / float, stockez-les simplement dans les mémoires de conversion:

 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’interrogateur a fourni ce lien:

http://cocoawithlove.com/2008/04/using-pointers-to-recast-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 répandu que soit le passage d’un pointeur à un autre, il s’agit d’une mauvaise pratique et d’un code potentiellement risqué. L’utilisation d’un pointeur peut créer des bogues à cause du type punning.

Ce n’est pas risqué du tout et ce n’est pas non plus une mauvaise pratique. Si vous ne le faites pas correctement, cela risque de causer des bugs, tout comme la programmation en C peut potentiellement 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.

Dactylographie
Une forme d’alias de pointeur où deux pointeurs et se réfèrent au même emplacement en mémoire mais représentent cet emplacement en tant que types différents. Le compilateur traitera les deux “jeux de mots” comme des pointeurs indépendants. La frappe de type peut entraîner des problèmes de dépendance pour toutes les données accessibles via les deux pointeurs.

Ceci est vrai, mais malheureusement totalement indépendant de mon code .

Ce à quoi il fait référence 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. Telle est la situation que vous devriez résoudre avec un syndicat, tout le rest est plutôt mauvais. Bad ce n’est pas ce que mon code fait!

Mon code copie par valeur , pas par référence . Je jette un double pointeur sur int64 (ou l’inverse) et le déférence immédiatement . Une fois que les fonctions sont revenues, il n’y a 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 vers une variable de type différent (dans un emplacement de mémoire propre). . Ainsi, la définition du type punning ne s’applique pas du tout, car il est dit “se réfère au même emplacement en mémoire” et rien ici ne fait référence au même emplacement de 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 de plus qu’une copie en mémoire, simplement écrite 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 de plus que cela, donc il n’y a 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.