Pages: [1]   Go Down
Author Topic: [prog] Déclaration de variables et mémoire, const volatile static...  (Read 1320 times)
0 Members and 1 Guest are viewing this topic.
Bretagne
Offline Offline
Edison Member
*
Karma: 10
Posts: 1294
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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é...
Logged

France
Offline Offline
Faraday Member
**
Karma: 23
Posts: 3010
There is an Arduino for that
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.

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

6) 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.
Logged

France
Offline Offline
Faraday Member
**
Karma: 52
Posts: 5341
Arduino Hacker
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Salut,

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, ...

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.

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.

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.

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 :
Code:
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) :
Code:
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.

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 :
Code:
avr-size -C --mcu=atmega328p

Tu auras alors le détail de l'utilisation de ta RAM (statique).
Exemple :
Code:
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 http://www.nongnu.org/avr-libc/user-manual/mem_sections.html)
Logged

Des news, des tuto et plein de bonne chose sur http://skyduino.wordpress.com !

Bretagne
Offline Offline
Edison Member
*
Karma: 10
Posts: 1294
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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"):

Code:
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 :

Code:
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...
Logged

France S-O ou exil en IDF
Offline Offline
Edison Member
*
Karma: 25
Posts: 1905
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
Logged

Offline Offline
Full Member
***
Karma: 0
Posts: 224
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

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

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

++
Logged


France
Offline Offline
Faraday Member
**
Karma: 52
Posts: 5341
Arduino Hacker
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

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).

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.

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.

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.

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 ...

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.

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 smiley-wink
Optimiser un code sans optimiser sa phase de compilation / link ne sert à rien smiley-mr-green

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

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 smiley-mr-green
Logged

Des news, des tuto et plein de bonne chose sur http://skyduino.wordpress.com !

Bretagne
Offline Offline
Edison Member
*
Karma: 10
Posts: 1294
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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!!!
Logged

France
Offline Offline
Faraday Member
**
Karma: 52
Posts: 5341
Arduino Hacker
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

(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 :
Code:
__attribute__((constructor));
Logged

Des news, des tuto et plein de bonne chose sur http://skyduino.wordpress.com !

Pages: [1]   Go Up
Jump to: