Club robotique de Sophia-Antipolis

Accueil > POBOTpedia > Programmation > Snippets > Optimisation de code

Optimisation de code

mercredi 18 février 2009, par Julien H.

Enfin un petit article simple sur l’optimisation de code. Alors bien sûr, il n’est pas question de parler ici d’assembleur contre langage C ou de Java contre C++, car tout le monde sait bien que le C bien compilé est plus rapide que du code en assembleur et que Java est plus performant que C++.

On va donc rester dans un même langage (ici ce sera le C) et montrer par l’exemple et chronomètre en main comment optimiser le temps de calcul de routines élémentaires.

Notre matériel est simple :
- un microcontrôleur (ici une carte mySmartControl à base d’AVR ATmega8)
- un compilateur C (ici avr-gcc et WinAVR)
- un analyseur logique (ici l’excellent Logic de Saleae) pour mesurer les progrès réalisés

Le premier exemple est très classique : prenons un développeur de robot lambda, plus doué avec Linux qu’avec un système temps réel. Il doit allumer et éteindre tout un tas de leds. Plutôt que d’écrire 20 fois la même ligne, il va... utiliser une boucle et une variable bien sûr !

Voici le code qu’il écrirait :


//----- Include Files ---------------------------------------------------------
#include "avr/io.h" // include I/O definitions (port names, pin names, etc)
#include "avr/interrupt.h" // include interrupt support

#include "global.h" // include our global settings
#include "timer.h" // include timer function library (timing, PWM, etc)

int main(void)

timerInit() ;
DDRC = 0xFF ; // port C en sortie

while (1)

int i=0 ; // la variable sans laquelle l’informaticien est perdu

// lancer un front haut pour démarrer le chrono
sbi(PORTC,0) ;

// allumer toute la série de leds
for (i=1 ;i<5 ;i++)

sbi(PORTC,i) ;

// les éteindre dans le sens contraire

for (i=4 ;i>0 ;i—)

cbi(PORTC,i) ;

// un front bas pour éteindre le chrono
cbi(PORTC,0) ;

delay_ms(1) ;

return 0 ;

Tout est correct, ça compile et ça prend pas beaucoup de place. On charge et on regarde combien ça prend de temps...

PNG - 55.2 ko
Code avec for : 63 µs

Argh ! 65 µs c’est quand même beaucoup pour allumer 5 leds. En effet, la carte utilisée a un quartz à 3.6864 MHz (millions de tops d’horloge par secondes) et comme on utilise un microcontrôleur performant, il lui faut juste 2 tops d’horloge pour changer un port d’entrée/sortie de niveau, soit
0,54 µs (microsecondes ou millionièmes de secondes).

Pour optimiser, il suffit d’enlever la boucle et d’utiliser directement les instructions "sbi" et "cbi" (pour "Set BIt" et "Clear BIt") qui correspondent à deux instructions de base du jeu réduit de cette famille (RISC ou Reduced Instruction Set Computer).

Le code devient :


//----- Include Files ---------------------------------------------------------
#include "avr/io.h" // include I/O definitions (port names, pin names, etc)
#include "avr/interrupt.h" // include interrupt support

#include "global.h" // include our global settings
#include "timer.h" // include timer function library (timing, PWM, etc)

int main(void)

timerInit() ;
DDRC = 0xFF ; // port C en sortie

while (1)

// lancer un front haut pour démarrer le chrono
sbi(PORTC,0) ;

// allumer toute la série de leds
sbi(PORTC,1) ;
sbi(PORTC,2) ;
sbi(PORTC,3) ;
sbi(PORTC,4) ;

// les éteindre dans le sens contraire
cbi(PORTC,4) ;
cbi(PORTC,3) ;
cbi(PORTC,2) ;
cbi(PORTC,1) ;

// un front bas pour éteindre le chrono
cbi(PORTC,0) ;

delay_ms(1) ;

return 0 ;

Alors oui ça peut prendre plus de place quand on a 100 leds à allumer ou éteindre, mais le résultat est là :

PNG - 48 ko
Code optimisé : 4.8 µs

Oui, pas plus de 4.8 µs. D’ailleurs c’est logique, pas de superflu : 8 instructions à 0.54 µs, cela fait 4.32 µs + la demi microseconde pour redescendre le signal de notre témoin pour la mesure (première ligne du chronogramme, patte 0 du port C dans le code).

Ok... hmm... ah oui... j’en étais sûr, vous vous dites que s’il y a 100 leds donc 100 lignes pour allumer les leds, on va utiliser une fonction cachée tout en bas du fichier source, pour garder un code "propre", du genre :


//----- Include Files ---------------------------------------------------------
#include "avr/io.h" // include I/O definitions (port names, pin names, etc)
#include "avr/interrupt.h" // include interrupt support

#include "global.h" // include our global settings
#include "timer.h" // include timer function library (timing, PWM, etc)

void allumer(void) ;

void eteindre(void) ;

int main(void)

timerInit() ;
DDRC = 0xFF ; // port C en sortie

while (1)

// lancer un front haut pour démarrer le chrono
sbi(PORTC,0) ;

// allumer toute la série de leds
allumer() ;

// les éteindre dans le sens contraire
eteindre() ;

// un front bas pour éteindre le chrono
cbi(PORTC,0) ;

delay_ms(1) ;

return 0 ;

void allumer(void)

// pourrait faire 100 lignes de ce genre :
sbi(PORTC,1) ;
sbi(PORTC,2) ;
sbi(PORTC,3) ;
sbi(PORTC,4) ;
sbi(PORTC,4) ;

void eteindre(void)

cbi(PORTC,4) ;
cbi(PORTC,3) ;
cbi(PORTC,2) ;
cbi(PORTC,1) ;

Regardons le résultat :

PNG - 53.1 ko
Code avec appels de fonctions : 9 µs

Et oui, 9 µs. On voit bien où le temps est perdu : à l’appel de la fonction et au retour (espaces allongés entre la première et la seconde ligne et écart plus grand au milieu de la dernière ligne, entre les deux appels). En effet, un "contexte" (les registres de mémoire, etc.., voir le détail dans un ouvrage spécialisé sur les µC Atmel) est sauvegardé, il faut empiler et dépiler les valeurs à l’appel et au retour.

Mais rien n’est perdu, une instruction spécifique existe : "inline" qui permet avant compilation de placer tout le code contenu dans la fonction à chaque emplacement qui appelle la fonction inlinée (comme les macros #define).

Voici le code :


//----- Include Files ---------------------------------------------------------
#include "avr/io.h" // include I/O definitions (port names, pin names, etc)
#include "avr/interrupt.h" // include interrupt support

#include "global.h" // include our global settings
#include "timer.h" // include timer function library (timing, PWM, etc)

void inline allumer(void) ;

void inline eteindre(void) ;

int main(void)

timerInit() ;
DDRC = 0xFF ; // port C en sortie

while (1)

// lancer un front haut pour démarrer le chrono
sbi(PORTC,0) ;

// allumer toute la série de leds
allumer() ;

// les éteindre dans le sens contraire
eteindre() ;

// un front bas pour éteindre le chrono
cbi(PORTC,0) ;

delay_ms(1) ;

return 0 ;

void inline allumer(void)

// pourrait faire 100 lignes de ce genre :
sbi(PORTC,1) ;
sbi(PORTC,2) ;
sbi(PORTC,3) ;
sbi(PORTC,4) ;

void inline eteindre(void)

cbi(PORTC,4) ;
cbi(PORTC,3) ;
cbi(PORTC,2) ;
cbi(PORTC,1) ;


Et voici le résultat :

PNG - 56.8 ko
Code avec inline : 5 µs

On retrouve bien nos 5 microsecondes !

N’hésitez pas à me contacter pour me faire part de vos propres optimisations et pour compléter cet article.

Vos commentaires

  • Le 6 mars 2009 à 15:19, par jbe En réponse à : Optimisation de code

    Bravo, une manipulation sympa.

    Un soucis, l’instruction inline va donc rajouter n fois le bloc lorsqu’il sera appelé, et donc rajouter n fois des lignes dans le programme compilé.

    Mais c’est comme d’habitude, un compromis entre temps d’exécution et taille mémoire.

    De plus, pourriez vous faire cette même manipulation en utilisant la bibliothèque sbit.h.
    Une utilisation de cette bibliothèque est écrite ci dessous.

    Ce qui m’intéresse, c’est l’utilisation de sbit , par rapport a cbi et sbi.

    /** Algo Studio - SansNom **/
    #define F_CPU 1000000UL
    #include

    <inttypes.h>

    #include io.h>
    #include interrupt.h>
    #include delay.h>
    #include sbit.h>

    #define bp0 SBIT (PIND, 0)
    #define bp1 SBIT (PIND, 1)
    #define bp2 SBIT (PIND, 2)
    #define bp3 SBIT (PIND, 3)

    #define LED0 SBIT (PORTB, 0)
    #define LED1 SBIT (PORTB, 1)
    #define LED2 SBIT (PORTB, 2)
    #define LED3 SBIT (PORTB, 3)

    /*** le code exécutable commence ici ***/
    int main(void) // 5 `Debut Prog` (RESET)

    DDRD =0b00000000 ;
    DDRB= 0b11111111 ;
    do // 2 `Début d’itération perpétuelle`

    if (bp0 == 1)

    LED0 = 1 ;

    else

    LED0 = 0 ;

    // 3 `Fin d’itération perpétuelle`
    while(1) ;
    return 0 ; // exigé par le compilateur WinAVR
    // Fin de la fonction "main"
    /* Fin du texte */

    Cordialement

    jbe

    • Le 7 mars 2009 à 10:48, par Julien H. En réponse à : Optimisation de code

      Je ne suis pas d’accord avec la première conclusion : ça rajoute du code dans le programme à compiler et pas forcément das le programme compilé. C’est-à-dire que la vision "naïve" (au sens premier du mot, pas critique) est que le contenu de la fonction "inline" est copié partout où c’est appelé, mais en fait les compilateurs modernes (dont avr-gcc) vont détecter qu’il s’agit du même code donc vont optimiser le code binaire (et même le code assembleur) en faisant de la mutualisation. Donc la taille de programme n’en souffre pas. Je ferai un test pour le vérifier, mais c’est déjà le cas pour un code sans inline. Si vous écrivez 10 fois une séquence d’opérations identiques, le compilateur ne le transcrit qu’une fois en code assembleur et fait des appels (qui ne sont pas aussi coûteux qu’un appel de fonction C puisque pas de pile (stack) à gérer. Si le coût est quand même trop grand (2 appels de pointeurs), on peut régler ce paramètre avec les options de compilation -O1, -O2, -O3, -Os (s = size).

      Je ferai le test avec SBIT.

    • Le 7 mars 2009 à 17:14, par ? En réponse à : Optimisation de code

      Julien,

      Tu as trop parlé ou pas assez.

      1 Qu’est ce que c’est cette optimisation du programme compilé.
      Aurais tu quelques exemples ?

      2 Sais tu comment s’en sort notre compilateur préféré, AVR GCC.

      3 Il faudrait monter une manipulation pour comparer différent compilateur. Il y a par exemple Code Vision, ICC de chez Image Craft, Keil C.

    • Le 7 mars 2009 à 17:22, par ? En réponse à : Optimisation de code

      Utilisation de SBIT

      Bien, j’ai fait quelques essais de mon coté.
      J’ai compilé le programme ci dessus, avec l’option s.
      Le programme assembleur utilise alors les instructions sbit et cbit.

      Donc le code est optimisé au maximum.

      L’utilisation de sbit.h est à généraliser de toute urgence.

      JBE

    • Le 8 mars 2009 à 19:17, par Julien H. En réponse à : Optimisation de code

      1) il y a plusieurs options à la compilation. Dans un makefile standard (livré avec WinAVR ou AVRlib, ce sont les options "#compiler flags".

      Exemple :

      CPFLAGS        = -g -Os -Wall -Wstrict-prototypes -I$(AVRLIB) -Wa,-ahlms=$(<:.c=.lst)

      "-Os" signifie qu’il optimise la taille du programme .hex à transférer dans le chip

      "-Wall" signifie qu’il montre tous les warnings.

      Je vous laisse consulter la doc pour le reste :)

      2) Il s’en sort bien. Attention à certaines optimisations (-O3) qui supprime des boucles vides qui servent d’attente en fonction du test effectué par le while()

      3) les autres compilateurs sont payants, seules les écoles peuvent se les payer, pas de pauvres amateurs comme nous. Donc pas de test de comparaison à moins de recevoir une aide financière.

    • Le 8 mars 2009 à 19:31, par Julien H. En réponse à : Optimisation de code

      Attention : "sbit" et "cbit" ne sont pas des instructions. SBI et CBI sont des instructions.

      On ne peut pas dire une optimisation "au maximum", il faut dire si on optimise en taille ou en mémoire ou en temps d’exécution.

    • Le 11 mars 2009 à 15:02, par jbe En réponse à : Optimisation de code

      Ces donc le rôle de ces différentes options de compilation :
      taille, place mémoire, ou temps d’exécution,

      que l’on a dans AVR GCC.

      Si tu as le numéro de la page de la doc AVR GCC où ceci est expliqué, je suis preneur.

      Cordialement

      JBE

    • Le 11 mars 2009 à 16:31, par Julien H. En réponse à : Optimisation de code

      Oups, AVR-GCC a une doc ? En fait je ne l’ai jamais lue, je réutilise ce que je connais "de loin" dans les compilateurs similaires. Mais ce serait intéressant de connaitre le détail, je te l’accorde.

    • Le 30 avril 2011 à 02:16, par ? En réponse à : Optimisation de code

      Pas mal du tout cette page ! J’en ai appris des choses.
      Donc, les appels de fonctions sont chères en temps à cause de la pile.
      Mais comment sont gérées les variables statiques ?
      J’ai des fonctions prenant beaucoup de paramètres (genre 30 bytes pour un petit AVR) et je me rends compte que c’est très lent. Comment améliorer cela ? Variables statiques ? Plusieurs appel de fonction plus petites ? Pas d’appel de fonction ?
      Ces fonctions rendent mon code c plus lisible. Si j’utilise "inline", les appels seront supprimés et mon code tournera plus vite ?

      Merci pour tes réponses si tu passes par ici :-)

    • Le 3 mai 2011 à 19:48, par Julien H. En réponse à : Optimisation de code

      Bonjour,

      Les variables statiques sont mises dans un emplacement mémoire fixe qui ne change pas. Chaque écriture ou lecture se fait à la même adresse.

      30 octets c’est beaucoup trop. Fais une structure (struct) pour couvrir tous tes paramètres, et passe la structure en paramètre. Fais des tests :)

      Oui, avec inline ça tournera plus vite.

    Répondre à ce message

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici
  • Ce formulaire accepte les raccourcis SPIP [->url] {{gras}} {italique} <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.

Ajouter un document