Question sur une fonction - C'est plus rapide, ou non ?

Bonjour !
Je me pose une question type programmation...


Je travaille sur un projet, en ce moment, nécessitant un LCD. Pour faire des affichages sur celui-ci, j'utilise la méthode classique suivante, par exemple :

#include LiquidCrystal_I2C.h
#include  Wire.h
LiquidCrystal_I2C lcd(0x27,  16, 2);
...
lcd.setCursor(0,0);
lcd.print(“un truc”);
lcd.setCursor(0,1);
lcd.print(“un autre truc”);

Maintenant, imaginons que je veuille, dans ma loop, faire des tache nécessitants que celle-ci aille vite, et qu'il n'y ai pas de tache longue ou bloquante. Afficher des trucs sur un LCD, c'est long. Je suis dans ce cas pour mon projet. Pour régler ce problème, on m'a conseillé de mettre le code d'affichage de "trucs" dans une fonction, appelée dans la loop. Apparement, ça permet qu'au lieu de passer du temps a exécuter le code d'affichage, ce qui ralenti la loop, on appelle seulement une fonction, ce qui est bien plus rapide.

Un exemple de code :

#include LiquidCrystal_I2C.h
#include  Wire.h
LiquidCrystal_I2C lcd(0x27,  16, 2);
...
void loop() {
  // loop devant s'exécuter rapidement
  uniquement des trucs rapides...
  // appelle de la fonction d'affichage
  fonctionAffichageRapide();
}

void fonctionAffichageRapide() {
  // affichages lents
  lcd.setCursor(0,0);
  lcd.print(“un truc”);
  lcd.setCursor(0,1);
  lcd.print(“un autre truc”);
}

Ce que je ne comprends pas, c'est que ça voudrait dire que le code d'affichage s’exécuterait en parallèle du code de la loop ? Est-ce réellement plus rapide d'exécuter une fonction que le code de celle-ci uniquement ?

La question que je me pose est : Le compilateur remplace le nom de la fonction par le code qu'elle contient, ou il ne fait qu'appeler cette fonction puis il continue, laissant le code de la fonction s'exécuter en parallèle ?

Merci d'avance pour vos réponses !
Cordialement
Pandaroux007

Si tu ajoutes une fonction, il y a, à priori, un appel de fonction supplémentaire donc je ne crois pas que ce soit plus rapide.
Dans le doute le plus simple, c'est de faire la mesure.
Tu places:

  • unsigned long debut = millis();
    avant l'appel à la fonction ou au début du bloc d'instructions
  • Serial.print(millis()-debut);
    après l'appel à la fonction ou après le bloc d'instructions et tu auras ta réponse.

Une chose est sûr, il n'y a pas d'exécution de code en parallèle puisqu'il n'y a qu'un seul cœur.

Tout dépend de ce que tu entends par long et rapide.
Une chose est certaine les afficheurs LCD ne sont pas très rapides et ils présentent pas mal de rémanence donc c'est parfaitement inutile de rafraîchir trop souvent leur contenu puisqu'il va être illisible.
Il faut aussi optimiser l'affichage si tu écris "un autre truc" mais qu'il n'y a que "truc" qui change à chaque fois peut-être est-il préférable de ne rafraîchir que "truc" et pas toute la ligne.
Tu peux aussi choisir de ne rafraîchir qu'une ligne par itération de loop() par exemple.

Bonjour et merci @fdufnews pour votre réponse :slight_smile:

OK - ah bah oui, c'est pas faux... Je n'y avais pas pensé.

Merci, je vais le faire. Je vous partagerai les résultats ici.

Donc, si je veux avoir une loop rapide, il me faut limiter les écritures sur le LCD au maximum. Moins il y a de caractères écrient sur le LCD à chaque loop, plus elle est rapide, c'est ça ?

Pandaroux007

Bonjour pandaroux007

Tu peux aussi afficher toutes les x millisecondes, ça ne sert à rien d'afficher à chaque "tour" de loop(), l'œil n'est de toute façon pas assez rapide.

Cordialement
jpbbricole

Bonjour,

+1 avec @jpbbricole.
Ça ne sert à rien d'afficher avec une période inférieure à 200 ou 300 ms, l’œil ne pourra pas suivre.

Pour répondre le plus simplement, le compilateur remplace le nom de la fonction, par un saut à l'adresse ou est définie la fonction, ce qui fait que lorsqu'il arrivera à cet instruction, il va aller à l'adresse indiqué et exécuter le code de celui-ci.
Lorsque la fonction est fini, on revient à l'adresse suivant le saut, qui a était au préalable sauvée.

Il n'y a donc pas de magie, si dans ta loop, tu appel une fonction, la loop va obligatoirement se rallonger du temps d'exécution de cette fonction, plus le temps d'appel de la fonction et le temps pour revenir à l'exécution de la loop.
Bien sûre en réalité c'est un peu plus compliqué.

L'appel et le retour devrait être de l'ordre de 4 périodes pour l'appel (comme on ne passe pas de paramètres, c'est juste un CALL) et le retour aussi.
En mettant le code dans une fonction, on devrait être plus long d'environ 8 périodes d'horloge, soit environ 0,5µs sur une nano. Même en utilisant micros() à la place de millis(), cela ne peut pas se voir. En plus sur une nano, micros() à une résolution de 4µs seulement.

Si il n'y a qu'un seul appel, il n'est pas impossible que le compilateur optimise et remplace l'appel par le corps de la fonction. Du coup on ne voir rien du tout.

Si on veut mesurer le temps appel/retour, il faut le faire par exemple un millier de fois. Avec micros(), on devrait consommer 1000x0,5µs qui peut se mesurer.

De toutes façons la fonction micro() ne compte qu’avec un pas de 4 micro seconde sur un avr à 16 MHz d’horloge -> choix de configuration par Arduino

Sur d’autre plate-forme je ne sais pas : ce pourra être pareil ou pas selon l’ecriture du framework.

Quand on arrive a s’y retrouver avec les registres des timers le mieux est de configurer le pre-scaler d’un timer à la valeur minimale et de lire la valeur de son compteur.
Avec un avr on a droit à la valeur 1, le resultat du compteur est alors exprimé en nombre de cycle horloge.

C’est très facile a faire avec un micro avr, devant la complexité des registres avec un ARM ou un espressif je n’ai pas tenté l’operation avec ces microcontroleurs.
Déjà dans ces micros il faut reussir a trouver la vraie valeur de l’horloge des timers qui est differente de la valeur horloge annoncée pour le micro.

Il ne reste que la solution de trouver, dans la documentation, le pas de mesure de millis() dans chaque platforme.

Sur une nano, millis() s'incrémente toutes les 1024µs (c'est à mon avis un choix pour perdre du temps et des octets). Si millis() s'incrémentait de 1 à chaque fois, elle retarderait. Elle va donc s'incrémenter 976 fois de 1 et 24 fois de 2. La résolution de millis() est donc de 2ms. Mais je ne l'ai pas vu dans la documentation.

Les 4µs de micros c'est à cause du compteur 0 de la nano qui est un 8 bits. En incrémentant son compteur, il déborde 1024 fois par secondes, ce qui permet d'incrémenter millis(). Si le compteur était un 16 bits, on pourrait alors avoir une résolution de 62,5ns.

Quand on a la paresse, on utilise micros() en déréglant l'horloge pour qu'elle compte les 62,5ns au lieu des 4µs. Il n'y a qu'un seul registre à changer! Sans se caser la tête: TCCR0B &=0b11111101; ou un équivalent. Mais je pense que TCCR0B=1; suffit!

C'est le résultat d'un compromis dans la config Arduino et peut-être bien déjà avec Wiring.
Compromis entre fréquence de récurence de la PWM et mesure du temps.

Mais on ne va pas se prendre le chou pour des atmega328p qui ne sont plus au catalogue de Microchip. [1]

Ma remarque était pour appuyer sur le fait que la valeur du pas de micro() sur les autres plateformes ne me parait pas être limpide.

[1]--> cela ne veut pas dire que Microchip ne continu pas à en produire, mais seulement pour ses principaux clients et pour une période limitée.

[2] --> il ne faut pas se fier à la fréquence d'horloge annoncée.
ATMEGA328p
Fréq horloge 16 MHz, horloge max du timer 16 MHz, diviseur min = 1

ESP32-C3
Fréq horloge 160 MHz, horloge max du timer 80 MHz, diviseur min = 2

ATmega : frequence timer max = 16 MHz
ESP32-C3 : frequence timer max = 40 MHz

Horloge multipliée par 10 et gain sur le timer = 2,5

Bonjour à tous et merci pour vos réponses !
Je n'ai pas compris le débat sur millis, micro, les timers etc... Mais j'ai suivi l'essentiel pour ce qui est des fonctions, pour le pour le moment, je retiens ceci :

Cordialement
Pandaroux007