Pourquoi le compilateur vire mon code?

Bonjour,

Cela fait un moment que je coinçais sur un bug dans un gros programme. J'ai trouvé un bidouillage qui fait tout fonctionner, mais ce n'est pas satisfaisant. Voici une version très allégé de mon code (faite pour y comprendre un peu plus).

//--- Variable pointeur sur ma classe ---
class ClassTest; // Prédéfinition
ClassTest *pointeurSecret; // Il pointera sur l'instance de ma classe


//--- Classe de test --------------------
class ClassTest
{
 public:
  ClassTest(void); // Constructeur!
  void (*functionExt)(void); // Une fonction externe de ma classe qu'elle peut appeler
};
ClassTest::ClassTest(void)
{
  pointeurSecret=this; // Quand je crée l'instance, on peut y accéder par le pointeurSecret
}


//--- Une fonction externe --------------
void uneFonction(void)
{
  // Elle doit faire n'importe quoi, cela n'a pas d'importance
  // Elle va afficher le mot bonjour, suivi d'un numéro
  Serial.println("Bonjour N°"+String(random(100)));
  delay(500); // C'est pas la peine de remplir trop vite la console
}


//--- Fonction pour loop() --------------
void fonctionSecrette(void)
{
  // Le pointeurSecret indique donc (si créée) une instance de ma classe
  // Cette insstruction appelle la fonction externe de l'instance
  // Cela va appeler uneFonction
  // uneFonction affiche le mot bonjour
  // En résumé, fonctionSecrète affiche bonjour (faut pas le dire au compilatuer)
  (*pointeurSecret->functionExt)();
}



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

  // Création de mon instance
  ClassTest test; // Je la crée
  test.functionExt=&uneFonction; // Je lui rajoute la possibilité d'accéder à une fonction externe

  asm("nop"); // Très utile!
}

void loop()
{
  fonctionSecrette();
}

Dans mon vrai code complet, mes classes sont des boutons, et l'utilisateur peut définir un comportement en surchargeant une méthode ou en utilisant une fonction externe.

Quand je compile cet exemple, cela fonctionne.

Mais si je retire le asm("nop"); le programme n'affiche plus rien. Après recherches, je me suis apperçu que la taille du code a diminuée, montrant que certaines instructions ont disparues de l’exécutable.

La ligne
ClassTest test;
peut se trouver un peu n'importe où, avant ou après start(), avant ou après Serial.begin, seule contrainte, c'est de la mettre avant
test.functionExt=&uneFonction;

Et c'est en déplaçant ces lignes que j'ai compris ce qu'il faut faire: la ligne
test.functionExt=&uneFonction;
ne doit pas se trouver en toute fin de start()!
un nop fonctionne comme le Serial.begin... Je m'en suis aperçu en mettant un Serial.println pour suivre une variable.

Mon problème est en partie résolu, je sais qu'il faut mettre quelque chose derrière test.functionExt=&uneFonction;, mais si le compilateur vire mon code parce qu'il croit que cette ligne ne sert à rien, je ne vois pas pourquoi un nop lui fait comprendre le contraire; En fait c'est le nop qu'il pouvait virer

peut être un élément de réponse là dedans : avr-libc: Compiler optimization

J'ai vu ça dans ton post, mais ils parlent de réorganisation du code, pas de sa suppression.

Quand bien même il croit que mon code ne sert à rien, il n'a pas de raison de le laisser si il y a un nop derrière. Un nop ou n'importe quelle instruction.

Qu’est-ce que ça donne si dans le setup vous faites un ClassTest* test = new ClassTest ; // Je la crée et que vous adaptiez ensuite bien sûr au fait que test est pointeur maintenant ?

Voici une version très allégé de mon code (faite pour y comprendre un peu plus).

C'est peut être le problème. En tout cas je ne vois pas.

mais si le compilateur vire mon code parce qu'il croit que cette ligne ne sert à rien, je ne vois pas pourquoi un nop lui fait comprendre le contraire; En fait c'est le nop qu'il pouvait virer

Le compilateur vire des variables qui ne servent à rien, pas le code, sauf s'il s'agit de fonctions entières.

Bonjour,

Le compilateur vire aussi le code qui ne sert à rien.

Tout dépend de ce que l'on considère comme code.

Si tu écris par exemple :
Serial, Serial1;
C'est bien du code, aucun doute, la compilation se passe bien mais le compilateur n'en tient pas compte, car c'est du code qui n'a aucun effet.

A mon avis il ne se permettra pas de virer du code ASM inline.

Bien sur il ne virera pas de l'asm inline
Mais par exemple dans un cas comme ça:

void loop()
{
  int a;
  a=10;
  a=a+1;
}

ou ça

void loop()
{
  int a=0;
  for (byte i = 0; i < 10; i++)
    a = a + 1;
}

Les instructions sont complètement virées.

sauf si a est volatile sans doute

Les instructions sont complètement virées.

Oui probablement.

  ClassTest test; // Je la crée
  test.functionExt=&uneFonction; // Je lui rajoute la possibilité d'accéder à une fonction externe

L'exemple d'olivier_pecheux illustre la même chose.
Comme test n'est pas utilisé dans setup(), il vire ces deux lignes.

Mais si l'objet test était déclaré en global, cela change tout car rien n'indique au compilateur que cet objet n'est pas utilisé ailleurs en extern.
Si l'objet test était déclaré en static, il ne le vire pas non plus.

Il y a de quoi écrire un roman sur le sujet :wink:

hbachetti:
Comme test n'est pas utilisé dans setup(), il vire ces deux lignes.

c'est pour cela que je me demandais si avec new il aurait un comportement différent.

un nop fonctionne comme le Serial.begin... Je m'en suis aperçu en mettant un Serial.println pour suivre une variable.

Afficher une variable avec println rend son utilité effective, mais ce n'est peut être pas le cas autrement.

void setup()
{ 
  Serial.begin(9600);
  // Création de mon instance
  ClassTest test; // Je la crée
  test.functionExt=&uneFonction; // Je lui rajoute la possibilité d'accéder à une fonction externe
  asm("nop"); // Très utile!
}

Ce qui est clair est que dans ce code, si tu affiches une variable membre de l'objet test, ou que tu appelles une de ses méthodes, cela change tout.
Mais en l'état ce code instancie un objet, change une variable membre et point final, donc cet objet ne sert à rien.
Comme l'objet local est détruit en fin de fonction, le compilo optimise et ne le crée pas.

c'est pour cela que je me demandais si avec new il aurait un comportement différent.

Oui le comportement est différent. Le code n'est pas viré.
Mais bon, l'utilisation de new implique certains moyens incompatibles avec la taille mémoire d'un ATMEGA328.

hbachetti:
Oui le comportement est différent. Le code n'est pas viré.
Mais bon, l'utilisation de new implique certains moyens incompatibles avec la taille mémoire d'un ATMEGA328.

A quoi pensez vous ?

C'est une allocation dynamique mais qui prend la même place que l'allocation en variable globale. Le tas et la pile sont dans le même espace. Si la durée de vie de cet objet est celle du programme (on ne le delete pas), je ne vois pas de souci particulier c'est équivalent non ?

C'est une allocation dynamique mais qui prend la même place que l'allocation en variable globale. Le tas et la pile sont dans le même espace. Si la durée de vie de cet objet est celle du programme (on ne le delete pas), je ne vois pas de souci particulier c'est équivalent non ?

Tout à fait mais s'il est déclaré dans une fonction, comme c'est le cas dans le code d'olivier_pecheux, il ne faut pas oublier de le détruire en sortant.
Donc on tombe dans la fragmentation.

Si l'objet est déclaré en global, comme je le disais en #9 le compilateur ne vire pas l'objet.
Qu'il soit déclaré sous forme d'instance ou instancié avec new ne change rien.
Sauf qu'avec un pointeur on peut pointer vers une instance, et ensuite pointer vers une autre.
A voir si c'est utile ...

Tout à fait mais s'il est déclaré dans une fonction, comme c'est le cas dans le code d'olivier_pecheux, il ne faut pas oublier de le détruire en sortant.
Donc on tombe dans la fragmentation.

son idée c'était à mon avis de ne pas le détruire puisqu'il voulait utiliser son instance dans la loop... souci de scope.

sinon On ne fragmente pas si on alloue et dé-alloue en LIFO, donc en faisant un peut attention et en n'ajoutant pas de String dans le bidule, ça se gère :slight_smile:

son idée c'était à mon avis de ne pas le détruire puisqu'il voulait utiliser son instance dans la loop... souci de scope.

Ouïe, effectivement je n'avais pas vu ce détail.

Ce qu'il a fait revient à instancier un objet local, et affecter son adresse à un pointeur global.
Ensuite à la sortie de setup(), l'objet est détruit et le pointeur pointe dans la pile à l'adresse d'un objet invalide.
Effectivement il y a là un gros problème :confused:

@olivier_pecheux : si tu es toujours à l'écoute, il va falloir changer de méthode.

Bien vu J-M-L.

pour être plus clair --> @Olivier

Votre instance est dégagée à la fin du setup car elle a une portée locale. Votre code fonctionne par chance car vous n'avez pas trop bidouillé la mémoire et donc l'appel de la fonction reste opérationnel mais en pratique l'objet a été dé-alloué.

Pour vous rendre compte de la différence entre allocation statique locale et instanciation par new, voici un petit code qui compte le nombre d'instances de la classe et on voit quelle instance est détruite.

class A
{
  public:
    static uint8_t count;

    // constructor
    A(uint8_t v) : id(v) {
      count++;
      Serial.print(F("Constructeur pour l'instance : "));
      Serial.print(id);
      Serial.print(F(" --> Count vaut: "));
      Serial.println(count);
    }
    ~A() {
      count--;
      Serial.print(F("Destructeur pour l'instance : "));
      Serial.print(id);
      Serial.print(F(" --> Count vaut: "));
      Serial.println(count);
    }
    char identifiant() {
      return id;
    }
    static uint8_t total() {
      return count;
    }

  private:
    char id;
};

uint8_t A::count = 0; // memory allocation mandatory for class variables (Static member variables), value optional if initialized at 0 (as static are initialized to 0 by the compiler)

void setup() {
  Serial.begin(115200);
  Serial.println(F("Dans setup"));
  A objetX('X');
  A* objetY = new A('Y');
}

void loop()
{
  Serial.println(F("Dans loop()"));
  Serial.print(F("Count vaut: "));
  Serial.println(A::count);
  while (true);
}

Notez que la compilation va se plaindre et nous dire que

[color=orange]
sketch_mar18b.ino:38:6: warning: unused variable 'objetY' [-Wunused-variable]
   A* objetY = new A('Y');
      ^~~~~~[/color]

mais il va bien faire les allocations et dé-allocations. Dans la console à 115200 bauds on va voir

[color=purple]
Dans setup
Constructeur pour l'instance : X --> Count vaut: 1
Constructeur pour l'instance : Y --> Count vaut: 2
Destructeur pour l'instance : X --> Count vaut: 1
Dans loop()
Count vaut: 1
[/color]

==> on voit qu'en sortie de setup() l'instance X est désallouée automatiquement, donc tout appel à une référence vers cette instance est illégal ensuite...

Dans votre cas, comme il n'y avait pas d'effet de bord et l'instance disparaissait donc tout pointeur était illégal, le compilateur était bien fondé à virer du code.

PS: mon code a une fuite mémoire (sauf si je voulais conserver objetY) car je l'alloue dans le setup avec un nom de variable locale donc je n'ai plus accès à cette instance dans la loop mais le compteur du nombre d'instance nous dit bien qu'elle n'a pas été désallouée.

J-M-L:
sauf si a est volatile sans doute

Bien sur. Mais dans ce cas on ne peut plus dire que le code ne sert à rien puisque l'écriture et/ou la lecture dans une variable volatile est supposé faire quelque chose hors de connaissance du compilateur.

kamill:
Bien sur. Mais dans ce cas on ne peut plus dire que le code ne sert à rien puisque l'écriture et/ou la lecture dans une variable volatile est supposé faire quelque chose hors de connaissance du compilateur.

oui mais si ça ne le fait pas, c'est un hack pour éviter l'optimisation :slight_smile:

mais là ce n'était pas le problème