Chronographe: temps d’exécution anormalement rapide

Bonjour,

Dans l’objectif de mieux évaluer les performances de la programmation en C pour Arduino, j’ai écris ce petit programme:

unsigned long t[8]; // t[n] marqueurs temporels

void setup() {
  int i, j;

  pinMode(2, INPUT); 
  pinMode(3,OUTPUT);
  Serial.begin(115200);
  
  t[0] = micros(); 
  // code dont on veux mesurer le temps d'exécution
  t[1] = micros(); // on mesure l'instant t ou le code c'est exécuté
  for (i = j = 0; i < 1000; i++) { // exemple ajouter 1000 fois une constante à un entier
    j += 3;
  }
  t[2] = micros();
  for (i = 0; i < 1000; i++) { // autre exemple écrire sur une sortie 1000 fois
    digitalWrite(3, LOW);
  }
  t[3] = micros();
  for (i = 0; i < 1000; i++) {
    digitalRead(2);
  }
  t[4] = micros();
  for (i = 0; i < 1000; i++) {
    delayMicroseconds(1);
  }
  t[5] = micros();
  for (i = 0; i < 1000; i++) {
    delayMicroseconds(2);
  }
  t[6] = micros();
  for (i = 0; i < 1000; i++) {
    delayMicroseconds(3);
  }
  t[7] = micros();

  for (i = 1; i <= 7; i++) { // on affiche le résultat
    Serial.print("temps "); 
    Serial.print(i); 
    Serial.print(": "); 
    Serial.println(t[i] - t[i-1]); // différence de temps entre les deux marqueurs = temps d’exécution
  }
}

void loop() {
}

Le principe consiste à écrire différents bouts de code dont on veux tester la vitesse d’exécution, en les encadrant entre deux marqueurs temporels, en utilisant la fonction micro().
Note: la fonction micros() renvoie le temps écoulé depuis le dernier reset en multiple de 4.

Voici le résultat:
temps 1: 4
temps 2: 4
temps 3: 5284
temps 4: 4152
temps 5: 1320
temps 6: 2080
temps 7: 3084

temps 1: 4 // 4 microsecondes, normal il n’y a pas de code.

temps 2: 4 // On incrémente 1000 fois une constante à la variable j, en seulement 4 microsecondes soit 64 cycles???
C’est ce résultat qui pose problème, dont j’aimerais avoir l’explication.

temps 3: 5284 // rien à redire sinon que c’est beaucoup plus lent qu’en assembleur, normalement un cycle (*1000 plus la boucle) de mémoire

temp 4: // idem

temps 5: // 1320 microsecondes contre environ 1000 attendu.

temps 6 et 7: // RAS

temps 2: 4 // On incrémente 1000 fois une constante à la variable j, en seulement 4 microsecondes soit 64 cycles???
C'est ce résultat qui pose problème, dont j'aimerais avoir l'explication.

La variable n'est pas utilisée ailleurs dans le programme. Il est fort probable que le compilateur l'aura supprimée ainsi que le code associé. Il faudrait faire un Serial.print de la variable J à la fin de la boucle comme ça le code ne serait pas optimisé.

La fonction micros() prend elle même du temps :grin: . Cela ne gêne peut-être pas dans une première approche mais sache que la fonction micros() est basé sur un timer (le timer0 il me semble) plus tout une tripaille logicielle notamment pour absorber plusieurs débordement de compteur

Quand j'ai eu à faire ce genre de manip j'ai utilisé le timer2 avec un réglage du " préscaler" à 1. J'aurais pu prendre aussi le Timer0 mais j'ai préféré éviter de modifier sa configuration.
En faisant cela il suffit de lire le contenu du registre TCNTx, c'est à dire pour le timer2 TCNT2, et tu as directement une lecture en cycles horloge, c'est à dire que le pas est maintenant de 1/16000000= 62,5 ns -> c'est plus précis que micro() :grin: .
Pour ne pas avoir à gérer les débordements de compteur comme le fait la fonction micro() il suffit de faire une raz sur le compteur TCNT2 juste avant de début de la partie de code à mesurer.
Exemple:

uint8_t temps ;
/*****
Code
*****/

TCNT2 = 0 ;
/******************
code à mesurer

*****************/
temps = TCNT2 ;

La lecture de la partie de la datasheet qui traite des timers est super enrichissante et c'est fou ce que l'on peut faire avec.

fdufnews:
La variable n'est pas utilisée ailleurs dans le programme. Il est fort probable que le compilateur l'aura supprimée ainsi que le code associé. Il faudrait faire un Serial.print de la variable J à la fin de la boucle comme ça le code ne serait pas optimisé.

J’ai testé le code et j’obtiens toujours 4µs.
J’ai ajouté:

  j = digitalRead(2);
  for (i = 0; i < 1000; i++) {
    j+=3;
  }

et j’obtiens 8µs.
J’ai ensuite ajouté une condition:

  j = digitalRead(2);
  for (i = 0; i < 1000; i++) {
    if (j == 0) j+=3;
  }

et j’obtiens 696µs.
J’en ai déduit que le compilateur dans les premiers cas, remplaçait la boucle
for (i = 0; i < 1000; i++) {
j+=3;
}
par
j += 3000;
i = 1000;
Lorsque j’ai ajouté la condition "if (j == 0)", le compilateur a compilé la boucle.
C’est plus complexe que je ne le pensais.
Je pensais que le compilateur se contentait bêtement de compiler la boucle en instructions machines (j’avais un doute sur la fonction micros()).

68tjs:
La fonction micros() prend elle même du temps . Cela ne gêne peut-être pas dans une première approche mais sache que la fonction micros() est basé sur un timer (le timer0 il me semble) plus tout une tripaille logicielle notamment pour absorber plusieurs débordement de compteur

Oui. Par curiosité j’ai mesuré 3µs pour l’appel de la fonction micros();

68tjs:
En faisant cela il suffit de lire le contenu du registre TCNTx, c'est à dire pour le timer2 TCNT2, et tu as directement une lecture en cycles horloge, c'est à dire que le pas est maintenant de 1/16000000= 62,5 ns -> c'est plus précis que micro()
[...]
La lecture de la partie de la datasheet qui traite des timers est super enrichissante et c'est fou ce que l'on peut faire avec.

Je ne connaissais pas. Intéressant tous ça. Merci pour ces précisions, je vais me pencher dessus.

Lorsque tu as compilé ton code (sans forcément le téléverser), la fenêtre de log t'indique dans quel répertoire ça a pondu des fichiers. Tu peux alors y aller, et trouver le .hex pondu : c'est lui qui sera envoyé à l'arduino.

J'ai utilisé un soft qui transforme le .hex en assembleur. C'est pas très lisible, mais en prenant son temps, on finit par découvrir de jolies choses, comme des abus de compilation, des rajouts qui ne sont pas nécessaires etc etc... c'est très instructif!

le soft que j'utilise est "reAVR" de ja-tools, j'arrive pas à le retrouver sur la toile, mais il doit y en avoir un paquet d'autres!

EDIT : ja-tools

Super_Cinci:
le soft que j'utilise est "reAVR" de ja-tools, j'arrive pas à le retrouver sur la toile, mais il doit y en avoir un paquet d'autres!

avr-objdump (contenu dans la toolchain avr)

option : -d

???

Super_Cinci:
J'ai utilisé un soft qui transforme le .hex en assembleur. C'est pas très lisible, mais en prenant son temps, on finit par découvrir de jolies choses, comme des abus de compilation, des rajouts qui ne sont pas nécessaires etc etc... c'est très instructif!

Merci pour le lien, j’ai téléchargé le soft.
J’avais déjà remarqué l’embonpoint du programme blink. Ca doit prendre 20 ou 30 instructions en assembleur :slight_smile:
L’éditeur Arduino ne propose pas la sauvegarde et le téléchargement du fichier binaire.
La sauvegarde peut se faire manuellement en allant dans le répertoire temporaire, mais le téléchargement à partir d’un binaire?