Une opération au niveau du bit entraîne une taille variable inattendue

24

Le contexte

Nous portons du code C qui a été initialement compilé à l'aide d'un compilateur C 8 bits pour le microcontrôleur PIC. Un idiome commun utilisé pour empêcher les variables globales non signées (par exemple, les compteurs d'erreurs) de revenir à zéro est le suivant:

if(~counter) counter++;

L'opérateur au niveau du bit inverse ici tous les bits et l'instruction n'est vraie que si elle counterest inférieure à la valeur maximale. Surtout, cela fonctionne indépendamment de la taille variable.

Problème

Nous visons maintenant un processeur ARM 32 bits utilisant GCC. Nous avons remarqué que le même code produit des résultats différents. Pour autant que nous puissions en juger, il semble que l'opération de complément au niveau du bit renvoie une valeur d'une taille différente de celle à laquelle nous nous attendions. Pour reproduire cela, nous compilons, dans GCC:

uint8_t i = 0;
int sz;

sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1

sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4

Dans la première ligne de sortie, nous obtenons ce que nous attendons: iest de 1 octet. Cependant, le complément au niveau du bit de iest en fait de quatre octets, ce qui cause un problème car les comparaisons avec celui-ci ne donneront pas les résultats attendus. Par exemple, si vous faites (où iest correctement initialisé uint8_t):

if(~i) i++;

Nous verrons i"boucler" de 0xFF à 0x00. Ce comportement est différent dans GCC par rapport à quand il fonctionnait comme nous le voulions dans le compilateur précédent et le microcontrôleur PIC 8 bits.

Nous sommes conscients que nous pouvons résoudre ce problème en effectuant un casting comme ceci:

if((uint8_t)~i) i++;

Ou, par

if(i < 0xFF) i++;

Toutefois, dans ces deux solutions de contournement, la taille de la variable doit être connue et est sujette à erreur pour le développeur de logiciels. Ces types de vérification des limites supérieures se produisent dans toute la base de code. Il y a plusieurs tailles de variables (par exemple, uint16_tet unsigned charetc.) et les changer dans une base de code qui fonctionne autrement n'est pas quelque chose que nous attendons avec impatience.

Question

Notre compréhension du problème est-elle correcte et existe-t-il des options pour résoudre ce problème qui ne nécessitent pas de revoir chaque cas où nous avons utilisé cet idiome? Notre hypothèse est-elle correcte, qu'une opération comme le complément au niveau du bit devrait retourner un résultat de la même taille que l'opérande? Il semble que cela se casserait, selon les architectures du processeur. J'ai l'impression de prendre des pilules folles et que C devrait être un peu plus portable que ça. Encore une fois, notre compréhension de cela pourrait être fausse.

En apparence, cela ne semble pas être un problème énorme, mais cet idiome fonctionnant précédemment est utilisé dans des centaines d'endroits et nous sommes impatients de le comprendre avant de procéder à des changements coûteux.


Remarque: Il y a une question en double apparemment similaire mais pas exacte ici: l' opération au niveau du bit sur char donne un résultat 32 bits

Je n'ai pas vu le véritable nœud du problème discuté ici, à savoir, la taille du résultat d'un complément au niveau du bit étant différente de celle transmise à l'opérateur.

Charlie Salts
la source
14
"Notre hypothèse est-elle correcte, qu'une opération comme le complément au niveau du bit devrait retourner un résultat de la même taille que l'opérande?" Non, ce n'est pas correct, des promotions entières s'appliquent.
Thomas Jager
2
Bien que certainement pertinents, je ne suis pas convaincu que ce sont des doublons de cette question particulière, car ils ne fournissent pas de solution au problème.
Cody Gray
3
J'ai l'impression de prendre des pilules folles et que le C devrait être un peu plus portable que ça. Si vous n'avez pas obtenu de promotions entières sur les types 8 bits, alors votre compilateur n'était pas compatible avec la norme C. Dans ce cas, je pense que vous devriez parcourir tous les calculs pour les vérifier et les corriger si nécessaire.
user694733
1
Suis-je le seul à me demander quelle logique, en dehors des compteurs vraiment sans importance, peut la porter à "incrémenter s'il y a suffisamment d'espace, sinon l'oublier"? Si vous portez du code, pouvez-vous utiliser int (4 octets) au lieu de uint_8? Cela éviterait votre problème dans de nombreux cas.
rondelle le
1
@puck Vous avez raison, nous pourrions le changer en 4 octets, mais cela romprait la compatibilité lors de la communication avec les systèmes existants. Le but est de savoir quand il y a des erreurs et donc un compteur à 1 octet était à l'origine suffisant et le reste.
Charlie Salts

Réponses:

26

Ce que vous voyez est le résultat de promotions entières . Dans la plupart des cas où une valeur entière est utilisée dans une expression, si le type de la valeur est plus petit que intla valeur est promue int. Ceci est documenté dans la section 6.3.1.1p2 de la norme C :

Les éléments suivants peuvent être utilisés dans une expression partout où un intou unsigned intpeuvent être utilisés

  • Un objet ou une expression avec un type entier (autre que intou unsigned int) dont le rang de conversion entier est inférieur ou égal au rang de intet unsigned int.
  • Un champ de bits de type _Bool, int ,signé int , ornon signé int`.

Si an intpeut représenter toutes les valeurs du type d'origine (limitées par la largeur, pour un champ binaire), la valeur est convertie en an int; sinon, il est converti en un unsigned int. Celles-ci sont appelées les promotions entières . Tous les autres types sont inchangés par les promotions entières.

Donc, si une variable a un type uint8_tet la valeur 255, l'utilisation d'un opérateur autre qu'une conversion ou une affectation dessus la convertira d'abord en type intavec la valeur 255 avant d'effectuer l'opération. C'est pourquoi sizeof(~i)vous donne 4 au lieu de 1.

La section 6.5.3.3 décrit que les promotions entières s'appliquent à l' ~opérateur:

Le résultat de l' ~opérateur est le complément au niveau du bit de son opérande (promu) (c'est-à-dire que chaque bit du résultat est défini si et seulement si le bit correspondant dans l'opérande converti n'est pas défini). Les promotions entières sont effectuées sur l'opérande et le résultat a le type promu. Si le type promu est un type non signé, l'expression ~Eest équivalente à la valeur maximale représentable dans ce type moins E.

Donc, en supposant un 32 bits int, si countera la valeur 8 bits, 0xffil est converti en valeur 32 bits0x000000ff , et l'appliquer ~vous donne 0xffffff00.

La façon la plus simple de gérer cela est probablement sans avoir à connaître le type de vérifier si la valeur est 0 après l'incrémentation, et si c'est le cas décrémenter.

if (!++counter) counter--;

L'enveloppement d'entiers non signés fonctionne dans les deux sens, donc la décrémentation d'une valeur de 0 vous donne la plus grande valeur positive.

dbush
la source
1
if (!++counter) --counter;pourrait être moins bizarre pour certains programmeurs que d'utiliser l'opérateur virgule.
Eric Postpischil
1
Une autre alternative est ++counter; counter -= !counter;.
Eric Postpischil
@EricPostpischil En fait, j'aime mieux votre première option. Édité.
dbush
15
C'est moche et illisible, peu importe comment vous l'écrivez. Si vous devez utiliser un idiome comme celui-ci, rendez service à chaque programmeur de maintenance et enveloppez-le comme une fonction en ligne : quelque chose comme increment_unsigned_without_wraparoundou increment_with_saturation. Personnellement, j'utiliserais une fonction générique à trois opérandes clamp.
Cody Gray
5
De plus, vous ne pouvez pas en faire une fonction, car elle doit se comporter différemment pour différents types d'arguments. Vous devez utiliser une macro de type générique .
user2357112 prend en charge Monica
7

en taille de (i); vous demandez la taille de la variable i , donc 1

en taille de (~ i); vous demandez la taille du type de l'expression, qui est un int , dans votre cas 4


Utiliser

si (~ i)

savoir si je ne valorise pas 255 (dans votre cas avec un uint8_t) n'est pas très lisible, il suffit de faire

if (i != 255)

et vous aurez un code portable et lisible


Il existe plusieurs tailles de variables (par exemple, uint16_t et char non signé, etc.)

Pour gérer n'importe quelle taille de non signé:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

L'expression est constante, donc calculée au moment de la compilation.

#include <limits.h> pour CHAR_BIT et #include <stdint.h> pour uintmax_t

bruno
la source
3
La question indique explicitement qu'ils ont plusieurs tailles à traiter, elle != 255est donc inadéquate.
Eric Postpischil
@EricPostpischil ah oui, j'oublie ça, donc "if (i! = ((1u << sizeof (i) * 8) - 1))" en supposant toujours non signé?
bruno
1
Cela ne sera pas défini pour les unsignedobjets car les décalages de la largeur totale de l'objet ne sont pas définis par la norme C, mais cela peut être corrigé avec (2u << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil
oh oui ofc, CHAR_BIT, mon mauvais
bruno
2
Pour la sécurité avec des types plus larges, on pourrait utiliser ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil
5

Voici plusieurs options pour implémenter «Ajouter 1 à xmais fixer à la valeur représentable maximale», étant donné qu'il xs'agit d'un type entier non signé:

  1. Ajoutez-en un si et seulement si xest inférieur à la valeur maximale représentable dans son type:

    x += x < Maximum(x);

    Voir l'article suivant pour la définition de Maximum. Cette méthode a de bonnes chances d'être optimisée par un compilateur pour des instructions efficaces telles qu'une comparaison, une certaine forme d'ensemble ou de déplacement conditionnel et un ajout.

  2. Comparez à la plus grande valeur du type:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (Ceci calcule 2 N , où N est le nombre de bits en x, en décalant 2 de N -1 bits. Nous faisons cela au lieu de décaler 1 N bits car un décalage du nombre de bits dans un type n'est pas défini par le C La CHAR_BITmacro peut ne pas être familière à certains; c'est le nombre de bits dans un octet, tout sizeof x * CHAR_BITcomme le nombre de bits dans le type de x.)

    Cela peut être enveloppé dans une macro comme souhaité pour l'esthétique et la clarté:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. Incrémentez xet corrigez s'il revient à zéro, à l'aide d'un if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Incrémentez xet corrigez s'il revient à zéro, en utilisant une expression:

    ++x; x -= !x;

    Ceci est nominalement sans branche (parfois bénéfique pour les performances), mais un compilateur peut l'implémenter comme ci-dessus, en utilisant une branche si nécessaire mais éventuellement avec des instructions inconditionnelles si l'architecture cible a des instructions appropriées.

  5. Une option sans branche, utilisant la macro ci-dessus, est:

    x += 1 - x/Maximum(x);

    Si xest le maximum de son type, ceci est évalué à x += 1-1. Sinon, ça l'est x += 1-0. Cependant, la division est quelque peu lente sur de nombreuses architectures. Un compilateur peut optimiser cela en instructions sans division, selon le compilateur et l'architecture cible.

Eric Postpischil
la source
1
Je ne peux pas me résoudre à voter pour une réponse qui recommande d'utiliser une macro. C a des fonctions en ligne. Vous ne faites rien à l'intérieur de cette définition de macro qui ne peut pas être fait facilement à l'intérieur d'une fonction en ligne. Et si vous allez utiliser une macro, assurez-vous de faire une parenthèse stratégique pour plus de clarté: l'opérateur << a une très faible priorité. Clang met en garde à ce sujet avec -Wshift-op-parentheses. La bonne nouvelle est qu'un compilateur d'optimisation ne va pas générer de division ici, vous n'avez donc pas à vous soucier de sa lenteur.
Cody Gray
1
@CodyGray, si vous pensez pouvoir le faire avec une fonction, écrivez une réponse.
Carsten S
2
@CodyGray: sizeof xne peut pas être implémenté à l'intérieur d'une fonction C car le xdevrait être un paramètre (ou une autre expression) avec un type fixe. Il ne pouvait pas produire la taille du type d'argument utilisé par l'appelant. Une macro peut.
Eric Postpischil
2

Avant stdint.h, les tailles des variables peuvent varier d'un compilateur à l'autre et les types de variables réels en C sont toujours int, longs, etc. et sont toujours définis par l'auteur du compilateur quant à leur taille. Pas d'hypothèses standard ou spécifiques. Les auteurs doivent ensuite créer stdint.h pour mapper les deux mondes, c'est le but de stdint.h pour mapper uint_this celui en int, long, short.

Si vous portez du code à partir d'un autre compilateur et qu'il utilise char, short, int, long, vous devez passer par chaque type et faire le port vous-même, il n'y a aucun moyen de le contourner. Et soit vous vous retrouvez avec la bonne taille pour la variable, la déclaration change mais le code écrit fonctionne ....

if(~counter) counter++;

ou ... fournissez le masque ou le transtypage directement

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

À la fin de la journée, si vous voulez que ce code fonctionne, vous devez le porter sur la nouvelle plate-forme. Votre choix quant à la manière. Oui, vous devez passer du temps à chaque cas et le faire correctement, sinon vous allez continuer à revenir à ce code qui est encore plus cher.

Si vous isolez les types de variables sur le code avant le portage et quelle est la taille des types de variables, puis isolez les variables qui le font (devrait être facile à grep) et modifiez leurs déclarations à l'aide des définitions stdint.h qui, espérons-le, ne changeront pas à l'avenir, et vous seriez surpris, mais les mauvais en-têtes sont parfois utilisés, alors même mettez des chèques pour que vous puissiez mieux dormir la nuit

if(sizeof(uint_8)!=1) return(FAIL);

Et bien que ce style de codage fonctionne (si (~ counter) counter ++;), pour la portabilité le souhaite maintenant et à l'avenir, il est préférable d'utiliser un masque pour limiter spécifiquement la taille (et ne pas s'appuyer sur la déclaration), faites-le lorsque le le code est écrit en premier lieu ou il suffit de terminer le port et vous n'aurez pas à le re-porter à nouveau un autre jour. Ou pour rendre le code plus lisible, faites le si x <0xFF alors ou x! = 0xFF ou quelque chose comme ça alors le compilateur peut l'optimiser dans le même code qu'il le ferait pour n'importe laquelle de ces solutions, le rend simplement plus lisible et moins risqué ...

Cela dépend de l'importance du produit ou du nombre de fois que vous souhaitez envoyer des correctifs / mises à jour ou faire rouler un camion ou vous rendre au laboratoire pour régler la question de savoir si vous essayez de trouver une solution rapide ou simplement de toucher les lignes de code affectées. si ce n'est qu'une centaine ou quelques-uns, ce n'est pas si grand qu'un port.

old_timer
la source
0
6.5.3.3 Opérateurs arithmétiques unaires
...
4 Le résultat de l' ~opérateur est le complément au niveau du bit de son opérande (promu) (c'est-à-dire que chaque bit du résultat est défini si et seulement si le bit correspondant dans l'opérande converti n'est pas défini. ). Les promotions entières sont effectuées sur l'opérande et le résultat a le type promu . Si le type promu est un type non signé, l'expression ~Eest équivalente à la valeur maximale représentable dans ce type moins E.

C 2011 Draft en ligne

Le problème est que l'opérande de ~est promu intavant que l'opérateur ne soit appliqué.

Malheureusement, je ne pense pas qu'il existe un moyen facile de résoudre ce problème. L'écriture

if ( counter + 1 ) counter++;

n'aidera pas, car les promotions s'appliquent également là-bas. La seule chose que je peux suggérer est de créer des constantes symboliques pour la valeur maximale que vous souhaitez que cet objet représente et de tester par rapport à cela:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;
John Bode
la source
J'apprécie le point sur la promotion des nombres entiers - il semble que c'est le problème que nous rencontrons. Il -1convient toutefois de noter que dans votre deuxième exemple de code, le n'est pas nécessaire, car cela entraînerait le compteur à 254 (0xFE). Dans tous les cas, cette approche, comme mentionné dans ma question, n'est pas idéale en raison des différentes tailles de variables dans la base de code qui participent à cet idiome.
Charlie Salts le