RSS link icon

L'ordre des variables dans les struct C/C++

Publication : le 27 févr. 2020

Comment sont gérées les struct en mémoire en C et C++ ? Comment réduire l'empreinte mémoire des structures ? Il y a des choses à savoir !

A l'heure où très peu de développeurs web ne se soucient de l'empreinte mémoire de leurs sites grâce aux technologies fourres-tout (:troll: coucou javascript et ton écosystème :troll:), il parait étonnant de chercher à se pencher sur le problème de l'optimisation de la mémoire en C et C++. Hérétique ! Vive le dieu de la RAM infinie !

Or, lorsqu'on fait de l'embarqué, du temps réel, du traitement d'image, de la 3D et/ou du jeux-vidéo, cette interrogation devient une nécessité. Sinon, vous pouvez être comme moi, fan de l'optimisation ; personne n'est parfait.

Un peu d'exercice

Durant les exercices suivants, nous allons afficher la taille de plusieurs types C++. Or étant très feignant de nature en plus d'avoir horreur de me répéter, je vous propose d'utiliser une macro qui permet d'afficher la taille d'un type et d'afficher son nom :

#include <iostream>
#define printSize(type) \
    std::cout << "Size of " #type " = " << sizeof(type) << std::endl;

Étant donnée une structure telle que définie en C++ :

#include <stdint.h>

struct str1
{
    int8_t var1;
    int64_t var2;
};

Et sachant qu'un int8_t fait 1 octet et qu'un int64_t fait 8 octets, la structure str1 doit faire 1+8=9octets.

Voyons cela avec le programme suivant :

int main()
{
    printSize(int8_t); // affiche 1
    printSize(int64_t); // affiche 8
    printSize(str1); // affiche 9 ?
    return 0;
}

Sa sortie :

Size of int8_t = 1
Size of int64_t = 8
Size of str1 = 16

Comme vous ne vous y attendiez pas (si si), la structure str1 ne fait pas 9 octets mais 16.

Explications

Tout ceci est une histoire de stratégie de rangement de la mémoire du système. Il faut simplement le savoir.

La plupart des ordinateurs modernes allouent leur mémoire par paquet. Sur les systèmes 32bits (x86), ces paquets font 4 octets, sur les 64bits (x86_64), 4 ou 8 octets. Ceci permet à l'ordinateur d'accéder plus facilement et rapidement aux données. Ainsi, il se permettra de mettre du vide à la fin des paquets si ceux-ci ne sont pas complètement remplis par la donnée.

Je vulgarise mais c'est l'idée.

Tip : afin de connaître son architecture sous Linux, tapez la commande suivante :

$ arch || uname -m
x86_64 # dans mon cas

Revenons à notre exemple. Sachant que mon ordinateur est en 64bits, il va chercher à faire des paquets de 4 ou 8 octets :

  • la première variable est un int8_t donc 1 octet. Donc un paquet de 8 octets est créé mais 7 octets sur 8 sont inutilisés ;
  • la seconde variable est un int64_t donc 8 octets. Un paquet de 8 octets est créé et tous les octets alloués sont utilisés.

Nous arrivons donc à nos 16 octets.

En inversant les deux variables, nous obtiendrons également 16 octets, tel que :

struct str1
{
    int8_t var1;
    int64_t var2;
};
struct str2
{
    int64_t var1;
    int8_t var2;
};

int main()
{
    printSize(str1); // affiche 16
    printSize(str2); // affiche 16
    return 0;
}

Nous ne pouvons donc pas optimiser cette structure. Ce n'est pas tout à fait vrai mais passons pour le moment.

Structure optimisable

Affichons maintenant la taille de la structure suivante :

struct str3
{
    int64_t var1;
    int8_t var2;
    int8_t var3;
    int8_t var4;
};

int main()
{
    printSize(str3); // affiche 16
    return 0;
}

Encore 16 octets alloués. Notre machine malgré sa bêtise, a vu clair dans notre jeu. Elle a profité de la place restante de 7 octets après la seconde variable pour y coller les 2 autres variables. Il reste alors 7-1-1=5 octets libres à la fin de cette structure.

Mais que se passe-t-il si nous changeons l'ordre des paramètres, tel que :

struct str4
{
    int8_t var1;
    int64_t var2;
    int8_t var3;
    int8_t var4;
};

int main()
{
    printSize(str4); // affiche ?
    return 0;
}

La sortie :

Size of str4 = 24

La structure devient alors plus grosse en mémoire alors que nous n'avons pas ajouté de nouvelles variables.

Vous l'aurez compris, l'ordre de déclaration des variables dans une structure est important si vous souhaitez optimiser votre programme.

Le code complet

#include <iostream>
#include <stdint.h>

struct str1
{
    int8_t var1;
    int64_t var2;
};

struct str2
{
    int64_t var1;
    int8_t var2;
};

struct str3
{
    int64_t var1;
    int8_t var2;
    int8_t var3;
    int8_t var4;
};

struct str4
{
    int8_t var1;
    int64_t var2;
    int8_t var3;
    int8_t var4;
};

#define printSize(type) \
    std::cout << "Size of " #type " = " << sizeof(type) << std::endl;

int main()
{
    printSize(int8_t);  // affiche 1
    printSize(int64_t); // affiche 8
    printSize(str1);    // affiche 16
    printSize(str2);    // affiche 16
    printSize(str3);    // affiche 16
    printSize(str4);    // affiche 24
    return 0;
}