Pages

samedi 28 juin 2014

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

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.

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

Naïvement, l'implémentation serait:

template<T, Deleter = std::default_delete<T>>
class unique_ptr {
  T * m_pointer;
  Deleter m_deleter;

  //...
};

Rien d'extraordinaire.

Cependant, même si le 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*) == 4
  • sizeof(Deleter) == 1
  • sizeof(unique_ptr<T>) == 8

Ouille, méchant padding alors que seule 4 octets sont vraiment utilisés.

La solution ? Une classe 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, les attributs se positionnent au début de la classe.

Cette optimisation se nomme Empty Base Class Optimization (EBCO). Merci Guillaume.

template<T, Deleter = std::default_delete<T>>
class unique_ptr {
  struct Pointer : Deleter {
    T * m_pointer;
    //...
  };
  //...
};
  • sizeof(unique_ptr<T>) == 4

Mieux, non ?

Et si le Deleter est une référence ? Bien entendu, l'héritage ne fonctionne pas, il faut se rabattre sur la première forme (celle naïve ^^).
Avec des traits et un code volumineux cela est "facile".

En allant encore plus loin, on peut utiliser un Deleter personnalisé final.
(Mais qui utilise des classes finales... ?)

À ce moment, évidemment, même remarque que les références, l'héritage ne compile pas et l'utilisation de la première forme s'impose.
Avec un trait is_final c'est possible, mais ce dernier n'existe pas et son implémentation est impossible... On est coincé !

Vraiment ? Non, car d'irréductibles développeurs de compilos ont créé __is_final, une fonction non-standard utilisée notamment par std::tuple.
De là à dire que toutes les implémentations l'utilisent... Il n'y a qu'un pas que je suis prêt à franchir :).
(En C++14 le trait std::is_final devient standard.)

Au final, l'utilisation de std::tuple permet de s'affranchir de ces difficultés tout en optimisant l'espace mémoire1.

template<T, Deleter = std::default_delete<T>>
class unique_ptr {
  std::tuple<T*, Deleter> m_t;
  //...
};

Il y a toutefois un léger problème avec std::tuple. Si un élément doit être initialisé, alors ils doivent tous l'être. Cela peut causer quelques soucis si un des types n'a ni copy-ctor, ni move-ctor.

1 À condition de mettre les types dans l'ordre croissant d'alignement afin de réduire le padding lorsqu'il y en a plus de 2 (falcon::optimal_tuple).

1 commentaire:

Jonathan Poelen a dit…

Je ne connaissais pas le nom, il est pourtant tellement naturel O_O.