Skyduino:~#
Articles
arduino, C/C++, Corrigé, programmation, tutoriel

Optimiser son code source C pour avr-gcc

Bonjour tout le monde !

Après un long silence radio me revoilà avec un article qui va faire plaisir à beaucoup de monde j’en suis sûr 😉
Histoire de me faire pardonner je vous ai écris un vrai roman 🙂

Le thème du jour : comment optimiser son code source C (et par extension C++) pour le compilateur avr-gcc.

La question m’ayant été posée très souvent j’ai décidé d’en faire un article !
Il n’y a pas vraiment d’ordre dans les différents points, c’est un peu en vrac, désolé.

Remarque : cet article est basé sur la note d’application d’ATMEL AVR035 « Efficient C Coding for AVR » (disponible ici : http://www.atmel.com/Images/doc1497.pdf) ainsi que sur mes expériences personnelles et sur mes cours d’IUT.

Vous êtes prêts ? C’est parti !

Edit :
Jordan (le fou furieux qui prend le temps de corriger mes articles :)) m’a fait remarquer que certains points de l’article pouvaient porter à confusion.
Je vais donc tacher d’éclaircir les choses avant de commencer 😉

Il n’y a aucun standard en C qui définit que tel type doit avoir telle taille, par conséquents tout peut (et va) changer entre deux plateformes.
Cet article part du principe que vous codez sur un microcontrôleur d’architecture « AVR-8 » (AVR 8 bits) avec le compilateur AVR-GCC.

Si on suit les logiques du C, le int devrait être le type le plus facile à manipuler par le cpu.
Sur un AVR-8 le int devrait donc logiquement être codé sur 8 bits, en réalité il est codé sur 16 bits (pour des raisons évidentes de compatibilité) …
Dans l’article je conseille d’utiliser les « types dures » (stdint.h), c’est justement pour éviter ce genre de questions métaphysique quand à la taille des types en mémoire…

Ps: en plus on gagne en portabilité, le code travaillant toujours avec les mêmes types / tailles de variables quelque soit la plateforme.
Note: l’optimisation faite pour le compilateur x sur une plateforme y n’est pas forcément valable avec un autre compilateur ou sur une autre plateforme !

1 – Sans framework, tu coderas

Ça peut paraitre un peu brutale comme premier point mais c’est ainsi que le monde est fait.
Oubliez le framework Arduino ou tout autre framework du même style, si vous voulez un code propre et léger il vous faudra utiliser les fonctions bas niveaux et les registres !

Pourquoi le framework Arduino est il à proscrire ?
– il est conçu pour être simple d’utilisation pour les non initiés à la programmation, mais pas pour être optimisé, loin de la !
– il demande BEAUCOUP de ressources pour pas grand choses (digitalWrite() par exemple requière un tableau en PROGMEM de +440 octets, 6 appels de fonctions et une ribambelle de tests)
– il est horriblement lent (cf exemple au dessus)

Utiliser le framework arduino pour une application qui se doit légère et optimisée c’est comme vouloir opérer un patient avec une hache de bucheron émoussée.
Certaines personnes ont essayé durant le moyen age, le résultat n’était pas franchement super 😉

2 – Le datasheet, tu liras

Le plus grand amis du développeur sur systèmes embarqués c’est le datasheet (et la cafetière, mais bon ça c’est une autre histoire).
Si vous voulez faire quelque chose de propre il FAUT avoir lu le datasheet !

Le datasheet c’est le manuel qui contient absolument TOUT ce qu’il faut savoir sur le composant sur lequel on travaille.

Ne pas lire le datasheet, ou le lire en diagonal implique à coup sûr :
– une perte de temps conséquente à faire du debug inutile
– des bugs incompréhensibles et souvent au moment le plus critique (cf loi de Murphy)
– des prises de tête sur des points on ne peut plus simple

3 – Les registres, tu utiliseras

Comme je le disais au point n°1 il ne faut pas utiliser de framework, soit, mais comment faire pour faire clignoter une led par exemple !?

La réponse est toute simple et se trouve dans le datasheet (Read The Fucking Manual !) : les SFR, aka « Special Function Registers », soit en bon français les « registres à fonction spéciale ».
En gros il s’agit de variables se situant à des adresses mémoire codées en dur dans le microcontrôleur et ayant des fonctions hardware bien précises.

L’exemple le plus simple qu’il soit, blink :
En arduino :

void setup() {
  pinMode(13, OUTPUT);
}

void loop() {
  digitalWrite(13, HIGH);
  delay(500);
  digitalWrite(13, LOW);
  delay(500);
}

En avr-c :

#include <avr/io.h>     // .h contenant les registres SFR
#include <util/delay.h> // .h contenant les fonctions de délai

void main(void) {
  DDRB |= (1 << PIN5); // pinMode OUTPUT

  for(;;) { // Équivalent loop()
    PORTB |= (1 << PIN5); // digitalWrite HIGH
    _delay_ms(500);
    PORTB &= ~(1 << PIN5); // digitalWrite LOW
    _delay_ms(500);
  }
}

Ça peut paraitre compliqué mais ça ne l’est pas 😉

Dans cet exemple j’utilise DDRB qui est le registre de « mode » du port B, auquel j’affecte un « 1 » logique au bit correspondant à la broche 5 (= sortie).
Ensuite je fais de même avec le registre de « sortie » du port B, pour faire clignoter la led.

Le gros avantage est que ce code (bas-niveau) est 5 voir même 10 fois plus léger qu’un seul digitalWrite() !
On voit tout de suite les possibilités qui s’offre à nous en terme de place !

Bonus, quelques macro utiles :
(1 << n) = 1 décalé de n bits vers la gauche (ex: (1 < ceci est un "bitmask", contenant 1 bit de masque
variable |= bitmask -> permet de mettre un ou plusieurs bits à "1" logique
variable &= ~bitmask -> permet de mettre un ou plusieurs bits à "0" logique
variable & bitmask -> retourne true si au moins un des bits du bitmask est à "1" logique

Plus d’info sur le « bitwise » (= manipulation de bits) :
http://en.wikipedia.org/wiki/Bitwise_operation

Les tables de conversions broches Arduino vers ports / broches AVR :
Uno : http://arduino.cc/en/Hacking/PinMapping?from=Main.PinMapping
Mega : http://arduino.cc/en/Hacking/PinMapping2560

Remarque : PAS DE POINTEURS SUR REGISTRES, sauf si vous voulez que je tue un mignon petit chaton 🙂
Accédez toujours aux registres de manière directe, le compilateur pourra optimiser le code de manière beaucoup plus efficace.
(On me dit dans l’oreillette que tout le framework Arduino est basé sur des pointeurs sur registres, ô les vilains !)

4 – Ton code, tu factoriseras

Factoriser autant que possible le code est un point clef !
La règle est simple, si un morceaux de code se voit copié / collé c’est que le code est FAUX !
Un bon code est un code SANS redondance dans les instructions.

Pour factoriser un code il y a énormément de solutions, les plus simple étant :
– de faire des fonctions
– d’utiliser des boucles

Pourquoi faire :

PORTB |= (1 << PIN1);
PORTB |= (1 << PIN2);
PORTB |= (1 << PIN3);
PORTB |= (1 << PIN4);

Quand on peut faire :

PORTB |= (1 << PIN1) | (1 << PIN2) | (1 << PIN3) | (1 << PIN4);

Pourquoi faire :

tab[0] = 0;
tab[1] = 1;
// ...
tab[12] = 12;

Quand on peut faire :

for(uint8_t i = 0; i < 13; ++i)
  tab[i] = i;

Dans le même genre, pourquoi déclarer des variables v1, v2, v3, … quand on peut faire un tableau v[3] …
Etc etc, la liste d’exemples est sans fin.

5 – Les define et les macro, tu n’utiliseras pas

Ça peut paraitre complétement fou mais ça ne l’est pas, je vous explique.

Un #define ne fait que remplacer une chaine de caractères par une autre AVANT la compilation.
Il n’y a donc aucun typage ni vérification avec un #define !

Encore pire : les macro !
Une macro c’est une fonction dont le code est inséré dans le reste du code avant la compilation.

Exemple :

#define MAX(a, b) (((a) > (b)) ? (a) : (b))

int r = MAX(1, 5);
// r = 5

// équivalent à :
int r = (1 > 5) ? 1 : 5;

Problème, si un des membres n’est pas « passif » :

#define MAX(a, b) (((a) > (b)) ? (a) : (b))

int z = 5;
int r = MAX(1, ++z);
// r = 7

// Pourquoi ?
// Tout simplement parce que l'équivalent post-préprocesseur est le suivant :
int r = (1 > ++z) ? 1 : ++z;

Soyez extrêmement vigilant avec les macro et les define !
La moindre faute d’inattention peut donner des erreurs qui ne générerons aucun warning.
Par contre à l’exécution tout va vous péter à la tronche !

Encore plus vicieux, les parenthèses soit disant « inutiles » dans une macro :

#define DIV(a, b) (a / b)

float r = DIV(1.0, 1 + 5);
// r = 6.0

// Pourquoi ?
// Tout simplement parce que l'équivalent post-préprocesseur est le suivant :
float r = 1.0 / 1 + 5;

// avec les parenthèses cette fois :
#define DIV(a, b) ((a) / (b))

float r = DIV(1.0, 1 + 5);
// r = 0,16666666666666666666666666666667

La solution : les « const » et les fonctions « inline ».

Les variables « const » sont des variables avec une valeur fixée qui ne peut pas être modifié.
Elles sont optimisées par le compilateur de la même façon qu’un #define mais avec tout les avantages d’une variables classique (typage, warning, …).

De même les fonctions « inline » sont des fonctions tout ce qui a de plus classique mais suivant le même principe que les macros.
Le code de la fonction « inline » est inséré dans le reste de la fonction parent lors de la compilation.
Gros avantage : les paramètres ne sont traités qu’une fois, comme une fonction classique.

Exemple :

inline int MAX(int a, int b) {
  return (a > b) ? a : b;
}

int z = 5;
int r = MAX(1, ++z);
// r = 6

6 – Les makefiles, tu utiliseras et ton compilateur, tu configureras

Utiliser un makefile pour compiler ses codes sources apporte des avantages non négligeable comparé à une compilation « à la main » en ligne de commande :
– possibilité de compiler / recompiler / nettoyer le projet en une commande
– possibilité d’appeler des scripts (si besoin) pour finaliser la compilation
– possibilité de configurer proprement chaque étape de la compilation

Parmi les indispensables à une bonne compilation se trouvent les paramètres suivant :
– pour gcc (le compilateur) : -Wall -Os -ffunction-sections
– pour ld (le linker) : -Wl,-gc-sections

-Wall active tous les warnings, partez du principe que s’il y a un warning dans 99.9% du temps il y a une erreur qui ce cache derrière.
-Os demande au compilateur d’optimiser le code « pour la taille » (Optimize for Size), compiler et tester un code sans optimisation une fois le debug finit est une grave erreur !
(Un code qui marche parfaitement en debug peut ne plus du tout fonctionner en release avec optimisation !)
-ffunction-sections demande au compilateur de placer chaque fonction dans une section séparée, cela permet d’optimiser la taille du code lors de la phase de « link »

-Wl,-gc-sections demande au linker (l’éditeur de liens) de supprimer purement et simplement les sections inutilisées.
Comme le compilateur a placé chaque fonction dans une section séparée (cf -ffunction-sections) le linker va pouvoir faire le ménage de la manière la plus optimisée qui soit.

Avec ce simple lot de paramètres vous pouvez réduire drastiquement la taille du code finale !

7 – Commenter le code, tu devras

Par pitié commentez systématiquement vos code source !
Ce qui parait limpide sur l’instant peut rapidement devenir un méli-mélo indéchiffrable quelques jours plus tard âpres avoir laissé le projet de côté.
Pensez aussi aux pauvres développeurs qui vont vouloir relire votre code !

Si vous commentez correctement votre code, puis que vous le relisez à tête reposée vous éviterez un nombre considérable de fautes d’inattention.

Une méthode loufoque mais très efficace pour éviter ce genre de problèmes est la méthode dite « du canard en plastique » :
http://fr.wikipedia.org/wiki/Méthode_du_canard_en_plastique

Et si vous voulez vraiment faire les choses bien, commentez vos codes suivant la syntaxe qui fait office de convention dans le domaine de la programmation : doxygen.
http://www.stack.nl/~dimitri/doxygen/

En bonus vous aurez une jolie documentation au format html, latex et man en une ligne de commande !

8 – Avant de coder, ton cerveau, tu utiliseras

Tout bon programmeur qui se respecte commence par prendre un crayon et une feuille de papier.
Investissez dans un lot de cahiers de brouillon 😉

Quand je débute un projet je commence toujours par mettre sur papier : description globale, contraintes, finalité, diagramme de fonctionnement, TOUT.
En réfléchissant avant de coder la moindre ligne de code on peut déjà avoir une vue d’ensemble de ce qu’il faut faire et de comment le faire.

Pour vous donnez une idée, dans mon IUT les DS de programmation se font sur feuilles UNIQUEMENT, le compilateur c’est nous, le debuggeur aussi.

9 – Les while(1), tu éviteras

Honte sur tous ceux qui font des while(1) {} !
En C/C++ une boucle infini se code avec un for(;;) {} !

Si vous ne suivez pas cette règle de base le compilateur vous tapera sur les doigts avec un gros warning : « Condition always true ».
Avec un for(;;) il ne dira rien, c’est une boucle infini point final.

Bonus, d’un point de vue optimisation un for(;;) ne demande qu’une seule instruction (jump), alors qu’un while(1) en demande 2 fois plus (test + jump) !

10 – Do {} while() autant que possible, tu utilisera

En règle générale une boucle do {} while() est plus facilement optimisée par le compilateur qu’une boucle for() ou while() simple.

11 – ++i et non i++ dés que possible, tu utiliseras

La différence entre ++i et i++ est subtile, mais elle peut vous faire gagner de la place et du temps !
++i incrémente i puis retourne la valeur, alors que i++ incrémente i, stock la nouvelle valeur dans la pile, effectue l’opération avec l’ancienne valeur de i, puis récupère la nouvelle valeur de i depuis la pile.

12 – Les boucles décroissantes, le compilateur appréciera !

Si vous faite une boucle se terminant lorsque la valeur testé = 0 le compilateur pourra optimiser son code !

Tout les CPU ont un registre de statut (SREG pour les AVR) qui permet de connaitre pleins de chose quand à l’opération précédente.
Valeur négative, égale à zéro, overflow lors du calcul, retenu, …

Si vous faites une boucle décroissante le compilateur utilisera le « flag » zéro pour tester la fin de boucle, ce qui vous fera gagner une instruction.

Attention, pour cela il faudra obligatoirement faire une boucle du style :

do {
  // ...
} while(--i);

13 – Aucun appel de fonctions dans une interruption, tu ferras

A moins que ça ne soit un cas de force majeur et que l’humanité se soit transformé en zombies, n’appelez pas de sous-fonctions dans une fonction d’interruption !
Une fonction d’interruption ce doit d’être la plus rapide possible, appeler une sous-fonction prend du temps et le temps c’est précieux !

14 – Les variables globales à la déclaration, tu initialiseras

Pas d’initialisation d’une variable globale dans le corps d’une fonction, une variable globale doit être initialisé lors de sa déclaration.

Avant d’appeler la fonction main() le compilateur exécutera un « boot code » copiant les zones mémoire contenant les variables globales en RAM.
Autant initialiser les variables lors de leurs copie en RAM, cela ne prend pas plus de temps mais évitera une assignation en plus dans le corps d’une fonction.

15 – Les variables globales, tu éviteras

C’est un peu contradictoire avec le point juste au dessus mais accéder à une variable globale demande deux fois plus de temps que d’accéder à une variable locale !
Quand c’est possible toujours utiliser des variables locales !

Remarque : Parfois une variable globale est nécessaire, voir même plusieurs.
Si c’est le cas le mieux est de placer TOUTES les variables globales dans une structure.

Pourquoi ?
Le compilateur dispose de registres spéciaux (X et Z dans le cas d’un AVR), permettant de manipuler des adresses mémoire de manière efficace.
Si vous utilisez une structure, le compilateur gardera dans un de ces registres l’adresse de la structure puis accédera aux champs de la structure via un offset.
Cela permet d’accéder très rapidement à n’importe quelle variable globale !

Même principe pour les tableaux déclarés en variable globale.
Si vous gardez un pointeur vers le début du tableau dans une variable locale vos calculs seront accéléré, tout ce fera via un couple adresse de base + offset !

Autre solution qui est moins couteuse qu’une variable globale : une variable « static ».
Elles se déclarent comme une variable locale classique mais avec le mot clef « static » devant le type, cela a pour effet de rendre la valeur persistante comme une variable globale.
Le temps d’accès étant moins long que pour une « vrai » variable globale c’est une des meilleurs solutions si on a un besoin (ponctuelle) de variables globale.

16 – Le type des variables, intelligemment tu choisiras

Est-ce que cela vous viendriez à l’esprit de tuer une mouche avec un tank ? Non !
Alors pourquoi utiliser un int pour faire une boucle de 0 à 10 !

Choisissez vos types suivant la valeur maximum possible de la variable !
Si la valeur ne dépasse pas 255 utilisez un char non signé, etc …

La librairie stdint.h contient les déclarations des typages « dur », si vous voulez faire quelque chose de portable et propre utilisez les systématiquement !
Il existe :
– uint8_t : valeur 8 bits non signé, 0 à 255
– int8_t : valeur 8 bits signé, -127 à 127
– uint16_t : valeur 16 bits non signé, 0 à 65535
– int16_t : valeur 16 bits signé, -32767 à 32767
– uint32_t : valeur 32 bits non signé, 0 à 4294967295
– int32_t : valeur 32 bits signé, -2147483647 à 2147483647

Plus le type utilisé sera petit, plus le code final sera léger, une opération sur un uint8_t demande 1 instruction, sur un uint16_t 2 instructions et sur un uint32_t 4 instructions.
Faite le bon choix suivant la variable !

17 – Le « scope » des variables, avec crainte tu surveilleras

Chaque variable ou fonction possède un « scope », c’est à dire une visibilité.
Il arrive que pour certaines fonctions on ait besoin d’une variable ou d’un tableau uniquement dans un cas précis.

Une variable locale n’est visible que dans le bloc {…} ou elle est déclarée, si vous n’avez pas besoin d’une variable dans un cas précis faites un bloc pour le cas en question !
Cela évitera une allocation de mémoire inutile !

Exemple :

void toto(char c) {
  char buf[20]; // pas bon

  if(c != '\0') {
    // utilisation de buf ...
  }
}

/* ----- */

void toto(char c) {

  if(c != '\0') {
     char buf[20]; // bon
    // utilisation de buf ...
  }
}

18 – Les bitmask, en locale tu manipuleras

Manipuler des bits est une opération très pratique, que ce soit avec des registres, des variables, etc …

Manipuler des bits avec une variable locale est très efficace (et rapide), de plus le compilateur pourra faire son optimisation comme il l’entends.
Manipuler des bits avec une variable globale … très mauvaise idée, croyez moi !

19 – Volatile et register, tu découvriras

Accéder à une variable en RAM demande du temps, c’est rapide certes mais pas autant que d’accéder à un registre de travail du CPU !
Le mot clef « register » est la pour ça !

Mettre le mot clef « register » devant les variables LOCALES que votre fonction va utiliser extrêmement souvent rendra son exécution bien plus rapide qu’avec une variable locale classique.

Attention : le nombre de registres de travail n’est pas illimité !
Sur un AVR il y a 32 registres de travail 8 bits, gardez cela en tête.

Le mot clef « volatile » est l’exact opposé de « register », il demande au compilateur de ne pas conserver la variable dans un registre de travail et de toujours remettre le résultat en RAM après chaque opération sur cette même variable.
Ce mot clef est OBLIGATOIRE pour toute variable accessible à la fois depuis une fonction « classique » et depuis une interruption.
Sans ce mot clef votre interruption ou votre fonction pourrait travailler avec des valeurs en RAM périmé et cela donne souvent des bugs impossible à résoudre sans un debuggeur « pas à pas ».

20 – Les constantes, en flash tu stockeras

La mémoire RAM est une denrée rare en programmation embarqué, gâcher 1Ko de RAM pour stocker 4-5 chaines de caractères CONSTANTES est un blasphème !

Toujours garder en flash les variables constante, pour cela (avec la avr-libc) il suffit d’ajouter le mot clef « PROGMEM » après le type de la variable et d’utiliser les fonctions pgm_read_xxx() pour récupérer les données par la suite.

(Les nouvelles versions du compilateurs GCC placent automatiquement en flash les variables const, malheureusement la version actuelle de avr-gcc n’as pas cette fonctionnalité)

Plus d’info sur PROGMEM ici :
http://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html

21 – La visibilité des variables / fonctions, par fichiers tu cloisonneras

Si une fonction ou une variable globale n’est utilisé QUE dans le fichier source en cours alors inutile de laisser le compilateur l’exporté pour que les autres fichiers sources puissent la voir !

Pour cela il suffit d’ajouter le mot clef « static » devant la fonction ou la variable globale, le compilateur n’exportera pas le « symbole » correspondant et cela lui permettra d’optimiser un peu plus le code.

Bonus: comme chaque fichier source ne voit pas les variables des autres fichiers sources avec le mot clef « static » vous pouvez déclarer plusieurs variables globales de même noms dans plusieurs fichiers sources différents sans créer d’erreur de redéfinition lors du link !
Mais gardez bien en tête qu’il s’agira de variables physiquement séparées (ne pas confondre avec le mot clef « extern »).

22 – Via une structure, aux registres non-mappés tu accéderas

Bon j’avoue c’est pas l’astuce d’optimisation la plus utile car les registres SFR non-mappé sur les AVR sont vraiment pas nombreux …
Sur un ARM par contre c’est très utilisé (librairie CMSIS par exemple), mais dans notre cas c’est pas super utile.

L’astuce consiste à déclarer une structure décrivant très exactement les registres SFR ciblés et de « caster » l’adresse du premier SFR en une structure du type défini préalablement.
Le compilateur peut ainsi faire de l’adressage indirecte (adresse de base + offset) ce qui est plus rapide que d’accéder directement à chaque champs un par un.

J’espère que cet article viendra en aide à ceux qui veulent faire une code AVR-C le plus léger possible.
Avec ce genre d’astuces vous pouvez faire des choses totalement dingue même avec les 4Ko de flash d’un ATtiny45.

Ps: je n’ai pas la science infuse et je ne suis pas (encore) développeur professionnel, donc si vous voyez des choses erronées dans mon article n’hésitez pas à me le faire savoir dans les commentaires 😉

Advertisements

Discussion

10 réflexions sur “Optimiser son code source C pour avr-gcc

  1. J’AIME !

    Publié par charles No | 26 novembre 2012, 21 h 52 min
  2. waaaaawwww grand merci

    Publié par raed | 27 novembre 2012, 16 h 09 min
  3. ta expliqué que dans l’utilisation des boucles infinie , de preférence d’utliser for (;;) au lieu de while(1) mais à mon avis que la boucle for utilise plus des registres que la boucle while est ce que vrais ou non

    Publié par raed | 27 novembre 2012, 16 h 14 min
    • Le for(;;) ne demande aucun registre.
      Il n’y a pas de pré-condition, de test ou d’incrémentation.

      En terme de code assembleur ça se traduit par un JMP = une seule instruction.
      C’est impossible de faire mieux qu’une instruction 😉

      Publié par skywodd | 27 novembre 2012, 20 h 08 min
    • Bonjour!

      Je me demandais à ce propos, le compilo ne transforme pas un while(42) en une seule instruction justement?

      Publié par Moot | 24 janvier 2013, 23 h 50 min
      • En demandant une compilation avec optimisation (-Os par exemple) GCC détecte automatiquement ce genre de conneries choses et les traduits par une boucle infini « classique » comme un for(;;).
        Par contre il y aura quand même un jolie warning pour prévenir que la condition est toujours vrai 😉

        Publié par skywodd | 25 janvier 2013, 21 h 01 min
  4. salut , eh oui trés bon remarque merci bien ^^

    Publié par fahdi | 2 décembre 2012, 22 h 58 min
  5. Pour apporter une contribution à votre article:

    for (i = 0; i < 1000000; i++)
    {
    //Test 628ms à vide
    }

    //Tests:
    volatile uint8_t *registerPIND = &PIND;
    volatile uint8_t *registerPORTD = &PORTD;
    const uint8_t BIT_PORTD0 = 0b00000001;

    PIND = 0b00000001; //691ms
    PIND = _BV (PORTD0); //691ms
    PIND = BIT_PORTD0; //691ms
    *registerPIND = 0b00000001; //691ms
    *registerPIND = _BV (PORTD0); //691ms
    *registerPIND = BIT_PORTD0; //691ms

    PORTD |= 0b00000001; //754ms
    PORTD |= _BV (PORTD0); //754ms
    PORTD |= BIT_PORTD0; //754ms
    *registerPORTD |= 0b00000001; //754ms
    *registerPORTD |= _BV (PORTD0); //754ms
    *registerPORTD |= BIT_PORTD0; //754ms

    //Autres tests avec portée différente:
    *registerPIND = BIT_PORTD0; //691ms (identique à au dessus) avec BIT_PORTD0 CONSTANTE en dehors de la fonction (portée plus grande)
    *registerPIND = BIT_PORTD0; //817ms avec BIT_PORTD0 VARIABLE en dehors de la fonction (portée plus grande)
    *registerPIND = BIT_PORTD0; //1005ms avec registerPIND en dehors de la fonction (portée plus grande)
    *registerPIND = BIT_PORTD0; //1131ms avec registerPIND et BIT_PORTD0 VARIABLE en dehors de la fonction (portée plus grande)
    *registerPIND = BIT_PORTD0; //1131ms avec registerPIND et BIT_PORTD0 VARIABLE en dehors de la fonction (portée plus grande) + initialisation du pointeur registerPIND et de la variable BIT_PORTD0 après déclaration

    Au final ce ne sont pas les pointeurs sur registres qui posent problème au niveau de la rapidité d'exécution, c'est le fait de les déclarer avec une portée globale, initialiser leur valeur après leur déclaration ne rajoute pas de temps d'exécution.
    Pour changer le bit d'un port en étant sûr de l'état actuel du bit, préférer "PINx =" à "PORTx |=" ou "PORTx &= ~", c'est plus rapide sans être extraordinairement plus rapide non plus, faut pas exagérer, mais il y a une petite différence.

    Par contre chose intéressante, si vous voulez passer de l'état 0 à 1 sans savoir si le port est déjà à l'état 1, préférer un seul "PORTx |=" plutôt qu'un contrôle de l'état 0 du port dans une condition logique, puis "PINx =", ce sera un peu plus rapide avec "PORTx |=" directement.

    Sur ce, votre article est très bien, il m'a aidé, je n'utilise pas le framework arduino, par contre je déroge un peu à ce que dit l'article car il y a presque 1 an j'ai décidé de faire mon propre framework (sacrilège non d'une pipe!), ceci dit rien à voir avec le framework arduino niveau performances si on tient compte des bonnes pratiques de programmation.

    J'aime bien votre slogan, "que serait le web sans bidouilleurs !", car cela reflète bien ma philosophie du bricolage maison. Surtout quand on va sur des forums comme futura science avec des mecs qui nous disent qu'il faut payer 10k de certif CEM pour fabriquer une carte électronique sinon on est un con. D’ailleurs, vous aussi le mr qui avez fait cet article, vous êtes un con ! Si si, ce sont les professionnels de futura science section électronique qui vous le disent, votre article c'est de la merde ! :p

    Publié par Mahé | 9 juin 2015, 12 h 14 min
  6. Quoi que, quoi que…. En utilisant l’option -O3 plutôt que -Os d’avr gcc, on se retrouve avec pour 16 millions d’itérations de:
    PIND = 0b00000001; //Temps d’exécution = 11042ms
    myLed.toggle(); //(équivalent de la ligne au dessus mais avec framework perso et donc pointeurs sur registres avec une portée dans toute la classe) Temps d’exécution = 12045ms

    Soit une augmentation de temps de seulement 8.3%.
    Donc en gros les tests au dessus seraient à refaire avec l’option d’optimisation d’avr gcc -O3, mais disons que ces 8.3% de différence inaugurent que du bon si on devait effectivement refaire des tests…

    A vous de tester, à vous de juger, mais pour moi c’est adopté 😉

    Publié par Mahé | 10 juin 2015, 19 h 41 min
  7. …Donc vraiment pour reprendre mes tests ci-dessus avec 1 million d’itérations, mais cette fois-ci avec l’option -O3 d’avr gcc:
    PIND = 0b00000001; //691ms , ça ne bouge pas malgré l’option d’optimisation du code
    *registerPIND = BIT_PORTD0; //753ms avec registerPIND et BIT_PORTD0 VARIABLE en dehors de la fonction (portée plus grande) + initialisation du pointeur registerPIND et de la variable BIT_PORTD0 après déclaration, la effectivement on passe de 1131ms précédemment à 753ms, donc on est pas tout à fait à la vitesse de l’avr pur mais pas si loin 🙂

    On retrouve bien nos ~8.3% d’augmentation à cause des pointeurs sur registre, ce qui est vraiment acceptable 🙂

    Publié par Mahé | 10 juin 2015, 19 h 54 min

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

Skyduino devient Carnet du Maker

Le site Carnet du Maker remplace désormais le blog Skyduino pour tout ce qui touche à l'Arduino, l'informatique et au DIY.