J'ai fait quelques recherches sur Google et je n'ai trouvé aucun bon article sur cette question. À quoi dois-je faire attention lors de la mise en œuvre d'une application que je veux être agnostique endian?
Cela pourrait être un bon article à lire: L'erreur de l'ordre des octets
L'ordre des octets de l'ordinateur n'a pas beaucoup d'importance, sauf pour les rédacteurs de compilateurs et autres, qui s'inquiètent de l'allocation d'octets de mémoire mappés pour enregistrer des morceaux. Les chances sont que vous n'êtes pas un écrivain de compilateur, donc l'ordre des octets de l'ordinateur ne devrait pas vous importer un peu.
Remarquez l'expression "ordre des octets de l'ordinateur". Ce qui importe, c'est l'ordre des octets d'un flux de données périphérique ou codé, mais - et c'est le point clé - l'ordre des octets de l'ordinateur effectuant le traitement n'est pas pertinent pour le traitement des données lui-même. Si le flux de données encode les valeurs avec l'ordre des octets B, l'algorithme pour décoder la valeur sur l'ordinateur avec l'ordre des octets C devrait concerner B, pas la relation entre B et C.
La seule fois où vous devez vous soucier de l'endianité, c'est lorsque vous transférez des données binaires sensibles à l'endian (c'est-à-dire pas du texte) entre des systèmes qui pourraient ne pas avoir la même endianité. La solution normale consiste à utiliser " ordre des octets résea " (AKA big-endian) pour transférer les données, puis à parcourir les octets si nécessaire à l'autre extrémité.
Pour convertir de l'hôte en ordre d'octets réseau, utilisez htons(3)
et htonl(3)
. Pour reconvertir, utilisez ntohl(3)
et ntohs(3)
. Consultez la page de manuel pour tout ce que vous devez savoir. Pour les données 64 bits, cette question et réponse sera utile.
À quoi dois-je faire attention lors de la mise en œuvre d'une application que je veux être agnostique endian?
Vous devez d'abord reconnaître quand l'endian devient un problème. Et cela devient surtout un problème lorsque vous devez lire ou écrire des données à partir d'un endroit externe, que ce soit la lecture de données à partir d'un fichier ou la communication réseau entre ordinateurs.
Dans de tels cas, l'endianisme est important pour les entiers supérieurs à un octet, car les entiers sont représentés différemment en mémoire par différentes plateformes. Cela signifie que chaque fois que vous devez lire ou écrire des données externes, vous devez faire plus que simplement vider la mémoire de votre programme ou lire des données directement dans vos propres variables.
par exemple. si vous avez cet extrait de code:
unsigned int var = ...;
write(fd, &var, sizeof var);
Vous écrivez directement le contenu de la mémoire de var
, ce qui signifie que les données sont présentées partout où ces données vont exactement comme elles sont représentées dans la mémoire de votre ordinateur.
Si vous écrivez ces données dans un fichier, le contenu du fichier sera différent que vous exécutiez le programme sur une grande machine endian ou une petite machine endian. Donc, ce code n'est pas agnostique endien, et vous voudriez éviter de faire des choses comme ça.
Concentrez-vous plutôt sur le format des données. Lors de la lecture/écriture de données, décidez toujours d'abord du format des données, puis écrivez le code pour le gérer. Cela a peut-être déjà été décidé pour vous si vous avez besoin de lire un format de fichier bien défini existant ou de mettre en œuvre un protocole réseau existant.
Une fois que vous connaissez le format des données, au lieu de par ex. en vidant directement une variable int, votre code fait ceci:
uint32_t i = ...;
uint8_t buf[4];
buf[0] = (i&0xff000000) >> 24;
buf[1] = (i&0x00ff0000) >> 16;
buf[2] = (i&0x0000ff00) >> 8;
buf[3] = (i&0x000000ff);
write(fd, buf, sizeof buf);
Nous avons maintenant choisi l'octet le plus significatif et l'avons placé comme premier octet dans un tampon, et l'octet le moins significatif placé à la fin du tampon. Cet entier est représenté en grand format endian dans buf
, quel que soit l'endian de l'hôte - donc ce code est agnostique endian.
Le consommateur de ces données doit savoir que les données sont représentées dans un grand format endian. Et quel que soit l'hôte sur lequel le programme s'exécute, ce code lirait très bien ces données:
uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i = (uint32_t)buf[0] << 24;
i |= (uint32_t)buf[1] << 16;
i |= (uint32_t)buf[2] << 8;
i |= (uint32_t)buf[3];
Inversement, si les données que vous devez lire sont connues pour être en petit format endian, le code agnostique endianess ferait juste
uint32_t i ;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i = (uint32_t)buf[3] << 24;
i |= (uint32_t)buf[2] << 16;
i |= (uint32_t)buf[1] << 8;
i |= (uint32_t)buf[0];
Vous pouvez créer de belles fonctions ou macros en ligne pour encapsuler et décompresser tous les types entiers de 2,4,8 octets dont vous avez besoin, et si vous les utilisez et que vous vous souciez du format des données et non de l'endian du processeur sur lequel vous exécutez, votre code ne dépend pas de l'endianisme sur lequel il fonctionne.
C'est plus de code que de nombreuses autres solutions, je n'ai pas encore écrit de programme où ce travail supplémentaire a eu un impact significatif sur les performances, même lors du brassage de 1 Gbps + de données.
Il évite également un accès à la mémoire mal aligné que vous pouvez facilement obtenir avec une approche de, par exemple.
uint32_t i;
uint8_t buf[4];
read(fd, buf, sizeof buf);
i = ntohl(*(uint32_t)buf));
ce qui peut également entraîner une baisse des performances (insignifiante pour certains, beaucoup d'ordres de grandeur sur d'autres) et, au pire, un plantage sur les plates-formes qui ne peuvent pas accéder de manière non alignée aux nombres entiers.
Plusieurs réponses ont couvert le fichier IO, ce qui est certainement la préoccupation endienne la plus courante. Je vais aborder un sujet non encore mentionné: nions.
L'union suivante est un outil courant dans la programmation SIMD/SSE et n'est pas pas compatible avec les endians:
union uint128_t {
_m128i dq;
uint64_t dd[2];
uint32_t dw[4];
uint16_t dh[8];
uint8_t db[16];
};
Tout code accédant aux formulaires dd/dw/dh/db le fera de manière spécifique à l'endian. Sur les processeurs 32 bits, il est également assez courant de voir des unions plus simples qui permettent de diviser plus facilement l'arithmétique 64 bits en portions 32 bits:
union u64_parts {
uint64_t dd;
uint32_t dw[2];
};
Étant donné que dans ce cas d'utilisation, il est rare (voire jamais) que vous souhaitiez parcourir chaque élément de l'union, je préfère écrire de telles unions comme ceci:
union u64_parts {
uint64_t dd;
struct {
#ifdef BIG_ENDIAN
uint32_t dw2, dw1;
#else
uint32_t dw1, dw2;
#endif
}
};
Le résultat est un échange endian implicite pour tout code accédant directement à dw1/dw2. La même approche de conception peut également être utilisée pour le type de données SIMD 128 bits ci-dessus, bien qu'elle finisse par être beaucoup plus verbeuse.
Avertissement: L'utilisation de l'Union est souvent mal vue en raison des définitions de normes vagues concernant le rembourrage et l'alignement de la structure. Je trouve les syndicats très utiles et les ai largement utilisés, et je n'ai rencontré aucun problème de compatibilité croisée depuis très longtemps (15 ans et plus). Le remplissage/alignement des unions se comportera de manière attendue et cohérente pour tout compilateur actuel ciblant x86, ARM ou PowerPC.
Dans votre code, vous pouvez à peu près l'ignorer - tout s'annule.
Lorsque vous lisez/écrivez des données sur le disque ou que le réseau utilise htons
Il s'agit clairement d'un sujet plutôt controversé.
L'approche générale consiste à concevoir votre application de telle sorte que vous vous souciez uniquement de l'ordre des octets dans une petite portion: les sections d'entrée et de sortie du code.
Partout ailleurs, vous devez utiliser l'ordre d'octets natif.
Notez que bien que la plupart des machines le fassent de la même manière, il n'est pas garanti que les données à virgule flottante et entière soient stockées de la même manière, donc pour être complètement sûr que les choses fonctionnent bien, vous devez connaître non seulement la taille, mais aussi si c'est le cas. entier ou virgule flottante.
L'autre alternative consiste à ne consommer et produire que des données au format texte. C'est probablement presque aussi facile à implémenter, et à moins que vous n'ayez un taux de données très élevé dans/hors de l'application avec très peu de traitement, c'est probablement très peu de différence de performance. Et avec l'avantage (pour certains) que vous pouvez lire les données d'entrée et de sortie dans un éditeur de texte, plutôt que d'essayer de décoder la valeur réelle des octets 51213498-51213501 dans la sortie, lorsque vous avez quelque chose de mal dans le code.
Si vous devez réinterpréter entre un type entier de 2,4 ou 8 octets et un tableau indexé sur les octets (ou vice versa), vous devez connaître l'endianité.
Cela revient fréquemment dans la mise en œuvre d'algorithmes cryptographiques, les applications de sérialisation (comme le protocole réseau, les systèmes de fichiers ou les bases de données), et bien sûr les noyaux et les pilotes du système d'exploitation.
Il est généralement détecté par une macro comme ENDIAN ... quelque chose.
Par exemple:
uint32 x = ...;
uint8* p = (uint8*) &x;
p pointe vers l'octet haut sur les machines BE et l'octet bas sur la machine LE.
En utilisant les macros, vous pouvez écrire:
uint32 x = ...;
#ifdef LITTLE_ENDIAN
uint8* p = (uint8*) &x + 3;
#else // BIG_ENDIAN
uint8* p = (uint8*) &x;
#endif
pour toujours obtenir l'octet de poids fort par exemple.
Il existe des façons de définir la macro ici: Définition de la macro C pour déterminer la grande machine endian ou little endian? si votre chaîne d'outils ne les fournit pas.