[prog] Déclaration de variables et mémoire, const volatile static...

Salut à vous tous!

Et bien oui, si j'arrive à faire tourner des codes en hard pur et dur sur mes arduinos, je ne connais que les bases du C / C++ (j'ai fait l'impasse dessus à l'école car j'étais en plein dans le turbo pascal et je ne voulais pas mélanger les deux). Comme vous le savez, je suis en train de programmer un 168, avec sa ram de ouf de 512 octets, et je commence à avoir pas mal de variables.

Ma question : comment optimiser au mieux les déclarations de variables et comment le compilateur "range"-t-il tout ça en mémoire(s) en fonction des déclarations?

En effet, lorsqu'on code, il y a plusieurs façons de déclarer des variables selon ce que l'on veut en faire, et je commence à me perdre.

1 - les constantes : exemple d'une tempo générale...

#define tempo 150
const tempo = 150;
const byte tempo = 150;

dans mon code, j'utilise un peu partout un delay(tempo);.
Je sais que avec le #define, le mot "tempo" est remplacé par "150" avant la compilation, et le code est alors envoyé au compilateur avec des delay(150);, il n'existe pas de variable tempo.
Mais est-ce qu'en utilisant un const ou un const byte, une variable "tempo" en lecture seule est déclarée (et me bouffe de la RAM inutilement, contrairement à un #define)? ou est-ce que le pré-compilateur va traduire mon const byte tempo = 150; en #define tempo 150 et donc ne pas générer de variable?

1bis - les tableaux de constantes

const byte array[5] = {...};

Ca va me donner quoi réellement? 5 octets en RAM que le compilateur va m'interdire de modifier?

2 - le mot clé static

il me semble qu'une déclaration avec static crée une variable en ram qui ne sera accessible qu'au code dans lequel elle a été déclarée et dont l'emplacement (l'adresse) est défini et invariable.

3 - le mot clé volatile

J'ai lu quelque part qu'une variable "volatile" était déclarée en RAM de manière définitive et une fois pour toutes (un peu comme en static), mais ce mot en français me fait penser aux vapeurs comme l’éther, c-à-d que ça se promène sans dire où ça va... un faux-ami? Pour moi, une "variable volatile" en français est une variable qui peut disparaître à tout moment, et réapparaître n'importe-où, comme un oiseau (qui lui est un volatile)... mais c'est pas ça du tout.

4 - "le compilateur optimise la variable"

Qu'est-ce qu'il faut comprendre par là? le compilateur essaie de voir ce à quoi une variable sert réellement et la rend "dynamique" (stack ou après ramend) si c'est possible? (définition française du mot volatile dans ce cas, le p'tit zozio qui se pose de branche en branche...)

5 - PROGMEM

Là, il y a un compromis à trouver entre le gain de ram et le temps de lecture de la variable...

6 - Conclusion : comme vous venez de le lire, j'y comprends rien dans les déclarations de variables. Je voudrais pouvoir coder et savoir réellement avant de compiler quelle taille de RAM va être occupée, et ce qu'il reste comme place pour le stack (donc les paramètres des fonctions, variables locales des fonctions...). Je veut pouvoir imaginer si ma pile (stack) va avoir une taille qui va beaucoup varier ou si justement, cette pile va être optimisée pour ne pas venir recouvrir mes variables...

Je suis preneur de toute info à ce sujet, voire même d'un tuto s'il y a... le net n'est pas très bavard de ce côté...

  1. const ou #define génère le même code (on avait eu une discussion sur ce sujet ans l'ancien forum je ne sais plus où)

  2. static, la variable reste locale mais ne perd pas sa valeur d'un appel à l'autre. Donc ne libère pas la mémoire après le retour dans la fonction appelante. En fin de compte elle se comporte comme une variable globale du point de vue de sa durée de vie mais pas de sa portée.

3)volatile, indique que la variable peut être modifiée à n'importe quel moment et interdit donc son optimisation. utilisé pour les variables modifiées par une routine de traitement d'interruption ou pour une variable qui pointe sur un registre (cas des registres du processeur).

4)"le compilateur optimise la variable", dans certain cas le compilateur peut optimiser l'utilisation d'une variable en ne la créant pas sur le stack mais en la plaçant directement dans un registre du processeur (typiquement une variable d'une boucle for ou alors une variable utilisée dans plusieurs lignes de code consécutives) cela accélère son utilisation.

  1. c'est un choix "lenteur d'accès" vs "occupation mémoire"

  2. il faut aussi prendre en compte les appels de fonctions. A chaque appel de fonction il y a création sur la pile d'une zone contenant l'adresse de retour + création des variables (arguments de la fonction, variables locales, valeurs retournées). Il faut donc faire attention au découpage du programme. Le découpage en petites fonctions d'un point de vue conception est une bonne chose car on peut tester chaque fonction facilement et cela limite les erreurs en contre partie les fonctions qui appellent des fonctions, qui appellent des fonctions, .... bouffent de la place sur la pile.

Salut,

Super_Cinci:
1 - les constantes : exemple d'une tempo générale...

#define tempo 150
const tempo = 150;
const byte tempo = 150;

dans mon code, j'utilise un peu partout un delay(tempo);.
Je sais que avec le #define, le mot "tempo" est remplacé par "150" avant la compilation, et le code est alors envoyé au compilateur avec des delay(150);, il n'existe pas de variable tempo.
Mais est-ce qu'en utilisant un const ou un const byte, une variable "tempo" en lecture seule est déclarée (et me bouffe de la RAM inutilement, contrairement à un #define)? ou est-ce que le pré-compilateur va traduire mon const byte tempo = 150; en #define tempo 150 et donc ne pas générer de variable?

Le mot clef "const" indique au compilateur que la variable en question doit être en "lecture seule".

Suivant le niveau d'optimisation (-O0, -O1, ... -Os) :

  • si les optimisations sont activées :
    const = define (mais avec les info sur le type de la variable en plus) -> 0 octet de ram utilisé
  • si les optimisations sont désactivées :
    const = variable classique en lecture seule -> n octets de ram utilisé

Attention: cela ne s'applique que sur les types de base, char, int, long, ... pas sur les structures, tableaux, ...

Super_Cinci:
1bis - les tableaux de constantes

const byte array[5] = {...};

Ca va me donner quoi réellement? 5 octets en RAM que le compilateur va m'interdire de modifier?

C'est un cas spécial du const.
Les tableaux de const (et par extension les structures const) sont des ensembles de valeurs d'un type défini (ou de plusieurs types défini dans le cas d'une structure ou d'une union).

Avec avr-gcc ces ensembles de const sont stockés en RAM et marqués comment étant en "lecture seul".
Bien que les AVR possèdent une instruction spécial (LPM) pour stocker en flash (PROGMEM) le compilateur ne l'utilise pas (par défaut) pour stocker les tableaux de const.
Contrairement à d'autre compilateurs comme celui pour PIC32 ou ARM qui stocke par défaut les const en flash et non en RAM.

Dans ton exemple (et sur une plateforme AVR avec avr-gcc) tu aurais donc effectivement 5 octets "non modifiable" en RAM.

Super_Cinci:
2 - le mot clé static

il me semble qu'une déclaration avec static crée une variable en ram qui ne sera accessible qu'au code dans lequel elle a été déclarée et dont l'emplacement (l'adresse) est défini et invariable.

Le mot clef "static" fonctionne différemment suivant le contexte :

  • sur une variable globale :
    la variable n'est visible que dans le fichier source en cours de compilation.
    Il est alors possible d'avoir plusieurs variables globale de même nom dans plusieurs fichiers source différents.
    Cela permet aussi au compilateur de faire une meilleur optimisation (pas besoin de s'occuper des accès à la variable depuis l'extérieur).
  • sur une variable locale :
    la variable est initialisé avec la valeur fourni lors du premier appel à la fonction mère, puis elle conserve sa valeur aux appels suivant.
    En gros c'est une variable globale avec une visibilité locale.
  • sur une fonction :
    Même principe que pour une variable globale, la fonction n'est visible que dans le fichier en cours.

Super_Cinci:
3 - le mot clé volatile

J'ai lu quelque part qu'une variable "volatile" était déclarée en RAM de manière définitive et une fois pour toutes (un peu comme en static), mais ce mot en français me fait penser aux vapeurs comme l’éther, c-à-d que ça se promène sans dire où ça va... un faux-ami? Pour moi, une "variable volatile" en français est une variable qui peut disparaître à tout moment, et réapparaître n'importe-où, comme un oiseau (qui lui est un volatile)... mais c'est pas ça du tout.

Toute variables déclaraient comme "volatile" sont (obligatoirement) laissez telle quelle par le compilateur.
Même si tu lui demande d'optimiser à mort ton code il ne touchera pas aux variables déclaré "volatile".

On utilise le mot clef "volatile" sur des variables globale partageaient entre une interruption et une fonction classique.
Si elle n'était pas "volatile" le compilateur l'optimiserait et l'accès à cette variable depuis l'interruption serait alors corrompu.

Super_Cinci:
5 - PROGMEM

Là, il y a un compromis à trouver entre le gain de ram et le temps de lecture de la variable...

D'un point de vue purement assembleur :

  • Lecture dans r0 d'une variable en RAM à l'adresse 0x0042 :
LDS r0, 0x0042
; la valeur de ram[0x0042] est désormais dans r0
  • Lecture dans r0 d'une variable en flash à l'adresse 0x0042 (voir datasheet §26.2) :
LDI r30, $00 
LDI r31, $42 ; r30 & r31 = registre Z
LPM ; r0 <- flash[Z]
; la valeur de flash[0x0042] est désormais dans r0

1 instruction pour la ram VS 3 instructions pour progmem, donc à part si tu travailles avec des timings hyper précis ou avec des boucles trés grosse, c'est quasiment la même chose.

Super_Cinci:
6 - Conclusion : comme vous venez de le lire, j'y comprends rien dans les déclarations de variables. Je voudrais pouvoir coder et savoir réellement avant de compiler quelle taille de RAM va être occupée, et ce qu'il reste comme place pour le stack (donc les paramètres des fonctions, variables locales des fonctions...). Je veut pouvoir imaginer si ma pile (stack) va avoir une taille qui va beaucoup varier ou si justement, cette pile va être optimisée pour ne pas venir recouvrir mes variables...

Oublie l'ide arduino, crée toi un makefile (ou prend en un tout fait : ici dans la partie code) et utilise :

avr-size -C --mcu=atmega328p

Tu auras alors le détail de l'utilisation de ta RAM (statique).
Exemple :

avr-size -C --mcu=atmega328p dcpu.elf
AVR Memory Usage
----------------
Device: atmega328p

Program:    4532 bytes (13.8% Full)
(.text + .data + .bootloader)

Data:       1831 bytes (89.4% Full)
(.data + .bss + .noinit)

.text = code machine
.data = données statique (chaine de char, ...)
.bootloader = zone mémoire pour le bootloader
.bss = données statique/globales non initialisé
.noinit = sous partie de .bss
(cf Memory Sections)

Alors chapeau à vous deux, c'est exactement le genre de réponse que j'attendais!

J'y vois maintenant plus clair.

Dans le cas de mon LCD, j'ai des tableaux de codes de caractères genre byte caractere[2][15][9]. autant donc les déclarer en volatile pour être sûr qu'ils ne "bougent" pas? Vu la moulinette que ça fait pour afficher 5 caractères (exemple : "01234"):

adresse = x * maxY + y;
lcd_change_adresse(adresse);
for(byte i = 0; i < 5; i++){
  for(byte j = 0; j < 9; j++){
    lcd_put_octet (caractere[0][i][j]);
    lcd_change_adresse(adresse);
    adresse++;
  }  
}

mieux vaut éviter le progmem, non?

Dans le cas de fonctions qui utilisent des variables locales de calcul temporaire, pour gagner de la ram, j'airais intérrêt à déclarer ces variables en global et volatile en faisant attention qu'une fonction appelée ne modifie pas une varaible de calcul de la fonction appelante?

Est-ce que je gagne beaucoup à utiliser des passage de paramètres en pointers plutôt que valeur, tant que ça reste compatible, j'ai certaines fonctions incompatibles du genre :

void fonction1(valeur){
  valeur++;
  lcd_put(valeur);
}

Oui, je sais, je cherche l'optimisation à la petite bête... mais l'histoire des paramètres de fonctions, ça ferait gagner du temps aussi, non?

toutes mes question sont soulevées parce que je tente de faire tourner un 168 comme une ferrari de manière presque transparente...

Puis je me dis qu'en optimisation de MON code, je serais certes plus lent que le compilateur, mais au final bien meilleur...

Je plussoie : merci à vous deux pour ces explications très claires.

Sans doute vous connaissez l'existance de la note d'application AVR4027 sur l'optimisation du code.
http://www.atmel.com/Images/doc8453.pdf
Je joins le lien pour ceux qui seraient intéressés.

Juste une petite remarque périphérique : "avr-size -C --mcu=atmegaXXX fichier.elf" peut s'invoquer directement en ligne de commande dans un terminal, le makefile n'est pas obligatoire.
De même Eclipse ou consort peut être configuré pour l'invoquer à chaque compilation.

J'étais passé à côté de cette note d'application relativement récente. En plus elle utilise AVR-GCC 8)

Du même tonneau, il y a aussi la note Efficient C Coding for AVR.

++

Super_Cinci:
Dans le cas de mon LCD, j'ai des tableaux de codes de caractères genre byte caractere[2][15][9]. autant donc les déclarer en volatile pour être sûr qu'ils ne "bougent" pas? Vu la moulinette que ça fait pour afficher 5 caractères (exemple : "01234"):

Si ils ne sont pas utilisés par une fonction d'interruption le "volatile" ne fera qu'aggraver les choses.
Il devrait plutôt être "static const" (static = local dans le fichier + const = constant) afin de laisser le compilateur faire son agencement de manière optimisé.
(le tableau sera toujours stocké de manière contigu quoi qu'il arrive).

Super_Cinci:
mieux vaut éviter le progmem, non?

Tout dépend du rapport taille/nombre d'appel.
Si tu as énormément de données à stocké le PROGMEM sera la meilleur option.
Si tu as peu de chose a stocker mais a appeler souvent la mise en RAM sera l'option la plus rapide.

Super_Cinci:
Dans le cas de fonctions qui utilisent des variables locales de calcul temporaire, pour gagner de la ram, j'airais intérrêt à déclarer ces variables en global et volatile en faisant attention qu'une fonction appelée ne modifie pas une varaible de calcul de la fonction appelante?

Non tu n'y gagneras rien, au contraire tu seras perdant.
L'accès à une variable globale demande 2x plus de temps qu'une variable locale.
En plus si elle est déclarée volatile (= sans optimisation) tu auras une perte de performance dans ton algo.

Super_Cinci:
Est-ce que je gagne beaucoup à utiliser des passage de paramètres en pointers plutôt que valeur, tant que ça reste compatible, j'ai certaines fonctions incompatibles du genre :

Pour l'utilisation des pointeurs c'est pas compliqué :
1 pointeur = 2 octets (sur un AVR)

Donc pour un byte (1 octet) ou un int (2 octets) cela n'a strictement aucun intérêt.
Maintenant si tu utilises des long, float, ... cela reste très moyen d'utiliser des pointeurs.
Au final tu gagnes quelques instructions lors de la copie des arguments, pour les perdre ensuite avec l'accès via le pointeur ...
A pars pour de grosses structures de données ou des cas spécifique demandant l'accès par pointeur cela n'as pas d'intérêt.

Super_Cinci:
Oui, je sais, je cherche l'optimisation à la petite bête... mais l'histoire des paramètres de fonctions, ça ferait gagner du temps aussi, non?

La meilleur optimisation ce n'est pas "static", "volatile", ... c'est de revoir ton code pour qu'il soit le plus efficace possible.

  • Pas de code en double (sinon c'est le signe que le code est mal pensé)
  • Jouer avec les syntaxes du C pour rendre l'optimisation plus poussé (il y a une note d'application d'atmel sur le sujet)
  • Float -> arithmétique à virgule fixe (gain de perf de l'ordre de x4 mais perte de précision)
  • Choix intelligent des types de données (stocker une valeur de 0 à 127 dans un int n'as pas d'intérêt, un uint8_t suffit).
  • Choisir intelligemment ses options de compilation / linker
  • etc ...

68tjs:
Sans doute vous connaissez l'existance de la note d'application AVR4027 sur l'optimisation du code.
http://www.atmel.com/Images/doc8453.pdf
Je joins le lien pour ceux qui seraient intéressés.

Voila c'est de cette note d'application dont je parlai, merci 68tjs pour le lien.

68tjs:
Juste une petite remarque périphérique : "avr-size -C --mcu=atmegaXXX fichier.elf" peut s'invoquer directement en ligne de commande dans un terminal, le makefile n'est pas obligatoire.

Oui mais faire un makefile t'oblige à regarder de plus prés tes options de compilation / linker :wink:
Optimiser un code sans optimiser sa phase de compilation / link ne sert à rien :grin:

SesechXP:
J'étais passé à côté de cette note d'application relativement récente. En plus elle utilise AVR-GCC 8)

Du même tonneau, il y a aussi la note Efficient C Coding for AVR.

Je connaissait pas cette note, ça va me faire un peu de lecture tient :grin:

Salut à tous,

Vos deux liens sont très intéressants. J'ai rapidement parcouru l'ensemble, et j'en conclus que je devrais revoir mon code, et finalement, virer tous mes "volatile", le compilateur fera le reste. Vu que sur mon code, il n'y a pas d'interruptions, c'est tout vu.

Je pense maintenant que je peux optimiser également mes tableaux de constantes, car niveau flash et sram, un tableau de constantes prend autant de place dans la sram que dans la flash (au lancement, le programme initialise chaque case du tableau dans la ram avec une valeur qui est déjà en flash, donc ça vaut le coup d'y réfléchir...)

j'ai de quoi lire et griffonner ce week-end...

Marsi bicou!!!

Super_Cinci:
(au lancement, le programme initialise chaque case du tableau dans la ram avec une valeur qui est déjà en flash, donc ça vaut le coup d'y réfléchir...)

Oui, du reste tu peut exécuter du code avant le main() et la copie en RAM des données statique de .bss grâce à l'attribut :

__attribute__((constructor));

salut a tous , je deterre le sujet , mais je me pose une certaine question , j'ai eu bo relire ce sujet ça me taraude toujours donc :

avec la nouvelle version de l'IDE (1.6) on peut voir le pourcentage de Ram alloué au variable local et globale, je travaille sur une Mega et
depuis pas mal de temps j'essaye de limité l'impacte de mon code sur la Ram et jusqua présent je me prenai la tete a decalré des tabeau buffer , type char ou int ( pour les structure) en global pour limiter l'impacte sur la RAM mais j'ai bien l'impréssion que c'est une abération en fait .

exemple deux fonction travaillant sur la lecture de données en PROMEM recupére ces dites donnés dans un tableau de type char :

char buffer_30[30];

et cela definis dans chaque fonction en local.

void ma fonction 1(){char buffer_30[30];};
void ma fonction 2(){char buffer_30[30];};

je pensai qu'en ne faissant qu'un tableau buffer defini global je limitérai l'impact mémoire en divisant par deux mais au contraire l'information donné en fin de compilation semble impacter la RAM au lieu de l'allégé.
il me semble que les variables local sont libéré ,c'est la que ça me bloc, sont t'elle complétement éffacé et l'espace est il rendu libre pour d'autre initialisation local,a la fin de chaque fonction?
mais il m'avais semblé lire ( je ne retrouve pas ) que les ATMEGA de par leur architecture ne libéré pas correctement la mémoire ( notion de new et delete)?

du coup je me dit que je melange certainement les chose et que tout mes buffer (que je me suis pris la tete a cree pour optimiser le code) n'optimise rien bien au contraire et que tout mes tableaux ou structure de travail temporaire devrai etre defini en local ?
quel sont vos avis la dessu ?

y'a til un impact a cause/grace au nouveau compilateur?

merci!
kris

Bonjour,

Je ne comprends pas bien ta description : si ton buffer est global, pourquoi serait-il alloué pour chaque fonction qui l'utilise ?

Une chose à savoir : toute variable non globale est allouée dans la pile (+ allocation dans le tas pour les objets créés par new ou malloc()). L'avantage d'une allocation locale est que cette mémoire est libérée quand le bloc est terminé (un bloc entre accolades ou une fonction). La RAM utilisée dépend donc du contexte d'utilisation, tout ne sera pas réservé immédiatement.

Une autre chose : partager un buffer de char globalement n'est peut-être pas une bonne idée, sauf c'est en const char. Sinon, tu auras la fonction 1 qui modifie ce buffer commun, tandis que la fonction 2 le modifie autrement, ce serait le boxon.

Desoler je réexplique a la base j'initialisais, mes buffers dans chaque fonction , mais par la suite en pensant libérer de l'espace memoire RAm j'ai supprimé mes buffers locaux pour n'utilisé qu'un buffer global .

je sait que ça peut poser probléme j'en ai fait les frait plusieur fois du coup je me forcé a surveiller la reinitialisation (passage a 0 de toute les cases) avant l'utilisation par une fonction. croi moi j'y fait gaf!

Xavier , as tu un site a me conseiller pour mieux comprendre ces notion de tas et de pile dont je ne saisi pas grand choses?

je croi que je vient de comprendre :
http://mchobby.be/wiki/index.php?title=Arduino_Memoire_Optimiser_SRAM

en gros en passant mes bufer locaux en un bufer global j'ai definitivement reservé de l'espasse mémoire , qui n'est plus dirrectement utilisable.
si je ne m'abuse cela ne change pas grand chose en definitive ( sauf la posibilité de faire des erreur) car au lieu de travailler en local j'utilise une zone global comme tampon.

j'imagine que ça peut avoir de l'iportance si l'on a besoin dans une fonction ou a un moment donné d'espace de memoire vive ( stoké sur la pile), dans ce cas d'un point de vu general il vaut mieux alouer localement( sur la pile) dans chaque fonction et non globalement( sous le tas) de maniere a ce que un plus grand espace memoire vive soit disponible en permanance, non?

Ce sujet est trés instructif .

J'ai défini un buffer global ainsi en début de mon programme:
char buff[20];

Il est défini avec 20 car mon écran lcd fait 20 x4 .

Dans une autre partie de mon code, je l'utilise ainsi mais aurait besoin d'une taille moindre:

char * ElapsedTime(int t)
{
//static char buff[12];
  t = t % 3600;
  int m = t / 60;
  int s = t % 60;
  sprintf(buff, "%02d:%02d", m, s);
  return buff;
}

...
lcd.print(F("Time:"));lcd.print(ElapsedTime(millis()/1000));
...

Est-il possible de redimensionner le buffer dans le but de gagner en taille mémoire ?

Salut,

Dans le cas que tu présentes il suffit de le déclarer en local et pas en static. La variable sera libéré à la fin de la fonction, donc gain en mémoire au final.

Il faut au maximum privilégier les variables locales, à l'exception de certains cas précis. On utilise de la RAM qu'au moment opportun (elle redevient dispo une fois son environnement clos) et ça éclairci le code