Optimisations gcc

Rédigé par NaïLyK Aucun commentaire
Classé dans : branlette Mots clés : aucun

Contexte

J'étais sur un réseau de chat en train de parler de l'"""iot""" sur lequel je m'amusais, et l'une des personnes qui s'y connait bien en C a bien voulu relire mon code.

Arrivé ici il m'explique que ce code:

    unsigned char node_addr = (int)(temp.addr.value/2);
    unsigned char sens_addr = (int)(temp.addr.value%2);

n'est pas optimal et qu'il vaudrait mieux utiliser cette variante:

    unsigned char node_addr = (int)(temp.addr.value) >> 1;
    unsigned char sens_addr = (int)(temp.addr.value) & 0x01;

Grosso modo, il y a ici deux calculs en virgule flottante, ou deux calculs avec de simples manipulations de bits.

Sur les microcontroleurs tels que les arduinos, le temps et la taille du code sont deux choses très importantes. Dans ce projet pas vraiment (qui ne remonte que des valeurs de températures à munin), mais parfois il peut être important de recuprer quelques octets en flash, ou encore de gagner quelques cycles CPU.

J'avais cherché un peu sur internet concernant le calcul qui permettait de récupérer le resultat de la division et ils expliquaient que les compilateurs était suffisaement optimisés et évolués pour avoir placé le reste de la division dans un registre mémoire et que ma deuxième ligne serait donc une simple copie d'octet (lien stack overflow).

Nous sommes donc parti à faire de (simples) tests pour vérifier ces optimisations.

Tous les essais suivants sont compilés avec gcc -O9 -save-temps.

Version division:

#include <stdio.h>


int main(int argc, char **argv) {
        int addr;

        if (sscanf (argv[1], "%i", &addr) != 1) {
            fprintf(stderr, "error - not an integer");
        }

        unsigned char node_addr = (addr/2);
        unsigned char sens_addr = (addr%2);

        printf("node: %i\n", node_addr);
        printf("sens: %i\n", sens_addr);


}

Les valeurs ne sont pas en 'dur' dans le code, car GCC est tellement optimisé, que le code ne fait qu'écrire du texte à l'écran (gcc va calculer toutes les valeurs, et le code assembleur ne contient aucun calcul).

Une fois compilé, un code assembleur de 64 lignes est produit:

% wc -l divide.s  
64 divide.s

Version décalage de bits:

#include <stdio.h>


int main(int argc, char **argv) {
        int addr;

        if (sscanf (argv[1], "%i", &addr) != 1) {
            fprintf(stderr, "error - not an integer");
        }

        unsigned char node_addr = (addr)>>1;
        unsigned char sens_addr = (addr) & 0x01;

        printf("node: %i\n", node_addr);
        printf("sens: %i\n", sens_addr);


}

Une fois compilé, un code assembleur de 102 lignes est produit:

wc -l shift.s        
102 shift.s

Donc le calcul en virgule flottante prend mois d'instructions assembleur.

Execution

% time ./divide 15
node: 7
sens: 1
./divide 15  0,00s user 0,00s system 0% cpu 0,003 total
% time ./shift 15
node: 7
sens: 1
./shift 15  0,00s user 0,00s system 0% cpu 0,007 total

 

Nous avons donc un code plus petit avec le décallage de bits, mais plus lent.

 

Pour l'arduino

Mêmes options de compilation: arduino-1.8.4/hardware/tools/avr/bin/avr-gcc -O9 shift.c -o avr_shift --save-temps.

 121 divide.s
 102 shift.s


Nous avons donc le même comportement avec avr-gcc, le code assembleur pour la division est plus gros. Toutefois nous n'avons pas testé de flasher pour mesurer le temps d'execution.

 

Pour info:

% gcc -O0 -save-temps shift.c -o shift
% gcc -O0 -save-temps divide.c -o divide
% wc -l *.s
  73 divide.s
  66 shift.s

 

Les optimisations GCC sont des choses pointues (2 par défaut) et peuvent causer des comportements differents, surtout sur des architectures comme les arduinos où il n'y a pas de noyau par dessus le code pour vérifier qu'il ne fait pas n'importe quoi.

Écrire un commentaire

Quelle est la première lettre du mot wroqxi ?

Fil RSS des commentaires de cet article