Un int qui change de taille et de valeur

Bonjour,

J'utilise l'ide 1.8.16 avec une carte Uno.

Je me suis intéressé à la fonction abs qui est définie dans Arduino.h de la façon suivante:
#define abs(x) ((x)>0?(x):-(x))
Je note qu'il n'y a pas de type défini.

Avec le programme:

void setup()
{
  Serial.begin(115200);

  Serial.print("abs(10) = "); Serial.println(abs(10)); 
  Serial.print("sizeof(abs(10)) = "); Serial.println(sizeof(abs(10)));
  Serial.println();

  Serial.print("abs(-10) = "); Serial.println(abs(-10)); 
  Serial.print("sizeof(abs(-10)) = "); Serial.println(sizeof(abs(-10))); 
  Serial.println();

  Serial.print("abs(32767) = "); Serial.println(abs(32767)); 
  Serial.print("sizeof(abs(32767)) = "); Serial.println(sizeof(abs(32767))); 
  Serial.println();

  Serial.print("abs(-32768) = "); Serial.println(abs(-32768)); 
  Serial.print("sizeof(abs(-32768)) = "); Serial.println(sizeof(abs(-32768))); 
  Serial.println();
}


void loop()
{
}

J'obtiens bien ce que j'attends:

abs(10) = 10
sizeof(abs(10)) = 2

abs(-10) = 10
sizeof(abs(-10)) = 2

abs(32767) = 32767
sizeof(abs(32767)) = 2

abs(-32768) = 32768
sizeof(abs(-32768)) = 4

Par défaut tout est en int, tant que c'est dans la limite [-32768, 32787] et si -32768 est un int son opposé ne tenant pas passe dans un long, et j'ai bien 4 octets. Mais le problème se pose quand je mets toutes ses valeurs dans un int.

Je teste le programme suivant:

void fonction(int parametre) // Permet l'analyse du paramètre
{
  int parametreAbs = abs(parametre); // Un bel int sur 2 octets
  unsigned long depart = millis(); // Pour avoir une idée du nombre de boucles faites
  
  Serial.print("sizeof(parametreAbs) = "); Serial.println(sizeof(parametreAbs)); // Normalement 2
  Serial.print("parametreAbs = "); Serial.println(parametreAbs); // normalement dans [-32768, 32767]

  while (parametreAbs > 0) 
  {
    parametreAbs--;
    delay(1); // Permet de compter le nombre approximatif de boucles, chacune durant environ 1ms
  }
  
  Serial.print(millis() - depart);  Serial.println("ms"); // Et affichange du temps
  Serial.println();
}


void setup()
{
  Serial.begin(115200);
}


void loop()
{
  fonction(100); // Devrait faire 100 boucles
  fonction(-32768); // Ne devrait rien faire du tout si parametreAbs = -32768
}

Mais j'obtiens un résultat curieux:

sizeof(parametreAbs) = 2
parametreAbs = 100
100ms

sizeof(parametreAbs) = 2
parametreAbs = 4294934528
32898ms

Pour fonction(100); tout est normal. Je passe 100, la valeur absolue est 100, cela tient dans un int, la taille indiquée est 2, et la boucle dure 100ms (des fois 101ms), il y a donc bien les 100 boucles.

Pour fonction(-32768); parametre vaut -32768 , c'est un int 16 bits, la valeur absolue ne tient pas dans le int, il y a dépassement de la capacité mais c'est normal, je m'y attends. Et quand je range la valeur -32768 dans parametreAbs qui est un int, le programme me dit que cela occupe bien 2 octets, mais que la valeur est 4294934528 qui ne peut pas rentrer.

Que contient réellement parametreAbs?
Des fois 4294934528 comme pour l'affichage
Des fois un nombre positif sinon, je ne ferais pas le while
Des fois +32768 ou à peu près sinon le nombre de boucles ne serait pas bon.

Je me suis dit que pour être sûr je vais transtyper abs(parametre) en int en mettant:
int parametreAbs = (int)abs(parametre); // Un bel int sur 2 octets
Mais cela donne toujours le même résultat.

Pour que cela soit plus bizarre, si on supprime dans loop la ligne
fonction(100); // Devrait faire 100 boucles
On obtiens ce que j'attendais, une suite de:

sizeof(parametreAbs) = 2
parametreAbs = -32768
4ms

J'aurais le résultat inverse, je me dirais que si la fonction n'est appelée qu'une fois, le compilateur pourrait comprendre que je veux faire 32768 boucles et coder cela en dur sans utiliser les variables. Mais c'est l'inverse!

On dit souvent que pour comprendre on peut rajouter un print de la variable. En rajoutant l'affichage de la valeur absolue:

void fonction(int parametre) // Permet l'analyse du paramètre
{
  int parametreAbs = (int)abs(parametre); // Un bel int sur 2 octets
  unsigned long depart = millis(); // Pour avoir une idée du nombre de boucles faites
  
  Serial.print("sizeof(parametreAbs) = "); Serial.println(sizeof(parametreAbs)); // Normalement 2
  Serial.print("parametreAbs = "); Serial.println(parametreAbs); // normalement dans [-32768, 32767]

  while (parametreAbs > 0) 
  {
    Serial.print("dans while -> parametreAbs = "); Serial.println(parametreAbs); // normalement dans [-32768, 32767]
    parametreAbs--;
    delay(1); // Permet de compter le nombre approximatif de boucles, chacune durant environ 1ms
  }
  
  Serial.print(millis() - depart);  Serial.println("ms"); // Et affichange du temps
  Serial.println();
}


void setup()
{
  Serial.begin(115200);
}


void loop()
{
  fonction(100); // Devrait faire 100 boucles
  fonction(-32768); // Ne devrait rien faire du tout si parametreAbs = -32768
}

J'obtiens:

sizeof(parametreAbs) = 2
parametreAbs = -32768
dans while -> parametreAbs = -32768
dans while -> parametreAbs = 32767
dans while -> parametreAbs = 32766
dans while -> parametreAbs = 32765
dans while -> parametreAbs = 32764
dans while -> parametreAbs = 32763
dans while -> parametreAbs = 32762
dans while -> parametreAbs = 32761
dans while -> parametreAbs = 32760
dans while -> parametreAbs = 32759

Cela change la valeur de parametreAbs qui passe de 4294934528 à -32768 comme je m'y attendais mais je rentre dans la boucle avec une valeur négative.

Je fais combien d'erreurs?

Sizeof est résolu à la compilation.

Je n’ai pas lu en détail mais Ça peut être les joie de l’optimiseur de code par que vous avez des constantes et que le pré processeur peut aussi faire des calculs

Que se passe-t-il si vous déclarez une variable volatile, que vous mettez -32768 dedans et appelez la fonction avec cette variable en paramètre ?

En faisant cela:

volatile int a=-32768; 
void loop()
{
  fonction(100); // Devrait faire 100 boucles
  fonction(a); // Ne devrait rien faire du tout si parametreAbs = -32768
}

j'ai toujours:

sizeof(parametreAbs) = 2
parametreAbs = 100
101ms

sizeof(parametreAbs) = 2
parametreAbs = 4294934528
32897ms

On peut même dire que c'est pire:

void loop()
{
  fonction(-32768); // Ne devrait rien faire du tout si parametreAbs = -32768
}

Fonctionne correctement, j'obtiens des:

sizeof(parametreAbs) = 2
parametreAbs = -32768
4ms

Alors que:

volatile int a=-32768; 
void loop()
{
  fonction(a); // Ne devrait rien faire du tout si parametreAbs = -32768
}

Ne fonctionne pas, j'obtiens:

sizeof(parametreAbs) = 2
parametreAbs = 4294934528
32897ms

Ok à mon avis ça veut dire que l’optimiseur remplace les expressions et enlève la variable et donc on se retrouve dans le print avec la macro et comme ça déborde le int est promu et donc on appelle la version de print qui traite un long

Le sizeof étant remplacé très tôt à la compilation, 2 est câblé en dur avant que l’optimiseur ne passe.

Le débordement d’un entier en c++ est qualifié de undefined behavior donc vous êtes fautif, pas la norme ni le compilateur :wink:

If during the evaluation of an expression, the result is not mathematically defined or not in the range of representable values for its type, the behavior is undefined . [...]

En pratique donc il faut tester les cas limites et les gérer avant de déborder

Ce qui est quand même curieux c'est que l'optimisation dépande de ce qu'il y a dans loop...

Ceci voudrait dire qu'il ne faudrait pas non plus jouer avec les débordemennts. Mais c'est ce que l'on fait dans:

byte n=0;
void loop()
{
  analogWrite(LED, n);
  n++;
}

pour avoir une dent de scie lumineuse

Encore pire, on utilise deux fois le débordement au retour à zéro quand on écrit
if (millis() - dernierChangement >= periode)

D'ailleurs millis() aussi utilise le débordement sans test. C'est bien un débordement et pas un test qui le fait repasser à 0.

 

Note: Ce problème de abs(-32768) me vient d'un bug de la bibliothèque Stepper.

Le propre de l’optimisation c’est de dépendre effectivement du code et une petite modification peut avoir des grands conséquences

J’aurais dû être plus précis quand je parlais d’entiers, il s’agit bien des entiers naturels signés.

Le débordement sur les unsigned est dans la norme, c’est un comportement déterministe. (Et lors d’une opération avec un unsigned les autres elements sont promus en unsigned)

1 Like

OK. Merci.