Timer haute résolution gint/graph 90+E
Posté le 17/01/2022 22:01
Hello,
je voudrais me faire une boucle de mesure de perf de certaines opérations pour booster mes routines et être capable de mesurer les gains lors d'optimisations. Pour ce faire, je voudrais avoir une chaine de mesure de "Delta t" entre 2 points d'un programme.
J'ai codé ça, qui me semble pas trop trop mal fonctionner mais peut être y-a-t'il moyen de faire mieux (j'ai pas l'impression qu'on puisse descendre en dessous de 256Hz avec rtc.h, donc j'ai fait avec timer.h) :
#include <gint/display.h>
#include <gint/keyboard.h>
#include <gint/timer.h>
#define DELTA_INC 5 // set resolution at 5µs
// does not seem to work with lower values
static volatile uint16_t counter = 0;
uint16_t a, b;
static int callback_tick(volatile uint16_t *tick)
{
*tick += DELTA_INC;
return TIMER_CONTINUE;
}
int main(void)
{
int t = timer_configure(TIMER_ANY, DELTA_INC, GINT_CALL(callback_tick, &counter));
counter = 0;
if(t >= 0) timer_start(t);
a = counter;
// operations to be evaluated
// BEGIN
for(int k=0;k<100;k++)
{
dclear(C_WHITE);
dprint(1, 1, C_BLACK, "Sample fxSDK add-in.");
dupdate();
}
// END
b=counter;
if(t >= 0) timer_stop(t);
// Print the result in µs
dclear(C_BLACK);
dprint(1, 1, C_WHITE, "Time to operate : %lu micro-seconds.", counter );
dprint(1, 10, C_WHITE, "Delta t (b-a) : %lu micro-seconds.", (b-a) );
dupdate();
getkey();
return 1;
}
Par contre, si je descends la résolution en dessous de 5µs pour la résolution, ça coince (le programme tourne en boucle sans faire les opérations d'affichage.
En fait, j'ai deux affichages (sauf erreur de compréhension de ma part, ce qui n'est pas exclu) :
- le premier, "counter" qui me donne le temps total entre timer_start() et timer_stop()
- et le second "delta (a-b)" qui retire le temps des opérations timer_start() et timer_stop()
Comme résultat, j'obtiens grosso modo 5µs de durée pour le couple timer_start() + timer_stop() et 7828µs pour la réalisation de 100 boucles (dclear/dprint/dupdate), soit ~78µs par tour.
Est-ce que cela vous parait plausible ?
Merci.
Sly
Citer : Posté le 17/01/2022 22:08 | #
Pourquoi ne pas utiliser la libprof ? C’est conçu pour
Citer : Posté le 17/01/2022 22:13 | #
Juste parce que je connaissais pas libprof
du coup je vais regarder ;-)
mais en fait j'ai l'impression que mon algo ne fonctionne pas, car si j'accélère la calculatrice avec Ptune3, le temps de la boucle augmente ...
pas très logique.
Citer : Posté le 17/01/2022 22:21 | #
Tu as plusieurs problèmes avec ce code. D'abord, un uint16_t c'est clairement trop petit ; ça ne te donne que 65.536 ms de temps de mesure, et ta boucle est beaucoup plus longue que ça. Donc les résultats sont clairement faux, tout est violemment réduit modulo.
Ensuite, le principe même de ta mesure a un problème fondamental. Ton incrémentation du compteur se fait par le biais d'un callback de timer, ce qui veut dire que toutes les 5 µs le processeur doit interrompre ton code pour émettre une interruption, gint doit traiter l'interruption, interagir avec le timer, et appeler ton callback qui incrémente la variable. Plus la fréquence de la mesure est élevée, plus ce travail prélève du temps sur le programme et déforme la mesure.
La raison pour laquelle ça freeze si tu mets moins c'est parce qu'en 4 µs le programme n'a pas le temps de traiter entièrement une interruption, et donc 4 µs après ton timer_start(), le programme se fait interrompre, suit la procédure que j'ai décrite et incrémente le compteur ; mais une fois la procédure terminée 4 µs se sont de nouveau écoulées, si bien que le même traitement redémarre immédiatement et ton programme ne peut jamais reprendre. Dans gintctl je mesure qu'on peut traiter environ 280'000 interruptions par secondes, ce qui revient à peu près à ça. Note que 4 µs ça ne fait que 500 cycles processeur, donc pas tant d'instructions que ça, c'est une échelle très courte.
Si 4 µs représente la fréquence à laquelle le traitement des interruptions occupe 100% du processeur, en te mettant à 5 µs tu te places volontairement le plus proche possible de ça, c'est-à-dire que dans ton programme le traitement des interruptions occupe certainement plus de 50-80% du temps de processeur, ce qui fausse complètement ta mesure encore une fois.
Si tu accélères avec Ptune3, le temps de la boucle change, et comme tout est réduit modulo 65.536 ms extrêmement agressivement tu as un résultat presque aléatoire.
-
Comme Dark Storm l'a indiqué, il existe une bibliothèque pour faire des mesures de performances avec gint, libprof. La différence entre libprof et ton code ici c'est que libprof va directement lire le compteur du timer et n'active jamais les interruptions. Ton idée générale d'utiliser un timer est tout à fait exacte ; il aurait fallu que tu saches que le compteur interne existe (et comment l'interpréter) pour avoir une mesure raisonnable.
Les mesures de libprof sont précises à un niveau inférieur à la micro-seconde, à tel point qu'avec un peu de répétition on peut mesurer les cycles d'instructions individuellement, ce dont j'admets être assez fier
Pour ce qui est de ton code, il faut savoir que sur la Graph 90+E le dessin prend du temps, beaucoup de temps par rapport au reste. C'est parce qu'il y a 80'000 pixels et 170 kio de données graphiques pour former une image (396×224 pixels de 16 bits chacun). Je t'invite à regarder les ordres de grandeur pour te faire une idée.
Ici, ta boucle prend 14 ms environ. (C'est même pas la peine de le répéter... la précision est largement suffisante !) Pour effacer la VRAM avec une boucle, il faudrait ~6 ms, mais dclear() utilise le DMA et termine en 2.5 ms. dupdate() est limité par la vitesse de l'écran et prend toujours 11 ms (*). Le texte occupe peu de surface, c'est négligeable.
Tu peux faire le même test avec libprof comme ceci :
prof_enter(ctx);
dclear(C_WHITE);
dprint(etc);
dupdate();
prof_leave(ctx);
int time_us = prof_time(ctx);
Ou dans le cas où le code à exécuter est assez simple pour passer en argument d'une macro :
dclear(C_WHITE);
dprint(etc);
dupdate();
});
(*) L'arnaque avec dupdate() c'est que dupdate() lance le DMA pour démarrer la copie des données graphiques de la VRAM vers l'écran, mais n'attend pas qu'elle se termine. Le transfert se fait plus ou moins en parallèle du code qui suit. Donc attention au piège : dans les exemples que j'ai mis dupdate() ne sera pas compté, et en plus si tu essaies de mesurer les performances du code juste après un dupdate() le temps de copie peut s'en mêler.
En général on mesure le temps du rendu sans le dupdate() et c'est tout (puisque c'est ça le plus long) et ça se passe bien.
-
Quitte à faire de la pub, je me permets de lier ce topic, qui contient définitivement quelques techniques utiles pour toi
Citer : Posté le 17/01/2022 22:39 | #
Super, merci pour les explications, c'est parfaitement clair et je supputais une ânerie de ce genre.
Sur les bons conseils de Dark Storm, j'ai fait une install de LibProf et effectivement ca sert à rien de réinventer l'eau chaude
Je note pour la subtilité de dupdate().
Bon a priori ce qui m'intéresse c'est justement avant le dupdate(); je vais considérer que le dupdate() sera un offset de temps similaire à chaque exécution.
Je vais encore me coucher moins bête ce soir
Merci aussi pour le lien en fin de post, il y a plein de choses intéressantes en effet.