Réduire l'empreinte mémoire d'une agglomération de types

Article suivant: Appel conditionnel de fonction selon la validité d'une expression
Article précédent: 256 couleurs et plus dans la console

Un petit article pour parler d’optimisation mémoire (si on peut appeler ça comme ça) avec comme exemple la structure de donnée utilisée par std::unique_ptr.

Implémentation naïve de std::unique_ptr

Pour rappel, std::unique_ptr prend 2 paramètres template: T et Deleter (qui par défaut égal std::default_delete<T>).

Naïvement, l’implémentation serait:

template<T, Deleter = std::default_delete<T>>
class my_unique_ptr {
  T* m_pointer;
  Deleter m_deleter;
  // …
};

Rien d’extraordinaire.

Cependant, même si Deleter est une classe sans attribut, sa taille est de 1 octet.

À partir d’ici je considère que Deleter est toujours la valeur par défaut, ce qui donne:

  • sizeof(T*) == 8
  • sizeof(Deleter) == 1
  • sizeof(my_unique_ptr<T>) == 16
  • sizeof(std::unique_ptr<T>) == 8

Ouille, méchant padding, alors que seuls 8 octets sont vraiment utilisés.

Comment fait la STL pour “supprimer” 8 octets ?

La bibliohèque standard utilise une optimisation surnommée Empty Base Class Optimization (EBCO). Concrètement, cela se traduit par une classe interne qui contient le pointeur et hérite de Deleter. Les attributs de la classe dérivée vont se mettre après ceux de Deleter, et s’il n’en a pas, ils se positionnent au début de la classe. Grâce à cette astuce, l’adresse du premier membre de la classe (ici, le pointeur) se confond avec celle de la classe englobante et parente, éliminant ainsi l’espace occupé par Deleter.

template<T, Deleter = std::default_delete<T>>
class my_unique_ptr {
  struct internal : Deleter {
    T* pointeur;
  } m_data;
  // ...
};
  • sizeof(my_unique_ptr<T>) == 8

Mieux, non ?

Et si l’héritage n’est pas possible ?

Si le Deleter est une référence ou une classe final, l’héritage ne fonctionne pas. Il faut se rabattre sur la première forme (celle naïve). Avec des traits et un code plus ou moins volumineux, cela est “facile”. Il faut cependant noter que std::is_final n’apparaît qu’à partir de C++14 et son implémentation n’est pas possible en pure C++. Il faut à la place utiliser __is_final qui n’est pas standard.

Toutefois, la STL possède un conteneur générique qui utilise l’EBO si possible: std::tuple. Ce qui permet de s’affranchir de ces difficultés tout en optimisant l’espace mémoire à condition de mettre les types dans l’ordre croissant d’alignement pour réduire le padding entre les membres lorsqu’il y en a plus de 2.

Commentaires

Aucun commentaire pour le moment :'(

Le système de commentaire passe par les issues de github et aucun n'est associée au billet. Vous pouvez faire votre commentaire dans une issue qui a comme titre celui du billet. Je me chargerai de les associer.

Revenir en haut