Existe-t-il un moyen standard ou une alternative standard de compresser une structure en c?

13

Lorsque la programmation dans CI a trouvé inestimable de regrouper des structures en utilisant l' __attribute__((__packed__))attribut GCCs , je peux donc facilement convertir un morceau structuré de mémoire volatile en un tableau d'octets à transmettre sur un bus, enregistré dans le stockage ou appliqué à un bloc de registres. Les structures empaquetées garantissent que lorsqu'elles sont traitées comme un tableau d'octets, elles ne contiennent aucun remplissage, ce qui est à la fois un gaspillage, un risque de sécurité possible et éventuellement incompatible avec le matériel d'interface.

N'y a-t-il pas de norme pour l'emballage des structures qui fonctionne dans tous les compilateurs C? Sinon, je suis une valeur aberrante en pensant que c'est une caractéristique critique pour la programmation des systèmes? Les premiers utilisateurs du langage C n'ont-ils pas trouvé le besoin de structurer les structures ou existe-t-il une sorte d'alternative?

satur9nine
la source
l'utilisation de structures sur des domaines de compilation est une très mauvaise idée, en particulier pour pointer du matériel (qui est un autre domaine de compilation). Les structures de paquets ne sont qu'une astuce pour le faire, elles ont beaucoup de mauvais effets secondaires, il existe donc de nombreuses autres solutions à vos problèmes avec moins d'effets secondaires, et qui sont plus portables.
old_timer

Réponses:

12

Dans une structure, ce qui compte, c'est le décalage de chaque membre par rapport à l'adresse de chaque instance de structure. Ce n'est pas tant la question de savoir comment les choses sont serrées.

Un tableau, cependant, compte dans la façon dont il est "emballé". La règle en C est que chaque élément du tableau est exactement N octets du précédent, où N est le nombre d'octets utilisés pour stocker ce type.

Mais avec une structure, il n'y a pas un tel besoin d'uniformité.

Voici un exemple d'un schéma d'emballage étrange:

Freescale (qui fabrique des microcontrôleurs automobiles) fabrique un micro doté d'un coprocesseur Time Processing Unit (google pour eTPU ou TPU). Il a deux tailles de données natives, 8 bits et 24 bits, et ne traite que des entiers.

Cette structure:

struct a
{
  U24 elementA;
  U24 elementB;
};

verra chaque U24 stocké son propre bloc 32 bits, mais uniquement dans la zone d'adresse la plus élevée.

Cette:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

aura deux U24s adjacentes stockées dans 32 blocs de bits, et le U8 sera stocké dans le « trou » en face de la première U24, elementA.

Mais vous pouvez dire au compilateur de tout emballer dans son propre bloc 32 bits, si vous le souhaitez; c'est plus cher sur la RAM mais utilise moins d'instructions pour les accès.

"emballage" ne signifie pas "emballer étroitement" - cela signifie simplement un schéma pour organiser les éléments d'une structure par rapport à l'offset.

Il n'y a pas de schéma générique, il dépend du compilateur + de l'architecture.

RichColours
la source
1
Si le compilateur pour le TPU se réorganise struct bpour se déplacer elementCavant l'un des autres éléments, alors ce n'est pas un compilateur C conforme. Le réarrangement des éléments n'est pas autorisé dans C
Bart van Ingen Schenau
Intéressant, mais U24 n'est pas un type C standard en.m.wikipedia.org/wiki/C_data_types , il n'est donc pas surprenant que le complice soit obligé de le gérer d'une manière quelque peu étrange.
satur9nine
Il partage la RAM avec le cœur du processeur principal qui a une taille de mot de 32 bits. Mais ce processeur a une ALU qui ne traite que 24 bits ou 8 bits. Il a donc un schéma pour disposer des nombres de 24 bits en mots de 32 bits. Non standard, mais un excellent exemple d'emballage et d'alignement. D'accord, c'est très non standard.
RichColours
6

Lorsque la programmation dans CI a trouvé inestimable de pack structs à l'aide de GCC __attribute__((__packed__))[...]

Puisque vous le mentionnez __attribute__((__packed__)), je suppose que votre intention est d'éliminer tout remplissage dans un struct(faire en sorte que chaque membre ait un alignement sur 1 octet).

N'y a-t-il pas de norme pour l'emballage des structures qui fonctionne dans tous les compilateurs C?

... Et la réponse est non". Le remplissage et l'alignement des données par rapport à une structure (et les tableaux contigus de structures en pile ou en tas) existent pour une raison importante. Sur de nombreuses machines, l'accès à la mémoire non aligné peut entraîner une pénalité de performance potentiellement importante (bien qu'elle devienne moindre sur certains matériels plus récents). Dans certains cas rares, un accès à la mémoire mal aligné entraîne une erreur de bus irrécupérable (peut même planter l'ensemble du système d'exploitation).

Étant donné que la norme C est axée sur la portabilité, il est peu logique de disposer d'un moyen standard pour éliminer tout le remplissage dans une structure et simplement permettre un désalignement des champs arbitraires, car cela risquerait potentiellement de rendre le code C non portable.

Le moyen le plus sûr et le plus portable de sortir ces données vers une source externe d'une manière qui élimine tout remplissage est de sérialiser vers / depuis les flux d'octets au lieu d'essayer simplement d'envoyer le contenu de la mémoire brute de votre structs. Cela empêche également votre programme de subir des pénalités de performances en dehors de ce contexte de sérialisation, et vous permettra également d'ajouter librement de nouveaux champs à un structsans supprimer et perturber le logiciel entier. Cela vous donnera également de la place pour lutter contre l'endianité et des choses comme ça si cela devient un problème.

Il existe un moyen d'éliminer tout remplissage sans atteindre les directives spécifiques au compilateur, bien qu'il ne soit applicable que si l'ordre relatif entre les champs n'a pas d'importance. Étant donné quelque chose comme ça:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

... nous avons besoin du remplissage pour un accès mémoire aligné par rapport à l'adresse de la structure contenant ces champs, comme ceci:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

... où .indique un rembourrage. Chacun xdoit s'aligner sur une limite de 8 octets pour les performances (et parfois même un comportement correct).

Vous pouvez éliminer le remplissage de manière portable en utilisant une représentation SoA (structure de tableau) comme ceci (supposons que nous ayons besoin de 8 Fooinstances):

struct Foos
{
   double x[8];
   char y[8];
};

Nous avons effectivement démoli la structure. Dans ce cas, la représentation de la mémoire devient comme ceci:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... et ça:

01234567
yyyyyyyy

... plus de surcharge de remplissage, et sans impliquer un accès à la mémoire mal aligné puisque nous n'accédons plus à ces champs de données comme décalage d'une adresse de structure, mais plutôt comme décalage d'une adresse de base pour ce qui est effectivement un tableau.

Cela présente également l'avantage d'être plus rapide pour l'accès séquentiel en raison de la réduction de la consommation de données (plus de remplissage non pertinent dans le mélange pour ralentir le taux de consommation de données pertinent de la machine) et également du potentiel pour le compilateur de vectoriser le traitement de manière très triviale. .

L'inconvénient est que c'est un PITA à coder. Il est également potentiellement moins efficace pour l'accès aléatoire avec une plus grande foulée entre les champs, où souvent les représentants AoS ou AoSoA feront mieux. Mais c'est une façon standard d'éliminer le rembourrage et d'emballer les choses aussi étroitement que possible sans visser avec l'alignement de tout.

ChrisF
la source
2
Je dirais qu'avoir un moyen de spécifier explicitement la disposition de la structure améliorerait considérablement la portabilité. Alors que certaines dispositions conduiraient à un code très efficace sur certaines machines et à un code très inefficace sur d'autres, le code fonctionnerait sur toutes les machines et serait efficace sur au moins certaines. En revanche, en l'absence d'une telle fonctionnalité, la seule façon de faire fonctionner le code sur toutes les machines est probablement de le rendre inefficace sur toutes les machines ou bien d'utiliser un tas de macros et une compilation conditionnelle pour combiner un non-portable rapide programme et un portable lent dans la même source.
supercat
Conceptuellement oui, si nous pouvions tout spécifier jusqu'aux représentations en bits et octets, les exigences d'alignement, l'endianité, etc. et avoir une fonctionnalité qui permet un tel contrôle explicite en C tout en le séparant éventuellement de l'architecture sous-jacente ... Mais je parlais juste de ATM - actuellement la solution la plus portable pour un sérialiseur est de l'écrire de manière à ce qu'il ne dépende pas des représentations exactes en bits et octets et de l'alignement des types de données. Malheureusement, nous n'avons pas les moyens ATM de faire autrement efficacement (en C).
5

Toutes les architectures ne sont pas identiques, activez simplement l'option 32 bits sur un module et voyez ce qui se passe lorsque vous utilisez le même code source et le même compilateur. L'ordre des octets est une autre limitation bien connue. Ajoutez une représentation en virgule flottante et les problèmes s'aggravent. L'utilisation de Packing pour envoyer des données binaires n'est pas portable. Pour le normaliser afin qu'il soit pratiquement utilisable, vous devrez redéfinir la spécification du langage C.

Bien que courant, utiliser Pack pour envoyer des données binaires est une mauvaise idée si vous voulez la sécurité, la portabilité ou la longévité des données. À quelle fréquence lisez-vous un blob binaire à partir d'une source dans votre programme. À quelle fréquence vérifiez-vous que toutes les valeurs sont valables, qu'un pirate ou un changement de programme n'a pas «obtenu» les données? Au moment où vous avez codé une routine de vérification, vous pourriez aussi bien utiliser des routines d'importation et d'exportation.

mattnz
la source
0

Une alternative très courante est le "padding nommé":

struct s {
  short s1;
  char  c2;
  char  reserved; // Padding
};

Cela ne suppose la structure ne sera pas rembourré à 8 octets.

MSalters
la source