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:
Je ne connaissais pas le nom, il est pourtant tellement naturel O_O.
Enregistrer un commentaire