Salut
En fait je crois qu'on est d'accord sur le fond:
Comme l'avait dit Donald Knuth (en reprenant Tony Hoare) "Premature Optimization Is the Root of All Evil" (même s'il ne faut pas non plus faire n'importe quoi sous prétexte que l'on optimisera plus tard).
Bien sûr lorsqu'on développe pour micro-contrôleur souvent il faut faire attention à la mémoire et au temps d'exécution. Si l'écriture "correcte" n'était pas assez rapide ou trop gourmande en mémoire parce que l'optimiseur n'a pas "vu" comment faire, alors bien sûr on peut prendre des optimisations quand on sait ce que l'on fait. Mais dans ce cas avant de dépendre d'effets de bords sur HIGH et LOW, je pense que j'enlèverai l'appel à digitalWrite() pour utiliser directement les PORTS...
En pratique on est d'accord sur le fait que votre approche fonctionne aujourd'hui et sans doute encore pour longtemps vu les dépendances et l'historique qui entraineraient des conséquences sur de nombreux codes si cela venait à être modifié.
Là où on n'est peut-être pas d'accord c'est plus conceptuel et un respect des bonnes pratiques: Comme le C++ est beaucoup plus typé que le langage C (et que ce typage fort est de plus en plus imposé par les compilateurs), essayer de respecter la cohérence des types en C++ est pour moi important pour éviter des soucis dans le futur et former "les apprentis développeurs" aux bonnes pratiques. Par exemple utiliser des effets de bords (la dépendance à des valeurs non documentées si ce n'est en regardant dans le code) n'est pas considéré comme une bonne pratique. C'est pour cela qu'ici je ne le recommanderai pas votre raccourci dans un code qui a vocation d'exemple pour un débutant.
----------------------------------------
Sinon long débat sur votre point
Si j'écris : byte a = 1; le compilateur ne passera pas par la case "1 est un entier" avant de se raviser à l'optimisation.
Je pense qu'on est d'accord mais ça dépend de ce que vous entendez par là.
Si vous voulez dire qu'il ne génère pas le code tout de suite avec un entier puis ensuite génère encore plus de code pour transformer les 2 ou 4 octets en 1 seul octets et espérer que l'optimiseur ensuite fasse le ménage, on est d'accord.
Schématiquement le compilateur (le parser) va d'abord évaluer l'expression et bâtir un arbre de représentation. l'opération est '=' avec 2 feuilles à gauche et à droite.
Le compilateur évalue d'abord la feuille de droite. il n'y a pas d'opération à droite et voit une rvalue (techniquement une constante littérale de type entier), donc pour le moment il la conserve comme rvalue typée. Ensuite il regarde à gauche et comme c'est une affectation attend une lvalue (un endroit en mémoire), ici c'est simple, on a une variable donc directement la lvalue.
Cet arbre n'est pas OK tel quel puisque le type sous jacent de la rvalue n'est pas identique à celui de la lvalue. Le compilateur déclenche donc ses règles de conversion implicites. La règle dit que le programme ne peut compiler que s'il existe une séquence de conversion implicite non ambiguë du premier type vers le second type. Ici le compilateur applique la règle dite de "Lvalue to rvalue conversion" et commence par regarder la règle de "Numeric promotions". Mais ici comme le type à droite est "plus petit" que le type à gauche, il appliquera la règle de "Numeric conversions" qui peut conduire à de la perte d'information et modification de la valeur. Le compilateur - au vu des types - détecte que la règle applicable est la suivante:
If the destination type is unsigned, the resulting value is the smallest unsigned value equal to the source value modulo 2
n where n is the number of bits used to represent the destination type.
Comme on a une rvalue de type littéral sur 2 octets, le compilateur sait prendre tout seul l'octet de poids faible (il connait directement la valeur) et génère le code d'affectation à la mémoire associée à la lvalue a dont il connait aussi l'adresse. Si ce n'était pas un littéral mais une autre variable, il aurait aussi su aller chercher uniquement l'octet correspondant au poids faible donc sans appliquer de masque non plus. Si c'était un calcul, il aurait une représentation dans un ou plusieurs registres du résultat du calcul et saurait quel est l'octet là encore qui représente le poids faible et donc n'affecterait que celui là.
Quand je fais
digitalWrite(ledPin, touchVal > threshold);
ce sont d'autres règles qui s'appliquent:
le compilateur doit évaluer les paramètres à mettre sur la pile pour appeler la fonction.
il voit `touchVal > threshold` et donc c'est une opération transitoire de type rvalue (pas de mémoire associée). L'arbre correspond à un opérateur '>' et deux lvalue. le type du résultat, porté par le noeud du graphe lié au >, est une rvalue de type booléen.
le compilateur sait qu'il doit générer un test et disposera dans un registre transitoire du booléen vrai ou faux.
Il voit ensuite que cette rvalue doit être affectée sur la pile à une lvalue de type uint8_t. Il n'y a pas égalité de type donc il doit faire appel aux "Implicit conversions" et ce coup ci la règle qui s'applique n'est plus une transformation numérique mais une règle d'abord spécifique aux booléens qui dit
the type bool can be converted to int with the value false becoming 0 and true becoming 1
donc il sait obtenir un type entier (une rvalue de type littéral) et ensuite il sait transformer (par la même règle que la règle précédente) cet entier sur plusieurs octets en un seul octet en prenant l'octet de poids faible.
C'est ce qu'il fait et vous avez donc 0 ou 1 sur la pile, sous forme d'un seul octet et votre code fonctionne parce que "par chance" HIGH c'est 1 et LOW c'est 0.
dans ma version plus compliquée des choses avec l'opérateur ternaire
touchPin > threshold ? HIGH : LOW
l'arbre d'évaluation est plus compliqué: il a comme racine l'opérateur ternaire
et attend à gauche un booléen et à droite deux expressions.
Je vous passe le cheminement des conversions et règles appliquées par le compilateur, elles sont identiques à celles que l'on a vu plus haut, mais comme les expressions que l'on retourne suivant le booléen sont aussi des constantes littérales, là c'est l'optimiseur qui va faire son travail et identifier que le résultat est équivalent à celui de la promotion du booléen. L'optimiseur va aussi aller plus loin et in-liner la fonction digitalWrite() sans doute plutôt que de faire un appel d'ailleurs
bref - si vous avez la variable (lvalue) b de type booléen en mémoire à l'adresse 0x100 et que vous faites ou
digitalWrite(13, b ? HIGH : LOW);
, l'assembleur généré pour le second paramètre sera simplement similaire à un c'est à dire que l'on va chercher un octet en mémoire qui est simplement l'octet reprenant b. c'est ce registre qui sera ensuite utilisé
Même coût informatiquement parlant, mais une version est plus "maintenable" et lisible que l'autre à mon avis et donc devrait avoir la préférence du développeur.
Partager