Forum Programmation.c Les espacements que mettent les compilateurs C dans les structures sont ils toujours les mêmes ?

Posté par  . Licence CC By‑SA.
Étiquettes :
3
17
juin
2014

Bonjour,

Je suis entrain d'implémenter une communication entre deux programmes sur un réseau. La communication utilise un protocole au format binaire. Je suis entrain de me poser quelques questions sur l'alignement des structure et surtout l'espacement que mettent les compilateurs pour respecter l'alignement. Voici un exemple de structure :

struct hello {
    uint8_t  version;
    uint16_t id;
    uint32_t name;
};

Les membres ont été volontairement arrangé pour que le compilateur ajoute des espacements. Si j'ai bien compris comment le compilateur est sensé modifier la structure, voici ce que cela donnerait :

struct hello {
    uint8_t  version;

    /* L'élément suivant fait deux octets, il faut donc l'aligner sur une adresse
     * multiple de deux. */
    char pad1[1];

    uint16_t id;

    /* L'élément suivant fait quatre octets, il faut donc l'aligner sur une adresse
     * multiple de quatre. */
    char pad2[2];

    uint32_t name;
};

Du coup, je me demande, est-ce-que je peux être sur que se sera toujours les mêmes espacements qui seront placés ? Sur n'importe-quelle architecture, avec n'importe-quel compilateur ? Si oui, je peux donc envoyer directement la structure sur le réseau, après avoir pris soin de modifier le boutisme ?

J'en profite pour poser une autre question : l'alignement est-il un problème unique aux développeurs noyaux/hardware, ou il concerne tout type de programmes C ?

Merci pour vos réponse :)

  • # Normalement tu n'as aucune garantie

    Posté par  (site web personnel) . Évalué à 3.

    Le compilateur va faire un alignement mémoire de tes éléments de structure pour coller avec l'adressage de ton processeur. Donc de fait, selon la capacité d'adressage du processeur considéré l'alignement mémoire ne sera pas identique. Comme tu sembles te préoccuper du boutisme, c'est bien que tu comptes dialoguer avec des processeurs potentiellement non X86.

    Note que sur le réseau, le boutisme standard est le grand boutisme. Ce qui est contraire à l'architecture dominant de nos ordinateurs de bureau.

    Après je ne me souviens plus de la norme exacte concernant le soin laissé aux compilateurs concernant les bits de bourrage, de mémoire tu ne peux faire une généralité sur le comportement des compilateurs à ce sujet surtout sur des architectures différentes.

    L'alignement est un problème que tu peux rencontrer dans tout programme C potentiellement, mais il est vrai que généralement tu ne t'en préoccupes pas surtout si tu enregistres les valeurs champ par champ (méthode conseillée pour éviter les problèmes avec les structures).

    • [^] # Re: Normalement tu n'as aucune garantie

      Posté par  . Évalué à 1.

      Merci bien :)

      Une dernière petite question: est-ce-que travailler avec des uint*_t en mémoire plutôt que les classiques int, long est moins performant ?

      J'imagine que non, mais bon, je ne connais pas encore tout les détails de ce genre de choses :)

      • [^] # Re: Normalement tu n'as aucune garantie

        Posté par  . Évalué à 2.

        Une dernière petite question: est-ce-que travailler avec des uint*_t en mémoire plutôt que les classiques int, long est moins performant ?

        Oui et non. Déjà, parce que performance, ça veut rien dire: performance en calcul, ou en place?
        Aussi parce que sur les plates-formes ou uint32_t ( par exemple ) est un define du int, non, par exemple, mais ça peut varier.

        Si la performance à ce niveau t'intéresse tant, je te conseille une petite lecture du fichier stdint.h. Plus précisément:

        /* Small types.  */
        
        /* Signed.  */
        typedef signed char   int_least8_t;
        typedef short int   int_least16_t;
        typedef int     int_least32_t;
        #if __WORDSIZE == 64
        typedef long int    int_least64_t;
        #else
        __extension__
        typedef long long int   int_least64_t;
        #endif
        
        /* Unsigned.  */
        typedef unsigned char   uint_least8_t;
        typedef unsigned short int  uint_least16_t;
        typedef unsigned int    uint_least32_t;
        #if __WORDSIZE == 64
        typedef unsigned long int uint_least64_t;
        #else
        __extension__
        typedef unsigned long long int  uint_least64_t;
        #endif
        
        /* Fast types.  */
        
        /* Signed.  */
        typedef signed char   int_fast8_t;
        #if __WORDSIZE == 64
        typedef long int    int_fast16_t;
        typedef long int    int_fast32_t;
        typedef long int    int_fast64_t;
        #else
        typedef int     int_fast16_t;
        typedef int     int_fast32_t;
        __extension__
        typedef long long int   int_fast64_t;
        #endif
        
        /* Unsigned.  */
        typedef unsigned char   uint_fast8_t;
        #if __WORDSIZE == 64
        typedef unsigned long int uint_fast16_t;
        typedef unsigned long int uint_fast32_t;
        typedef unsigned long int uint_fast64_t;
        #else
        typedef unsigned int    uint_fast16_t;
        typedef unsigned int    uint_fast32_t;
        __extension__
        typedef unsigned long long int  uint_fast64_t;
        #endif

        Donc, voila: en fonction de tes besoins, tu as des types différents.

        Sinon, j'ai noté l'utilisation d'un char, au milieu de types stdint, pourquoi ne pas avoir utilisé uint8_t? Ça me semblerait plus cohérent.

        Enfin, si tu ne veux pas utiliser packed mais que tu veux contrôler l'alignement, il existe de mémoire une syntaxe pour les bitfields en C. Je ne m'en souviens plus, c'est pas le genre de trucs que j'utilise souvent, mais c'est simple à retrouver.

        Bon, je mentionne surtout ça parce que tu risques d'y être confronté, et que c'est parfois intéressant si tu sais que tel variable est plus petite qu'un octet et que tu cherches une performance de taille maximale sans avoir à compresser par algo. Dans ton usage précis par contre, je ne pense pas que ce soit une solution acceptable.

  • # packed

    Posté par  . Évalué à 5.

    Je suis entrain de me poser quelques questions sur l'alignement des structure

    Comme dit précédemment, il n'y a par défaut aucune garantie.

    Par contre, tu peux utiliser le mot-clé packed pour que le compilateur ne rajoute pas de 'blanc' entre les membres de ta structure.

    Et bien sûr, les dispositions habituelles grand-boutisme / petit-boutisme. ;-)

    Hop,
    Moi.

    • [^] # Re: packed

      Posté par  . Évalué à 0.

      Pour packed je suis pas trop pour, c'est pas très portable et cela pose des problèmes de performances non ? Je préfère avoir une structure non 'packée' sur laquelle je peux travailler et 'packer' manuellement au moment de l'envoi sur le réseau.

  • # Abstraction

    Posté par  (site web personnel) . Évalué à 1.

    Le padding peut effectivement varier d'une archi à l'autre, d'un compilo à l'autre. Cumulé avec toutes les autres petites variations subtiles (e.g. boutisme, …), c'est l'une des raisons pour lesquelles il y a eu l'émergence de technos genre ASN.1 pour décrire de manière plus formelle et indépendante de la plateforme des structures de données complexes.

    • [^] # Re: Abstraction

      Posté par  (site web personnel) . Évalué à 2.

      Ah, et pour :

      J'en profite pour poser une autre question : l'alignement est-il un problème unique aux développeurs noyaux/hardware, ou il concerne tout type de programmes C ?

      C'est utile dès que ton application "exporte" une structure qui sera utilisée ailleurs : dans la majorité des cas ce sera donc quand tu veux communiquer la structure via un réseau ou via un fichier, sans pouvoir présumer de la machine/os/application à la réception. Ce n'est donc pas réservé au noyau.

      Comme mentionné dans d'autres commentaires, on peut également s'y intéresser pour des problématiques de perf, pour des parties critiques de code (notamment, mais pas seulement, dans le noyau).

  • # Non garantie du compilateur

    Posté par  . Évalué à -1.

    Attention à plusieurs choses:

    1- selon votre machine, surtout si vous avez des machines <32bits (même rarement ces temps ci) vous pouvez aussi jouer avec les mots clés du compilo (si dispo) du genre aligned ou packed (voir https://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html)

    2- il se peut aussi que le compilateur casse l'ordre des champ de la structure pour réorganiser par taille. Si vous voulez maitriser votre structure, il est de bon usage de toujours spécifier vos champs par ordre décroissant de taille.

    3- par expérience, sauf besoin spécifiques, il faut se passer un maximum des types uintXX_t. n'utiliser que des types primitifs (char, int, long, …) suffit largement et permet surtout de s'affranchir de la machine cible. Par contre, si l'on doit faire communiquer 2 machines différents (ex 32 et 64 bits) les typer uintXX_t sont pratique et ne sont que des macros sur les types primitifs de la machines (char, int, long, …)

    4- pour maitriser un padding implicite du compilo on peut aussi faire un padding explicite dans la structure que nous voulons.

    • [^] # Commentaire supprimé

      Posté par  . Évalué à 2.

      Ce commentaire a été supprimé par l’équipe de modération.

    • [^] # Re: Non garantie du compilateur

      Posté par  . Évalué à 1.

      3- par expérience, sauf besoin spécifiques, il faut se passer un maximum des types uintXX_t.

      Je serais intéressé de savoir quels problèmes tu as eu avec ces types? Mis à part qu'ils peuvent ne pas être définis sur une archi ( contrairement aux types least et fast ), je ne vois pas de problème particulier?

      En fait, j'ai l'impression que c'est même le contraire, dans un code:

      • écrit par des gens ne connaissant pas stdint et donc les macros maxi/mini pour les int/char/short/long : on ne m'a jamais parlé de stdint.h à l'école, c'était pourtant en 2006, ça existait… à la place de ça, les profs nous disaient qu'un int c'est toujours 32 bits, la même taille qu'un long ( Chance pour moi: j'avais appris en autodidacte sur du C 16bits, avec un compilo différent, et un peu pratiqué du 32bits avec ce même compilo, du coups j'ai tilté qu'il est plus sûr d'utiliser short et long plutôt qu'int, moins de variations sur les plate-formes que je connaissait à l'époque ). Et là, je parle d'un étudiant. Un vieux roublard des intel x86 aura lui, potentiellement acquis des habitudes pas clean, et aurait pu utiliser les mêmes trucs crades de comparer à des constantes numériques faites main. Par exemple, je ne serais pas surpris de voir ce type de choses dans le moteur de certains vieux quake ou de duke nukem.
      • écrit avant 1999: stdint.h, c'est le standard C99
      • écrit pour être compatible avec du C++ pré 2011: stdint.h n'est standard que depuis C++11

      voir des constantes numériques faites main pour tester la valeur maxi lors d'une itération n'est pas rare. Et bien sûr, la portabilité prend un sacré coup dans la gueule ( voire même la stabilité s'il s'est planté, mais bon… ). Alors que des types XintYY_t, quitte à ce qu'ils soient redéfinis ( comme le fait la SDL 1.2, par exemple ) peuvent être comparés au premier coup d'œil à la valeur testée: 0xFFFF et 0x7FFF pour les signés, -1 pour les non signés ( ça au moins ça marche toujours… je crois? ).

      Ce que je veux dire, c'est qu'au moins, avec un uint16_t les valeurs mini/maxi/maxi non signé numériques sont fiables ( même si je suis d'accord qu'il faut éviter, mais les codes sources n'ont pas tous la possibilité d'être compilé avec des compilos respectant C99 ( 15 ans c'est jeune comparé au langage C ).

  • # Erreur d'analyse

    Posté par  (site web personnel) . Évalué à 2.

    Au vu de ta structure, la représentation mémoire ne nécessite qu'un padding d'un octet, en effet le padding de 8bits pour id suffit à aligner name sur un 32bits:

    0    8   16        32                  64
    +----+----+----+----+----+----+----+----+
    | ver|XXXX| id      | name              |
    +----+----+----+----+----+----+----+----+
    

    Pour savoir ce que le compilo (gcc en tout cas) fait: -Wpadded est utile

  • # Utiliser un standard / de l'existant

    Posté par  (site web personnel) . Évalué à 3.

    Salut, tu va réinventer ce qui a déjà été fait par d'autres qui ont traité les différents cas en ayant une base d'architectures / compilos (et donc problèmes potentiels) bien plus large que toi.
    Sans aller jusqu'à CORBA et la définition d'interfaces IDL, tu pourrais regarder la définition de messages ONC RPC, ou plus récent le protobuf de Google.

    A+, bons développements.

    Python 3 - Apprendre à programmer dans l'écosystème Python → https://www.dunod.com/EAN/9782100809141

  • # le mur

    Posté par  . Évalué à 3.

    je te suspecte de vouloir sérialiser des struct à la main.
    ça va faire mal :)
    je rejoints le commentaire de lolop : ne réinvente pas le roue, utilise protobuf ou autre.

    • [^] # Re: le mur

      Posté par  . Évalué à 2.

      Haha, j'utilise le préprocesseur pour générer le code qui sérialise les structures. Un genre de X macros mais poussé un peu plus loin que la simple génération d'énumération.

      J'ai un fichier qui contient ceci:

      BEGIN(hello)
          FIELD_8(version)
          FIELD_16(id)
          FIELD_32(name)
      END()

      Et par exemple je génère la structure comme ceci:

      #define BEGIN(name)                                                     \
          struct name {
      #define FIELD_8(name)                                                   \
              uint8_t name;
      #define FIELD_16(name)                                                  \
              uint16_t name;
      #define FIELD_32(name)                                                  \
              uint32_t name;
      #define END()                                                           \
          };
      #include "fichier_qui_contient_les_definitions.def.h"
      #undef  BEGIN
      #undef  FIELD_8
      #undef  FIELD_16
      #undef  FIELD_32
      #undef  END

      J'utilise ce trick à mainte reprise pour générer les fonctions qui sérialisent les structures, etc… Bon la je le montre de façon simple, mais l'ensemble est beaucoup plus complet et permet de sérialiser plein de choses :)

      • [^] # Re: le mur

        Posté par  (site web personnel) . Évalué à 3.

        Et tu as essayé ça sur combien de compilateurs, avec des petits et gros boutiens, avec des archis 16, 32, 64 bits, et chacun communique avec les autres sans problème ?

        Python 3 - Apprendre à programmer dans l'écosystème Python → https://www.dunod.com/EAN/9782100809141

        • [^] # Re: le mur

          Posté par  . Évalué à 1.

          Pour l'instant uniquement 32 bit 64 bit x86 avec clang ou gcc. Et tous communiquent sans problèmes.

          Mais je me fait trop de soucis, les seules fonctions que j'utilise sont celles de la famille ntohl() ! Bon du coups il faudrait que je voie avec une architecture dont le boutisme est différent de celui du x86. Et cela sera fait, et si problème il y a, il sera rapidement repéré car j'ai pas mal de tests qui concernent uniquement le protocole et les communications sur le réseau :)

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.